Парсим HTML на Clojure

​Время от времени пробую применить Clojure к реальным задачам из production. Опыта использования Clojure у меня очень мало, но нравится синтаксис, нравится философия, REPL. А что не нравится — так это скудная документация, мало статей, мало туториалов, чтобы прям по шагам для начинающих.

Итак, задача: сделать POST запрос на некий сайт и найти там url ссылки с определённым текстом. POST запрос сделал легко, теперь нужно распарить html. Быстрое гугление clojure parse html даёт несколько популярных вариантов: clj-tagsoup, Enlive и Hickory.

clj-tagsoup

Начал с clj-tagsoup — легковесный парсер основанный на Java библиотеке TagSoup.
Распарил html строку с помощью функции parse-string, получил огромную структуру векторов и мапов, но как найти требуемую ссылку? Совершенно не понятно. CSS селекторов нет, xpath нет, придётся самому писать обход дерева рекурсивными функциями? Хотелось более простого решения. Что ещё удивило, если сделать (parse-string "test"), то получу структуру, которая начинается с html, а потом body, потом мой тег a — библиотека как бы дополнила фрагмент до полного html документа. Бегать по дереву вручную лень, поэтому перешел к следующему парсеру.

Enlive

Для начала изучил документацию Enlive на предмет поддержки селекторов. Они есть и сделаны в форме нативных структур Clojure, что мне понравилось. Говорят, что они даже более мощные, чем CSS селекторы. Но, чего я долго не мог найти в документации, так это примеров куда эти селекторы собственно передавать, чтобы получить набор подходящих узлов из документа? В какую функцию, каким параметром, что на выходе? В документации куча промеров в README.md и в wiki, как заполнить шаблон, вставив какой-нибудь контент по селектору. А вот примеры поиска по селектору нашлись не сразу.

Следующий не очевидный вопрос, а какую собственно функцию для парсинга использовать? Есть deftemplate, template, defsnippet, snippet, html-resource и мутное описание. Методом проб и ошибок в REPL выбрал для себя html-resource. Важный момент, если я хочу распарсить готовую строку, то просто так эту строку в html-resource передать нельзя, нужно обернуть в java.io.StringReader (нет функции типа parse-string):

(def resource
  (enlive-html/html-resource
    (java.io.StringReader. "html code string goes here")))

Теперь попробуем написать селектор, который ищет требуемую мне ссылку по содержимому (т.е. по тексту ссылки), для этого используем text-pred.

Первая попытка:

