← На главную

Работа с регулярными выражениями в Scala

Недавно мне захотелось прикрутить к блогу виджет, содержащий список самых просматриваемых заметок. Поскольку WordPress не собирает соответствующую статистику, количество просмотров предполагалось брать из LiveInternet. Обычная задача из серии «скачать и пропарсить», берем Perl и вперед. Но ведь мы уже много раз так делали, не интересно. Давайте лучше посмотрим, как справится с этой задачей Scala.

Спустя какое-то время, у меня получилась такая программа:

package me.eax.mostviewed import scala.io._ import scala.util.matching.Regex._ object Application extends App { def buildStatUrl(domain: String, date: String) = { s"http://www.liveinternet.ru/stat/${domain}/" + s"pages.html?date=${date}&period=month&total=yes&per_page=100" } def fetchStatUrl(domain: String, date: String) = { val url = buildStatUrl(domain, date) Source.fromURL(url).mkString } def getStat(domain: String, date: String) = { val data = fetchStatUrl(domain, date) val re = """(?s)for="id_\d+"><a href="([^"]+)"[^>]*>.*?⏎ <td>([\d,]+)</td>""".r for (re(url, count) <- re findAllMatchIn data) yield (url, count.split("").filter(_ != ",").mkString.toInt) } def getTitle(url: String) = { val data = Source.fromURL(url).mkString val reStr = """<h2>(.*?)</h2>""" reStr.r findFirstMatchIn data match { case Some(x: Match) => x group 1 case None => throw new RuntimeException(s"$url does not match $reStr") } } def numberOfViews(views: Int) = { val end = { val rem100 = views % 100 if(5 <= rem100 && rem100 <= 20) "ов" else { val rem10 = views % 10 if(rem10 == 1) "" else if(2 <= rem10 && rem10 <= 4) "а" else "ов" } } "$views просмотр$end" } if(args.size < 3) { println("Usage: mostviewed <domain> <date> <number>") sys.exit(1) } val Array(domain, date, numberStr, _*) = args val number = numberStr.toInt println("<ul>") for((url, views) <- getStat(domain, date) .filter(_._1 != s"http://${domain}/") .take(number)) { println(s"""<li><a href="${url}">${getTitle(url)}</a>, """ + s"${numberOfViews(views)} за месяц</li>") } println("</ul>") }

Давайте попробуем в ней разобраться:

def buildStatUrl(domain: String, date: String) = { s"http://www.liveinternet.ru/stat/${domain}/" + s"pages.html?date=${date}&period=month&total=yes&per_page=100" } def fetchStatUrl(domain: String, date: String) = { val url = buildStatUrl(domain, date) Source.fromURL(url).mkString }

Тут для нас нет ничего нового. Интерполяция строк и объект-одиночка Source нам уже знакомы.

def getStat(domain: String, date: String) = { val data = fetchStatUrl(domain, date) val re = """(?s)for="id_\d+"><a href="([^"]+)"[^>]*>.*?⏎ <td>([\d,]+)</td>""".r for (re(url, count) <- re findAllMatchIn data) yield (url, count.split("").filter(_ != ",").mkString.toInt) }

Строки в Scala имеют метод .r, который компилирует строку в регулярное выражение (экземпляр класса Regex). Синтаксис регулярных выражений в Scala такой же, как и в других языках. Благодаря тройным кавычкам мы можем писать регулярные выражения, не экранируя обратные слэши, как это приходится делать в Java.

Класс Regex имеет методы findAllIn, findAllMatchIn, replaceAllIn и другие. Полный перечень методов и их описание вы найдете в официальной документации. Синтаксис Scala позволяет использовать методы, как операторы. Другими словами, выражения re findAllMatchIn data и 1 + 2 эквивалентны re.findAllMatchIn(data) и (1).+(2) соответственно. Исключение из этого правила составляют методы, имена которых заканчиваются двоеточием. Эти методы правоассоциативны. Другими слоавами, 1 :: Nil эквивалентно Nil.::(1). Если вы недоумеваете, почему так, обязательно помедитируйте над этим вопросом, это занятно.

Оператор for в Scala имеет двойное назначение. Он предназначен как для объявления циклов, так и для замены генераторов списков. Например, так объявляются простые for-циклы:

scala> for(x <- 1 to 3) println(x) 1 2 3 scala> for(x <- 4 until 1 by -1) println(x) 4 3 2

А так в Scala выглядят генераторы списков:

scala> for (x <- 1 to 3) yield x res0: scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3) scala> for { | x <- 1 to 3 | y = x * x | z <- 1 to 10 | if (y + z) % 7 == 1 | } yield (x, z) res1: scala.collection.immutable.IndexedSeq[(Int, Int)] = Vector(⏎ (1,7), (2,4), (3,6))

Учитывая вышесказанное, вы должны с легкостью прочитать код:

for (re(url, count) <- re findAllMatchIn data) yield (url, count.split("").filter(_ != ",").mkString.toInt)

В строке data находятся все совпадения с регулярным выражением re. Регулярные выражения в Scala также являются и экстракторами, благодаря чему мы легко и непринужденно помещаем в переменные url и count совпадения, соответствующие первой и второй паре скобочек в re.

Затем для каждого найденного совпадения мы генерируем пару (кортеж из двух элементов), содержащую url, а также count, из которого отфильтровываются запятые, после чего count преобразуется в число:

scala> val count = "1,234" count: java.lang.String = 1,234 scala> count.split("").filter(_ != ",").mkString.toInt res2: Int = 1234

Кортежи в Scala похожи на кортежи в Haskell. Они неизменяемы, и, в отличие от списков, могут содержать элементы различных типов:

scala> val t = (123, "aaa") t: (Int, java.lang.String) = (123,aaa) scala> t._1 res27: Int = 123 scala> t._2 res28: java.lang.String = aaa scala> t._1 = 456 <console>:8: error: reassignment to val t._1 = 456

Для доступа к N-му элементу кортежа используется метод _N. Нумерация элементов начинается с единицы.

Думаю, теперь вы можете самостоятельно разобраться в остальном коде. Полную версию исходников, включающую файл build.sbt и тому подобное, вы найдете в этом архиве.

Дополнение: Строим диаграммы с помощью Scala Chart