Статьи

Фильтрация исходного кода с использованием шаблона

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