Недавно мне захотелось прикрутить к блогу виджет, содержащий список самых просматриваемых заметок. Поскольку 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