Я большой поклонник приложения для 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/