Parsing / Сбор информации, Анализ данных

Пишем кросплатформенный многопоточный парсер на языке Scala

Время прочтения: 4 мин.

Scala — это современный мультипарадигмальный язык программирования, разработанный для выражения общих концепций программирования в простой, удобной и типобезопасной манере. Scala полностью совместим с популярной средой Java Runtime Environment (JRE), языком Java и его библиотеками.

Рассмотрим пример написания многопоточного парсера для сайта магазина РЕТ — ret.ru.

В качестве библиотеки для парсинга возьмём библиотеку scala-scrapper. В ней используются популярные Java библиотеки для парсинга — JSOUP и HTMLUnit.

Создавать проект удобнее всего в IntelliJ IDEA. Для сборки проекта используем SBT – систему сборки проектов на языке Scala. Так же можно выбрать другие системы сборки, например Gradle.

Достаточно выбрать SBT при создании проекта и IDEA сгенерирует все необходимые файлы. В автосгенерированном в корне проекта файле build.sbt пропишем следующую конфигурацию:

name := "Parser" //имя проекта

version := "1.0" //версия проекта

scalaVersion := "2.13.1" //версия языка

libraryDependencies ++= Seq(
  "net.ruippeixotog" %% "scala-scraper" % "2.2.0",
  "org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0") //подключаемые библиотеки

Таким образом подключаются библиотеки и указывается версия программы, название и версия языка Scala.

Создадим основной объект Main, необходимый для запуска программы:

object Main extends App {
	println(“Hello World!”)
}

Наследование Main от App позволяет не писать классическую функцию main, которая обычно необходима для запуска программы.

Будем считывать позиции для поиска из txt файла с помощью следующей функции:

def readFile(path: String): List[String] = {
    val source = Source.fromFile(path)
    val items = source.getLines().toList
    source.close()
    items
}

В path будем передавать путь к txt файлу. В Scala не используется привычное return для обычного возврата значений из функции, а возвращается значение последней строки в функции. В данном случае это item — список с позициями для поиска. Напишем функцию непосредственно для парсинга:

def getParsedData(item: String): Try[String] = {
  try {
    val url = shttps://www.ret.ru/?search=$item 
    val browser: Browser = JsoupBrowser() //экземпляр браузера
    val page = browser.get(url) //получаем веб-страницу
    val retPrice = page >?> extractor("span.full_price", text) //находим цену по тегу
    Success(retPrice.get) //возвращаем значение цены
  } catch {
    case e: Exception =>
      println(e.getMessage) //выводим ошибку в консоль
      Failure(e) //возвращаем ошибку
  }
}

В функции сразу ловим возможные ошибки при получении данных. Функция возвращает специальный тип Try[String], который может содержать успешный результат (Success) и ошибки (Failure). Внутри функции создаём экземпляр Jsoup браузера для парсинга и с помощью browser.get(url) получаем веб-страницу.

Затем с помощью метода extractor находим цену на товар, которая имеет класс full_price и находится в span элементе в полученном html коде.

В данном случае для получения контента нет необходимости исполнять JS код сайта, в противном случае необходимо использовать не JSOUP, а HTMLUnit.

Также напишем функцию для запуска парсинга:

def runParser(items: List[String]) : Unit = {
  val results: ParSeq[Try[String]] = items.par 
    .map(item => getParsedData(item)) //передаём наименования в функцию
    .filter(x => x.isSuccess) //фильтруем результаты
  results.foreach(println) //выводим результаты в консоль
}

Функция принимает список позиций. С помощью метода .par распараллеливаем список и передаем его значения в функцию getParsedData с помощью метода .map. Метод .map принимает анонимную функцию, которая преобразует каждый элемент коллекции. При таком подходе программа автоматически определит на сколько потоков разбить вызов функции, в зависимости от доступных аппаратных ресурсов, и запросы будут выполнять параллельно.

Полученные результаты фильтруем методом .filter, чтобы оставить только успешные запросы. Для этого у каждого элемента вызываем метод .isSuccess, отсеивая таким образом Failure. Функция ничего не возвращает (тип Unit), а просто выводит результаты в консоль.

Осталось добавить несколько строк в Main в которых читаем файл и запускаем парсинг.

val items = readFile(path) //получаем наименования из txt файла
runParser(items) //запускам парсинг

Для удобства запуска программу можно собрать в .jar файл, включающий все необходимые библиотеки и запускающийся на разных ОС.

Удобнее всего это сделать встроенными средствами IDEA. Сначала в Project Structure -> Artifacts выбираем обычный JAR с опцией From modules with dependencies и указываем Main.

Затем достаточно выбрать Build -> Build Artifacts и IDEA создаст JAR который можно запустить из командной строки.

Подробнее ознакомиться с инструментами можно по ссылкам:

https://scala-lang.org/ — официальный сайт языка Scala

https://github.com/ruippeixotog/scala-scraper — библиотека для парсинга

Советуем почитать