(enlive-html/select 
    resource 
    [(enlive-html/text-pred #{"MSKU0581488"})])

результат: ("MSKU0581488") — получили сам текстовый узел, но не всю ссылку.

Вторая попытка: нужно искать ссылки (селектор [:a]), которые внутри содержат текстовый узел, отвечающий селектору (enlive-html/text-pred #{"MSKU0581488"}) — для этого подойдёт функция has. При этом два условия (два селектора) нужно объединить логическим И: элемент должен быть ссылкой И внутри содержать определённый текстовый узел — для этого оба селектора нужно написать рядом друг с другом, обернув дополнительными квадратными скобками:

(enlive-html/select 
    resource 
    [[:a 
      (enlive-html/has [(enlive-html/text-pred #{"MSKU0581488"})])]])

результат уже радует:
({:tag :a,
:attrs {:href "http://..."},
:content ("MSKU0581488")})

Вынем содержимое атрибута href:

(:href
  (:attrs
    (first
      (enlive-html/select
        resource
        [[:a (enlive-html/has [(enlive-html/text-pred #{"MSKU0581488"})])]]))))

— это уже полностью рабочий вариант, но надо его сделать clojure-way.

Более стильный вариант (знатоки clojure, прокомментируйте, отредактируйте, пожалуйста):

(-> resource
    (enlive-html/select [[:a (enlive-html/has [(enlive-html/text-pred #{"MSKU0581488"})])]])
    first
    :attrs
    :href)

Оформим в функцию, которая принимает html текст страницы и текст ссылки, возвращает значение атрибута href:

(defn findlink [html linktext]
  (let [resource (enlive-html/html-resource (java.io.StringReader. html))
        selector [[:a (enlive-html/has [(enlive-html/text-pred #{linktext})])]]]
    (-> resource
        (enlive-html/select selector)
        first
        :attrs
        :href)))

Вывод по Enlive: задачу решил, решение нравится, но разобраться с документацией было не просто.

Hickory

Третья популярная библиотека для прасинга html на Clojure (судя по гуглу) — это Hickory. Селекторы в наличии и выглядит гораздо проще, чем Enlive.

Для начала распарсим html строку с помощью hickory.core/parse. Удобно, что нет никаких дополнительных приседаний с java.io.StringReader, в отличие от Enlive. Функция prase возвращает некий документ, который нужно конвертировать в Clojure структуры данных с помощью hickory.core/as-hiccup или hickory.core/as-hickory. Селекторы работают только со структурами в формате as-hickory, поэтому парсим следующим образом:

(def resource 
  (hickory.core/as-hickory 
    (hickory.core/parse "html string goes here...")))

Составим селектор для поиска ссылки по содержимому. Документация простая и полная, селектор для поиска всех ссылок на странице получился элементарным и заработал с первого раза: (hickory.select/tag :a)

Применяем этот селектор так:

(hickory.select/select (hickory.select/tag :a) resource)

и получаем вектор элементов ссылок.

Но вот проблемка, нет никаких селекторов для поиска по текстовому содержимому узлов. Значит будем использовать нативные функции Clojure, например, filter.

(filter 
  #(= ["MSKU0581488"] (:content %))
  (hickory.select/select (hickory.select/tag :a) resource))

получаем структуру
({:type :element,
:attrs {:href "http://..."},
:tag :a,
:content ["MSKU0581488"]})

Наконец оформим в виде функции

(defn findlink2 [html linktext]
  (let [resource (hickory.core/as-hickory (hickory.core/parse html))
        selector (hickory.select/tag :a)
        predicate #(= [linktext] (:content %))]
    (->
      (filter predicate (hickory.select/select selector resource))
      first
      :attrs
      :href)))

Вывод по Hickory: проще чем Enlive, потребовалось гораздо меньше времени до получения результата.

Исходные коды с тестами выложил на GitHub: https://github.com/pqr/htmlparsers-clojure

Оставляйте свои комментарии и предложения, присылайте пул-реквесты, как решить эту задачу в идиоматичном для Clojure стиле?

PHP vs Clojure

Прошел ClojureScript Koans — набор простых задачек/примеров для изучения ClojureScript. Сам по себе ClojureScript — это язык компилируемый в JavaScript, т.е. для браузера или для Node, поэтому сравнение с PHP (чисто backend языком) выглядит странно. Правильнее было бы сравнить PHP и Clojure, который запускается на JVM и предназначен для написания server-side приложений. Однако, что касается синтаксиса и базовых возможностей, здесь ClojureScript ничем не отличается от Clojure. Так что я решил озаглавить этот пост как «PHP vs Clojure«, не смотря на то, что он основан на ClojureScript Koans.

Приведённые в таблице выражения не полностью эквивалентны. Нужно учитывать, что Clojure (и ClojureScript) используют неизменяемые структуры данных, в то время как в PHP некоторые методы меняют массивы по месту (по ссылке) или имеют другие сайд-эффекты (например, изменение внутреннего указателя на текущий элемент).

PHP Clojure
true = true (= true true)
1 + 1 (+ 1 1)
2 != 3 (not= 2 3)
[1, 2, 3, 4, 5] '(1 2 3 4 5) или [1 2 3 4 5]
['key1' => 'value1', 'key2' => 'value2'] {'key1' }
reset([1, 2, 3, 4, 5]) (first '(1 2 3 4 5))
$list = [1, 2, 3, 4, 5]; $rest = $list; array_shift($rest); (rest '(1 2 3 4 5))
count(['dracula', 'dooku', 'chocula']) (count '(dracula dooku chocula))
$list = ['b', 'c', 'd', 'e']; array_unshift($list, 'a'); (cons :a '(:b :c :d :e)) или (conj '(:b :c :d :e) :a)
reset(['a', 'b', 'c', 'd', 'e']) (peek '(:a :b :c :d :e))
$list = [1, 2, 3, 4, 5]; $rest = $list; array_shift($rest); (pop '(:a :b :c :d :e))
null nil
$vector = [111, 222]; $vector[] = 333; (conj [111 222] 333)
$vector = ['penaut', 'butter', 'and', 'jelly'];
$last = end($vector);
(last [:peanut :butter :and :jelly])
['penaut', 'butter', 'and', 'jelly'][3] (nth [:peanut :butter :and :jelly] 3)
array_slice(['peanut', 'butter', 'and', 'jelly'], 1, 3) (subvec [:peanut :butter :and :jelly] 1 3)
[1, 2, 3] === [1, 2, 3] (= (list 1 2 3) (vector 1 2 3))
['a' => 1, 'b' => 2]['b'] (get {:a 1, :b 2} :b) или ({:a 1, :b 2} :b) или (:b {:a 1, :b 2})
$map = ['a' => 1, 'b' => 2]; $result = array_key_exists('c', $map) ? $map['c'] : null; (get {:a 1, :b 2} :c)
$map = ['a' => 1, 'b' => 2]; $result = array_key_exists('c', $map) ? $map['c'] : 'key-not-found'; (get {:a 1, :b 2} :c :key-not-found)
array_key_exists('b', ['a' => null, 'b' => null]) (contains? {:a nil, :b nil} :b)
$map = [1 => 'January']; $map[2] = 'February'; (assoc {1 "January"} 2 "February")
$map = [1 => 'January', 2 => 'February']; unset($map[2]) (dissoc {1 "January", 2 "February"} 2)
$keys = array_keys([2010 => 'Vancouver', 2014 => 'Sochi', 2006 => 'Torino']); sort($keys) (sort (keys {2010 "Vancouver", 2014 "Sochi", 2006 "Torino"}))
$vals = array_values([2006 => 'Torino', 2010 => 'Vancouver', 2014 => 'Sochi']); sort($vals) (sort (vals {2006 "Torino", 2010 "Vancouver", 2014 "Sochi"}))
function square($n) {return $n * $n;}
square(9);
(defn square [n] (* n n))
(square 9)
false === (4 === 5) ? 'a' : 'b' (if (false? (= 4 5)) :a :b)
null === 0 (nil? 0)
nulll === 0 ? 'a' : null (if (nil? 0) :a)
empty([]) (empty? ())
$x === 1 ? "result1" : ($x === 2 ? "result2" : "result3") (cond (= x 1) "result1" (= x 2) "result2" :else "result3"
!(0 === $x) ? "not zero" : "zero" (if-not (zero? x) "not zero" "zero")
switch($x) {case "value1": $result = "result1"; break; case "value2": $result = "result2"; break; default: "default result";} (case x "value1" "result1" "value2" "result2" "default result")
array_map(function($x) {return 4 * $x;}, [1, 2, 3]); (map (fn [x] (* 4 x)) [1 2 3])
array_map('is_null', ['a', 'b', null, 'c', 'd'] (map nil? [:a :b nil :c :d])
array_reduce([1, 2, 3, 4], function($a, $b) {return $a * $b;}, 100) (reduce (fn [a b] (* a b)) 100 [1 2 3 4])
range(1, 4) (range 1 5)
range(0, 4) (range 5)
array_fill(0, 10, "a") (repeat 10 "a")
$result = [];
foreach(['top', 'middle', 'bottom'] as $row) {
foreach(['left', 'middle', 'right'] as $column) {
$result[] = [$row, $column];
}
}
(for [row [:top :middle :bottom] column [:left :middle :right]] [row column]]

Пишем игру на ReactJs с использованием webpack + react-hot-loader

После прослушивания 11-го выпуска подкаста RadioJS и прочтения статьи на хабре про webpack (система управления зависимости для JavaScript) решил попробовать написать что-нибудь простое и записать небольшое видео-скринкаст.

На тему связки webpack с моим любимым ReactJS есть отличный модуль react-hot-loader и статья по нему: Integrating JSX live reload into your React workflow — рекомендую к прочтению, там также есть видео!

В репозитории react-hot-loader можно найти набор шаблонных проектов, так называемых Starter Kits. Взяв один из них за основу и, добавив к нему загрузчик css файлов, у меня получился свой шаблон проекта: https://github.com/pqr/react-hot-boilerplate.

Итого, работа выглядит следующим образом:

  1. клонирую свой boileplate шаблон проекта
  2. запускаю npm install для уставноки всех зависимостей (сам webpack, react, react-hot-loader, style-loader, css-loader)
  3. запускаю webpack-dev-server к которому подключён модуль react-hot-loader (командой npm start)
  4. открываю в браузере локальный адрес запущенного webpack-dev-server и вижу исходное Hello World приложение
  5. любые изменения в app.js (в коде React компонент) автоматически появляются и в браузере. Например, меняю что-то в методе render и сразу вижу это в браузере! Также webpack умеет без перезагрузки страницы обновлять стили при редактировании css файла.

Что не получилось: метод getInitialState() в React компонентах вызывается только один раз при инициализации, поэтому дальнейшие правки в коде никак не отображаются в браузере, нужна ручная перезагрузка страницы.

Видео рекомендую смотреть в Full HD с субтитрами:

Haskell vs PHP

Здравствуйте, меня зовут Пётр и я PHP программист.

Давно уже поглядывал на Haskell, когда-то начинал читать книгу «Изучай Haskell во имя добра», а на днях пробежал по диагонали «О Haskell по-человечески» — действительно по-человечески написана.

Захотелось сделать что-нибудь реальное для production системы! Вспомнил, что недавно писал на PHP парсер простенького текстового формата — из длинного файла получить массив структур (массивов). Алгоритм элементарный — самое то, чтобы попробовать Haskell в деле.

TL;DR: удалось переписать за один рабочий день, включая тест производительности. В итоге получилось три программы: php скрипт в императивном стиле, программа на Haskell и php скрипт в функциональном стиле (калька с Haskell).

Скорость: Haskell программа выполняется быстрее всех, php скрипт в императивном стиле примерно в 6 раз медленнее, а php скрипт в функциональном стиле ещё в 5 раз медленнее (т.е. в итоге 30 раз медленнее чем Haskell). Причём «функциональный php» не может переварить большие объёмы тестов — заканчивается память из-за рекурсии.

Объём кода: php в императивном стиле — 25 строк, Haskell — 40 строк, php в функциональном стиле — 55 строк.

Read More »

Mercurial: MQ в сравнении с Git

Перевод статьи Steve Losh: A Git User’s Guide to Mercurial Queues


Последнее время я всё больше и больше использую Mercurial Queues. Благодаря словам Brendan Cully во время недавнего спринта Mercurial я внезапно понял, что MQ в Mercurial это некая улучшенная версия index из git.

Я захотел написать о схожести этих двух концепций, чтобы пользователи git смогли лучше понять расширение MQ для Mercurial и увидеть, на сколько эта концепция ушла далеко вперёд по сравнению с git index.

Эта статья не является руководством по командам MQ. Это лишь попытка объяснить принципы работы MQ и дать сравнение с git. Полное описание команд вы можете найти в специальной главе hg book.

Read More »

Mercurial: руководство по созданию веток

Перевод статьи Steve Losh: A Guide to Branching in Mercurial


Последнее время я много сидел на irc каналах #mercurial и #bitbucket на freenode и заметил, что часто всплывает вопрос «чем создание веток в Mercurial отличается от git

Какое-то время назад в твиттере я обсуждал с Ником (Nick Quaranto) модель ветвления в Mercurial и git’е, что в итоге вылилось в небольшую заметку об основных отличиях. Я показывал эту заметку в том числе и пользователям git и, похоже, им понравилось. Я решил осветить вопрос более подробно.

Примечание: этот пост не претендует на руководство пользователя по командам в Mercurial. Это руководство описывает лишь концепцию, которая лежит в Mercurial за использованием веток. Если вы ищете описание конкретных команд, почитайте отличное руководство под названием hg book (и, если понравится, вы даже можете купить бумажную версию и увидеть величайший фейл в истории печати).

Пролог

Для начала давайте посмотрим на пример репозитория, который я подготовил:
image
Read More »