Работа с регулярными выражениями в Scala
27 марта 2013
Недавно мне захотелось прикрутить к блогу виджет, содержащий список самых просматриваемых заметок. Поскольку WordPress не собирает соответствующую статистику, количество просмотров предполагалось брать из LiveInternet. Обычная задача из серии «скачать и пропарсить», берем Perl и вперед. Но ведь мы уже много раз так делали, не интересно. Давайте лучше посмотрим, как справится с этой задачей Scala.
Спустя какое-то время, у меня получилась такая программа:
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>")
}
Давайте попробуем в ней разобраться:
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 нам уже знакомы.
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-циклы:
1
2
3
scala> for(x <- 4 until 1 by -1) println(x)
4
3
2
А так в Scala выглядят генераторы списков:
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))
Учитывая вышесказанное, вы должны с легкостью прочитать код:
yield (url, count.split("").filter(_ != ",").mkString.toInt)
В строке data находятся все совпадения с регулярным выражением re. Регулярные выражения в Scala также являются и экстракторами, благодаря чему мы легко и непринужденно помещаем в переменные url и count совпадения, соответствующие первой и второй паре скобочек в re.
Затем для каждого найденного совпадения мы генерируем пару (кортеж из двух элементов), содержащую url, а также count, из которого отфильтровываются запятые, после чего count преобразуется в число:
count: java.lang.String = 1,234
scala> count.split("").filter(_ != ",").mkString.toInt
res2: Int = 1234
Кортежи в Scala похожи на кортежи в Haskell. Они неизменяемы, и, в отличие от списков, могут содержать элементы различных типов:
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
Метки: Scala, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.