Pull to refresh

Readability своими руками

Reading time5 min
Views22K
Поскольку побеждать Великий Китайский Роскомнадзор наша штука для обхода блокировок в интернете пока не особенно научилась, а рассказать что-нибудь странное про свою работу все равно хочется, расскажу про реимплементацию похожего на Readability алгоритма при помощи Node.js и Бэйцзинского технологического института.

Что это вообще такое


Readability — это радикальное продолжение идеи AdBlock убирать с веб-сайтов лишние элементы. Там, где AdBlock старается снести только самые бесполезные для пользователя вещи (в основном рекламу), Readability удаляет заодно скрипты, стили, навигацию и все остальное ненужное. Раньше такой вид страницы называли «версия для печати», хотя на самом-то деле текст предназначен для чтения (отсюда название Readability – «Удобочитаемость»).

Лирическое отступление про парсеры


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

Вырожденный случай обладания всеми знаниями – это парсер какого-то одного сайта. Т.е. если мы хотим воровать статьи с Хабрахабра, например, чтобы распечатывать их ночью на струйном принтере и приносить в жертву Сатане – мы можем посмотреть на существующую верстку и легко определить, что заголовок поста это h1.title.

Программа, написанная таким способом, почти не будет ошибаться; для каждого отличного от Хабрахабра сайта придется писать новую программу.

Вырожденный идеальный случай: парсер вообще не знает, в каком формате он получил данные. Пример такой программы – strings (существует в большинстве неигровых операционных систем).

Если применить strings к какому-то не предназначенному для чтения файлу, можно получить список всего, что похоже на текст внутри этого файла. Например, команда
strings `which ls`
напечатает ворох строк для форматирования внутри бинарника ls, и справку.

%e %b %T %Y 
%e %b %R 
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]

Чем меньше знаний, тем универсальнее парсер.

Что уже есть


Исходники первой версии Readability опубликованы и являют собой леденящий душу клубок регулярных выражений. Это само по себе неплохо, но частные случаи просто аховые. Мне бы хотелось алгоритм, который обладает гораздо меньшими знаниями о популярных сайтах в интернете (см. выше, «лирическое отступление»).

Актуальная версия Readability закрыта и увешана плюшками разнообразной востребованности. Есть API.

Существует форк первой версии Readability компанией Apple (функция Reader в браузере Safari). Исходный код не очень-то открыт, но посмотреть на него можно, там еще больше регулярных выражений и частных случаев (например, есть такая переменная – isWordPressSite).

Проблемы оригинального скрипта – сложность модификации, аркадная эвристика. В основном работает, но требует нетривиальной доводки напильником. Версия Apple еще и лицензирована непонятно.

Что надо написать


Парсер сайтов, обладающий минимальными знаниями о разметке. Входные данные – одна страница сайта, или фрагмент страницы. Результат – текстовое представление входных данных.

Важный критерий – универсальность: программа будет работать и на клиенте, и на сервере. Поэтому мы не привязываемся к существующим реализациям DOM, а строим свою структуру данных (она еще и работает быстрее, чем полноценный DOM, т.к. данных нам нужно с гулькин, допустим, нос).

По этой же причине программа не будет уметь самостоятельно скачивать страницы из интернета, хранить результаты на диске, обладать пользовательским интерфейсом, вышивать крестиком.

Жизнь и приключения алгоритма


В поисковике нашлось несколько статей на тему алгоритмизации описанного выше процесса. Больше всего мне приглянулись вот эти китайцы PDF.

Формулы у меня получились немного другие, поэтому я расскажу кратенько про свой вариант китайского алгоритма.

Для каждого тега в документе:
  1. Считаем оценку

    Здесь chars – количество текста (символов) внутри тега, hyperchars – количество текста в ссылках, tags – количество вложенных тегов (все метрики рекурсивные).
  2. Считаем сумму оценок
    Сумма оценок детей первого поколения (т.е. не рекурсивно).
  3. Нашли тег с максимальной суммой
    Это с высокой вероятностью контейнер основного текста. Или самый длинный комментарий. В любом случае там внутри буквы, это классно.

