Граббер сайта на OwnLang за пять минут

от
Прочие языки    граббинг, ownlang, jsoup

В этой статье я покажу как можно быстро добавить любую Java библиотеку для последующего взаимодействия с ней в OwnLang (Desktop), как вызывать Java-код, на примере библиотеки Jsoup, а также сохранять данные в json и SQLite.


Подключаем Java-библиотеки
Начну, пожалуй, с самого нуля - загружаем последнюю версию (на момент написания статьи это OwnLang Desktop 1.3.0) со страницы релизов на GitHub. Распаковав zip-архив, мы видим две папки:
  - modules - папка дополнительных модулей
  - libs - папка Java-библиотек.

Мы будем работать с библиотекой Jsoup, поэтому качаем последнюю версию и копируем jar-файл в папку libs. Теперь при запуске интерпретатора, мы сможем работать с классами этой библиотеки.


Выбираем цель
Для примера будем получать информацию об Android-библиотеках с сайта https://android-arsenal.com/

Прежде, чем мы начнём, необходимо взглянуть на HTML-код страницы, чтобы узнать, где находятся интересующие нас данные.

HTML-код страницы

Как видно, краткая информация о библиотеке находится в блоке <div class="project-info">.

На странице https://try.jsoup.org/ можно попробовать получить данные онлайн и подобрать селекторы.

Импортируем страницу:
Получение страницы

Получаем:

Список библиотек: .project-info
Список библиотек

Название: .project-info .title a:first-child
Название библиотек

Описание: .project-info .desc
Описание библиотек

Ссылка на готовый пример из try jsoup


Работаем с Java-кодом из OwnLang
Для взаимодействия с Java-кодом воспользуемся модулем java. Для начала проверим, доступна ли нам библиотека Jsoup:
  1. use "java"
  2.  
  3. Jsoup = newClass("org.jsoup.Jsoup")
  4. print Jsoup.name

Если всё нормально, при запуске мы получим строку org.jsoup.Jsoup.


Парсим страницу при помощи Jsoup
Только что мы загрузили класс Jsoup и теперь можем вызывать все его статические методы. Давайте получим код главной страницы:
  1. use "java"
  2.  
  3. Jsoup = newClass("org.jsoup.Jsoup")
  4. doc = Jsoup.connect("https://android-arsenal.com/")
  5.         .userAgent("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36")
  6.         .get()
  7. print doc
Сайт блокирует подключение, если не указан User-Agent, поэтому пришлось притвориться браузером Google Chrome.

Теперь получим список из названий библиотек на странице:
  1. elements = doc.select(".pi").toArray()
  2. for element : elements {
  3.   aElement = element.select(".title a:first-child")
  4.   title = aElement.text()
  5.   println title
  6. }
BreadcrumbsView
android-easy-crash-handle
Twitter Kit for Android

Вместо вывода на консоль, давайте соберём всю информацию в массив:
  1. elements = doc.select(".pi").toArray()
  2.  
  3. use "std"
  4. libs = []
  5. for element : elements {
  6.   aElement = element.select(".title a:first-child")
  7.   title = trim(aElement.text())
  8.   // Пропускаем рекламу
  9.   if (title == "") continue
  10.  
  11.   lib = {}
  12.   lib.title = title
  13.   lib.url = aElement.attr("href")
  14.   lib.tag = trim(element.select(".tags").text())
  15.   lib.desc = trim(element.select(".desc").text())
  16.   // Добавляем в массив
  17.   libs += lib
  18. }
  19.  
  20. println libs

Получили:
[{desc=A customizable Android view..., url=/details/1/4475, tag=Bread Crumbs, title=BreadcrumbsView}, {desc=This is An Easy Android Crash Handle Library..., url=/details/1/4474, tag=Crash Reports, title=android-easy-crash-handle}, ...]

Сохраняем данные в json
Вместо вывода на консоль, можно сохранить данные в виде файла в формате json:
  1. use "json"
  2. use "files"
  3. f = fopen("android-libraries.json", "w")
  4. writeText(f, jsonencode(libs))
  5. flush(f)
  6. fclose(f)


