describe until element scoring

This commit is contained in:
DarkCat09 2024-12-24 15:46:39 +04:00
commit 059b99b53b
Signed by: DarkCat09
GPG key ID: BD3CE9B65916CD82

160
readability_explained.txt Normal file
View file

@ -0,0 +1,160 @@
Точка входа: Readability.parse() строка 2679
1. Ищем картинки без сорца, ищем noscript с <img> внутри (либо div>div>...>img, если во всех контейнерах больше нет других детей и нет текста), и если этот носкрипт идёт после тега картинки, заменяем картинку на ту, что из носкрипта, с сохранением атрибутов оригинальной
2. Вытаскиваем все <script type="application/ld+json">, убираем CDATA-маркеры по регулярке /^\s*<!\[CDATA\[|\]\]>\s*$/g, проверяем схему JSON-LD и сохраняем себе метаданные страницы в отдельный объект
3. Вот теперь убираем все <script> и <noscript>
** _prepDocument()
4. Убираем все <style>
5. Меняем двойные переносы <br><br> на врап в абзацы <p>
6. Меняем <font> на <span>
**
7. Вытаскиваем метаданные из <meta> с учётом ранее полученного JSON-LD, если он был (данные из него приоритетнее, <meta> только дополняет)
** _grabArticle() -- по сути главный алгоритм
*** Часть 1
Проход по всему дереву DOM, начиная с <html>
let elementsToScore = [];
Функция получения следующего элемента:
1. если есть дети, то переходим к первому из них
2. если есть соседи, то переходим к следующему в дереве
3. иначе поднимаемся наверх к родителям, пока есть куда подниматься (то есть node.parentNode != null) и пока мы последние в соседях (то есть пункт 2 уже выполнялся для этих родительских элементов)
8. Если у элемента:
style.display!=none &&
style.visibility!=hidden &&
нет атрибута hidden &&
(aria-hidden!="true" || есть класс .fallback-image)
то он видимый для пользователя, не трогаем,
иначе удаляем и переходим к следующему (см. функцию перехода)
9. Если у элемента одновременно атрибуты aria-modal=true и role=dialog, то он невидимый, удаляем и переходим
10. Если в метаданных нет автора и мы ещё не встречали и не парсили блок с автором статьи (англ. byline), то проверяем для текущего элемента атрибуты:
rel=author ||
itemprop содержит подстроку "author"
либо
класс или айди матчится по регулярке
/byline|author|dateline|writtenby|p-author/i
и при этом длина текста внутри элемента должна быть меньше 100 знаков (0 > textContent > 100)
тогда этот элемент очень похож на блок с автором статьи, его textContent без лишних пробелов по краям мы сохраняем себе, а сам элемент удаляем из дерева и делаем переход к следующему
11. Если ещё ни разу не встречали <h1> или <h2>, который дублирует заголовок статьи из метаданных, и текущий элемент как раз H1 или H2, то сравниваем его с заголовком, и при совпадении более 75% считаем, что уже встретили на странице дублирующий заголовок, больше не проверяем это условие, а текущий элемент удаляем и делаем переход
Алгоритм проверки совпадения текста: переводим всё в lowercase, разбиваем обе строки textA и textB на массив токенов tokensA и tokensB по регулярке /\W+/g, фильтруя пустые строки (другой вариант, работающий идентично, и не возвращающий пустых строк: находим все вхождения регулярки /\w+/g, то есть match/findall вместо split и \w вместо \W); при пустых массивах токенов сразу возвращаем 0%; ищем уникальные токены textB, то есть которых нет в textA (но Set здесь не нужен, повторяющиеся уникальные токены мы тоже сохраняем, по крайней мере если в точности повторять алгоритм ридабилити); делим сумму длин уникальных токенов textB на сумму длин всех токенов textB, это будет расстоянием; возвращаем (1 - расстояние), то есть 0.25 => 0.75
12. Если класс или айди матчится по регулярке
/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i
и при этом класс и айди НЕ матчатся по регулярке
/and|article|body|column|content|main|shadow/i
и при этом элемент находится не внутри <table> и не внутри <code> (при проверке родителей поднимаемся наверх максимум три раза),
и при этом элемент сам не <body> и не <a>,
тогда это нежелательный элемент, удаляем и переходим к следующему
13. Если атрибут role один из:
menu, menubar, complementary, navigation, alert, alertdialog, dialog,
тогда это нежелательный элемент, удаляем и переходим к следующему
14. Если это <div>, <section>, <header> или <h1-6>, у которого внутри нет никакого текста (кроме пробелов; textContent.trim().length == 0), у которого нет детей либо только <br> и <hr>, то это элемент без контента, уверенно удаляем и делаем переход
15. Если это <p>, <section>, <h2-6>, <td> или <pre>, то добавляем элемент в массив elementsToScore -- их мы точно будем проверять на полезность, это уже не выглядит как мусор в отличие от проверяемых выше условий
16. Если это <div>:
16.1. Весь фразовый контент* собираем в блоки параграфов
Например, эта разметка:
<div>
<span>Hello</span>
World
<b>!!</b>
<a href="#phrasing">abc</a>
<div>Блок 1</div>
<a href="#with-block">
<div>Блок 2</div>
</a>
<canvas></canvas>
n<sup>2</sup> = n * n
</div>
Станет:
<div>
<p>
<span>Hello</span>
World
<b>!!</b>
<a href="#phrasing">abc</a>
</p>
<div>Блок 1</div>
<a href="#with-block">
<div>Блок 2</div>
</a>
<canvas></canvas>
<p>
n<sup>2</sup> = n * n
</p>
</div>
*Phrasing content включает в себя:
- текстовые ноды
- элементы <a>, <del>, <ins> в случае, если они содержат в себе только фразовый контент
- элементы: abbr, audio, b, bdo, br, button, cite, code, data, datalist, dfn, em, embed, i, img, input, kbd, label, mark, math, meter, object, output, progress, q, ruby, samp, select, small, span, strong, sub, sup, textarea, time, var, wbr
Детали реализации см. в строках 1149-1169
Вкратце: цикл по детям div-а:
- если встретили фразовый контент и нет активного параграфа (p==null, то есть либо мы в самом начале, либо после нефразового элемента) и этот элемент/текстнода не пустой и не из пробелов, то создаём параграф в переменной p, вставляем туда этот фразовый контент, а в DOM заменяем элемент на параграф (таким образом враппаем контент)
- если встретили фразовый контент при уже созданном активном параграфе, то просто добавляем его в параграф, без проверок на пустоту-пробелы (хотя полностью пустые можно и пропускать)
- если встретили НЕфразовый элемент при активном параграфе, во-первых, очищаем созданный параграф от пробельных-пустых элементов в конце, во-вторых, убираем активный параграф из временной переменной -- следующий фразовый контент нужно будет добавлять в новый параграф, а не в этот же; текущий параграф уже есть в DOM, мы его не потеряем никак, делаем p=null
16.2. Если div содержит только один параграф и больше ничего, даже нет текстовых нод, то проверяем, чтобы плотность гиперссылок в нём была меньше 25%, и тогда заменяем этот <div> на <p> внутри него (анвраппаем/развёртываем параграф), добавляем параграф в массив elementsToScore
Вычисление плотности гиперссылок: для каждого <a> считаем длину текста и, если это ссылка на #якорь на странице, домножаем длину на 0.3, иначе оставляем как есть (домножаем на 1); делим сумму длин ссылок на длину всего текста
16.3. Если div не прошёл условие 16.2 с <p> и плотностью ссылок, проверяем другой вариант -- если блок не содержит в себе элементов:
div, p, blockquote, dl, img, ol, ul, pre, table,
то есть семантически все его дети -- инлайн-элементы, а не блочные,
то меняем элементу тег с DIV на P и добавляем его в массив elementsToScore
***
*** Часть 2
Скоринг (оценка) найденных полезных элементов
let candidates = [];
17.
***
*** Часть 3
***
**