Простор для трудового подвига


Дальше оптимизация. Я опишу несколько случаев, но вообще это самая интересная тема, можно в комментариях поболтать.

Мусор в основном тексте. Всякие горе-блогеры любят прямо в тело поста засунуть многочисленные кнопки своих социальных вконтактов, твиттеров и т.п. ненужные вещи. У таких кнопок оценка (score, см. выше) стремится к нулю, по этому принципу их можно сносить.

Я на всякий случай проверяю еще, что после удаления мусора оценка родителя выросла, если нет (или выросла несущественно) – то не удаляю, мало ли что там.

HTML. В алгоритме не используются знания о структуре документа, их можно теперь добавить, чтобы улучшить (или ускорить) работу программы. Т.е., допустим, пессимизировать заранее <footer> и <nav>, или добавлять к невидимым элементам (в браузере) аннотации и пропускать их совсем – тут реально простор для деятельности, я пока ничего не реализовал.

Текстовые сигналы. Если в тексте присутствуют запятые, точки и другие знаки препинания, это с большой вероятностью связный текст (в отличие от навигации, например). Такая эвристика была в Readability.

Тут надо обратить внимание на то, что знаки препинания в разных языках все-таки разные, и запятые в китайском ("," Unicode U+FF0C) отличаются от символа "," (ASCII 44).

Что получилось, как пользоваться


Результат я назвал незатейливо readability2, выложил в npm.

Кратенько про тесты


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

Тут возникает некая проблема: тест readability – это сохраненная страница совершенно постороннего сайта, плюс выдранный с нее же руками «эталонный» текст. Я не очень понимаю, как распространять это таким способом, чтобы правоторговцы не стремились меня уничтожить за незаконное копирование сайтов и текстов.

Если кто-то знает правильный ответ, напишите, пожалуйста, в комментариях. Сейчас тесты живут в закрытом репозитории, но им очень хочется на свободу.

Исходники без тестов: GitHib

Пример использования


Для иллюстративных целей я написал страничку demo.html, в которой две строки текста среди всякой навигации.

Текст называется «Название». Содержательная часть:
Весь микрорайон тихонько чудо божье наблюдал:
Поп Игнатий тилибонькал свой церковный причиндал.
(Public domain)
К слову, я отказываюсь от имущественных авторских прав на это литературное произведение, передавая его таким образом в общественное достояние (public domain). Распространять и использовать полный текст теперь могут все без ограничений.

Таким должен быть результат прогона программы. Если результат не такой, значит, все сломалось.

А вот исходник программы demo.js с комментариями. Используется парсер sax авторства Isaac Z. Schlueter.

Документация, она же API


Конструктор:

var reader = new Readability

Ничего не принимает.

SAX интерфейс:

reader.onopentag(tagName)       // <тег>
reader.onattribute(name, value) // атрибут=значение
reader.ontext(text)             // текст
reader.onclosetag(tagName)      // </тег>

Тут все аргументы – строки.

Чтобы получить результат:
var res = reader.compute(),
    text = reader.clean(res)

На выходе: res.heading – название статьи и text – основной текст без форматирования.

Вместо reader.clean можно написать другой форматтер, тогда будет получаться не текст, а простая разметка, например.

Вывод


Программа работает. Ей пока немного страшно пользоваться, т.к. тестов всего около 20 штук, но я работаю над этим. Обновления будут. Патчи приветствуются, кроме всяких глупых. GitHub. Лицензия MIT, я забыл ее залить в репозиторий.

Важное замечание: картинка слева не имеет никакого отношения к посту. Поэтому если она не загрузилась, и вы не видите слева никакой картинки – не расстраивайтесь.

Лучше напишите в комментариях, что вы обо всем этом думаете.
Tags:
Hubs:
Total votes 58: ↑53 and ↓5+48
Comments13

Articles