Работаем с базой данных SQLite
Можно поступить иначе, сохранять в базу данных, разбив данные на отдельные таблицы. В нашем случае таблиц можно сделать две: теги (категории) и библиотеки. OwnLang поддерживает MySQL и SQLite, но добавив драйверы других БД, можно расширить этот список. Мы же воспользуемся СУБД SQLite.

Подключаем модуль для работы с базами данных:
  1. use "jdbc"

Создаём подключение к файлу:
  1. conn = getConnection("jdbc:sqlite:android-arsenal.db")

Создаём структуру таблиц:
  1. st = conn.createStatement()
  2. st.executeUpdate("drop table if exists tags")
  3. st.executeUpdate(
  4.   "create table tags (
  5.    id integer primary key,
  6.    name string
  7.  )")
  8. st.executeUpdate("drop table if exists libs")
  9. st.executeUpdate(
  10.   "create table libs (
  11.    id integer primary key,
  12.    title string,
  13.    url string,
  14.    desc string,
  15.    tag integer
  16.  )")

Выполняем специальную SQLite команду, говорящую, что сейчас будет множественная вставка данных (batch insert):
  1. st.execute("begin")

Чтобы не создавать каждый раз Statement для добавления данных, будем переиспользовать PreparedStatement. Заодно получим безопасность типизации (это в sqlite-то? :)):
  1. stTags = conn.prepareStatement("insert into tags(id, name) values(?, ?)")
  2. stLibs = conn.prepareStatement("insert into libs(id, title, url, desc, tag) values(?, ?, ?, ?, ?)")

Перебираем список библиотек и заносим данные по своим таблицам:
  1. tagids = {}
  2. tagsIndex = 0
  3. libsIndex = 0
  4. for lib : libs {
  5.   libsIndex++
  6.  
  7.   // Теги
  8.   if !arrayKeyExists(lib.tag, tagids) {
  9.     // Добавляем новый тег
  10.     tagsIndex++
  11.     stTags.setInt(1, tagsIndex)
  12.     stTags.setString(2, lib.tag)
  13.     stTags.addBatch()
  14.     tagids[lib.tag] = tagsIndex
  15.   }
  16.  
  17.   // Добавляем библиотеку
  18.   stLibs.setInt(1, libsIndex)
  19.   stLibs.setString(2, lib.title)
  20.   stLibs.setString(3, lib.url)
  21.   stLibs.setString(4, lib.desc)
  22.   stLibs.setString(5, tagids[lib.tag])
  23.   stLibs.addBatch()
  24. }

Выполняем множественное добавление и закрываем PreparedStatement:
  1. stTags.executeBatch()
  2. stLibs.executeBatch()
  3. stTags.close()
  4. stLibs.close()

Выполняем специальную SQLite команду, говорящую, что множественная вставка данных завершена:
  1. st = conn.createStatement()
  2. st.execute("end")

Выведем количество добавленных библиотек:
  1. rs = st.executeQuery("select count(*) from libs")
  2. println "Libs: " + rs.getInt(1)

Наконец, закроем соединение с БД:
  1. conn.close()

Вот и всё. Запустив программу, мы увидим:
Libs: 30Теперь можно посмотреть данные в sqlitebrowser или где-нибудь ещё:

sqlitebrowser


Добавляем парсинг страниц
Всё это время мы парсили только одну страницу, так что обернём код в функцию parsePage и будем вызывать для каждой страницы:
  1. libs = []
  2. page = 1
  3. while (true) {
  4.   println "Page " + page
  5.   pageLibs = parsePage(page)
  6.   if (length(pageLibs) == 0) break
  7.   libs <<= pageLibs
  8.   page++
  9.   sleep(1000)
  10. }

После трёхминутного парсинга 148 страниц, получаем сообщение:
Libs: 4399и 835 кб файла базы данных.

Готовая БД

Полный код:
android_arsenal.own
  • +8
  • views 5579