Я большой поклонник приложения для iPad Flipboard , особенно его способности отфильтровывать не важный контент на веб-страницах и просто показывать мне основной контент, поэтому я искал библиотеки с открытым исходным кодом, которые предоставляют эту возможность.
Я наткнулся на страницу с кворами, где кто-то спросил, как это делается, и предложенные библиотеки были читабельны , гусь и котельная труба .
Котел был написан Кристианом Кольшюттером, а также имеет соответствующий документ и видео .
На очень высоком уровне это мое понимание того, что делает код:
Он основан на архитектурном стиле конвейеров / фильтров, посредством которого TextDocument пропускается через фильтры, которые выполняют преобразования над ним. После того, как все они были применены, мы можем получить основное содержание статьи с помощью вызова метода.
Я использовал подход «трубы / фильтры», когда играл с clojure / F #, но проблемы, над которыми я работал, были намного меньше, чем эта.
В коде было около 7 или 8 полей, которыми манипулировали, поэтому мне иногда было трудно узнать, как поля могут заканчиваться определенными значениями, которые часто включают просмотр других фильтров и просмотр того, что они сделали с документом.
Я всегда думал, что должна быть возможность просматривать каждый фильтр совершенно независимо, но когда происходит манипулирование состоянием, это не так .
К счастью, у Кристиана есть комментарии в своем коде, которые объясняют, как вы можете снова составлять различные фильтры и почему некоторые фильтры не имеют смысла сами по себе, только если они объединены с другими.
Например, класс BlockProximityFusion , который используется для объединения смежных текстовых блоков, содержит следующий комментарий:
Предохранители соседних блоков, если их расстояние (в блоках) не превышает определенного предела. Это, вероятно, имеет смысл только в тех случаях, когда восходящий фильтр уже удалил некоторые блоки.
Я полагаю, что то же самое можно было бы достичь с помощью некоторых автоматических тестов, показывающих сценарии, в которых составлены разные фильтры.
Кристиан использует логический оператор OR («|») во всей кодовой базе, чтобы гарантировать выполнение всех фильтров, даже если предыдущий успешно внес изменения в документ.
Например, основной точкой входа в код является ArticleExtractor, который выглядит так:
public final class ArticleExtractor extends ExtractorBase { public static final ArticleExtractor INSTANCE = new ArticleExtractor(); public static ArticleExtractor getInstance() { return INSTANCE; } public boolean process(TextDocument doc) throws BoilerpipeProcessingException { return TerminatingBlocksFinder.INSTANCE.process(doc) | new DocumentTitleMatchClassifier(doc.getTitle()).process(doc) | NumWordsRulesClassifier.INSTANCE.process(doc) // cut for brevity | ExpandTitleToContentFilter.INSTANCE.process(doc); } }
Я заметил аналогичную вещь в коде underscore.js, но в этом случае оператор «&&» использовался для выполнения кода справа, только если выражение слева было успешным.
Если мы не используем какие-либо библиотеки, которые имитируют коллекции первого класса в Java (например, totallylazy / Guava ), то что-то вроде этого также может работать:
public final class ArticleExtractor extends ExtractorBase { ... public boolean process(TextDocument doc) throws BoilerpipeProcessingException { List<BoilerpipeFilter> filters = asList(TerminatingBlocksFinder.INSTANCE, new DocumentTitleMatchClassifier(doc.getTitle()), ExpandTitleToContentFilter.INSTANCE); boolean result = true; for (BoilerpipeFilter filter : filters) { result = result | filter.process(doc); } return result; } }
Первоначально я начал просто просматривать код и думал, что примерно понял его, прежде чем понял, что не могу объяснить, что он на самом деле делал. Поэтому я изменил свой подход и начал писать некоторые модульные тесты, чтобы увидеть, каково было текущее поведение.
Из того, что я могу сказать, основной алгоритм в коде содержится внутри NumWordsRulesClassifier, где каждый текстовый блок в документе классифицируется как контент или не контент.
Я написал тесты, охватывающие все сценарии в этом классе, а затем провел рефакторинг кода, чтобы посмотреть, смогу ли я сделать его немного более выразительным. Я закончил с этим :
private boolean currentBlockHasContent(final TextBlock prev, final TextBlock curr, final TextBlock next) { if (fewLinksInCurrentBlock(curr)) { if (fewLinksInPreviousBlock(prev)) { return curr.getNumWords() > 16 || next.getNumWords() > 15 || prev.getNumWords() > 4; } else { return curr.getNumWords() > 40 || next.getNumWords() > 17; } } return false; } private boolean fewLinksInCurrentBlock(TextBlock curr) { return curr.getLinkDensity() <= 0.333333; } private boolean fewLinksInPreviousBlock(TextBlock prev) { return prev.getLinkDensity() <= 0.555556; }
Вся логика основана на проверке текстовых блоков непосредственно перед и после текущего, чтобы определить, будет ли это содержимое котельной пластины.
Логика вокруг следующих / предыдущих текстовых блоков написана весьма императивно и кажется, что ее можно было бы сделать более краткой, используя что-то вроде F # ‘Seq.windowed’ над коллекцией, но я пока не совсем понимаю, как это сделать!
Вы можете прочитать больше об алгоритме на страницах 4-7 статьи .
После запуска кода для нескольких статей, которые я сохранил в ReadItLater, он, кажется, работает достаточно хорошо.
В целом…
Я не читал каждый кусочек базы кода, но из того, что я прочитал, я думаю, что pipepipe — это довольно крутая библиотека, и подход к фильтрации контента аккуратен.
Я нашел особенно полезным иметь возможность читать части статьи, а затем идти и смотреть на соответствующий код. Часто такие вещи остаются в воображении читателя из моего опыта!
С http://www.markhneedham.com/blog/2012/02/13/reading-code-boilerpipe/