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

27 марта 2013

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

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.