From 045fc41660de99c62561517d82b87a07d11b0efa Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 28 Apr 2026 21:04:24 +0300 Subject: [PATCH] Refactor loader and processor documentation; enhance index module with README --- instructions/INDEX_README.md | 85 ++++++++++++++++++++++++++++++++ instructions/LOADER_README.md | 9 +--- instructions/PROCESSOR_README.md | 45 +---------------- main.py | 7 ++- modules/index.py | 3 +- modules/loader.py | 3 +- modules/processor.py | 7 ++- 7 files changed, 104 insertions(+), 55 deletions(-) create mode 100644 instructions/INDEX_README.md diff --git a/instructions/INDEX_README.md b/instructions/INDEX_README.md new file mode 100644 index 0000000..1cbeecf --- /dev/null +++ b/instructions/INDEX_README.md @@ -0,0 +1,85 @@ +## Как работает index + +Index находится в `modules/index.py`. Его задача: взять исходный текст УК РФ и построить предметный указатель — топ-N наиболее частотных лемм с позицией первого вхождения. + +Модуль использует `good_words_from_line` из `modules/processor.py` и ничего не знает о загрузке или обходе страниц. + +## Что делает index + +1. Обходит текст построчно и извлекает «хорошие» слова через `processor.py`. +2. Считает частоту каждой леммы. +3. Запоминает, где лемма встретилась впервые. +4. Возвращает топ-N по частоте. +5. Сохраняет результат в CSV и JSON. + +## Генератор prepared_terms + +```python +prepared_terms(text) +``` + +Обходит текст построчно и для каждого слова, прошедшего фильтрацию, выдаёт кортеж: + +```python +(лемма, номер_строки, позиция_символа, исходное_слово) +``` + +Нумерация строк начинается с `1`, позиция символа — 1-based, как в `processor.py`. + +## Построение предметного указателя + +```python +build_subject_index(text, top_n=100) +``` + +Алгоритм: + +1. Проходит по всем словам через `prepared_terms`. +2. Считает частоты через `Counter`. +3. Запоминает первое вхождение каждой леммы через `setdefault` — первый вызов выигрывает, последующие игнорируются. +4. Берёт `top_n` самых частотных лемм. + +Одна запись результата: + +```python +{ + "word": "срок", + "count": 3342, + "line": 34, + "char": 151, + "source_word": "срок", +} +``` + +## Запись результатов + +Для сохранения индекса есть две функции. + +```python +write_subject_index_csv(entries, path) +``` + +Сохраняет CSV с разделителем `;`: + +```text +word;count;line;char;source_word +срок;3342;34;151;срок +``` + +```python +write_subject_index_json(entries, path) +``` + +Сохраняет JSON с отступами и без ASCII-экранирования кириллицы (`ensure_ascii=False`). + +## Где используется index + +В `main.py`: + +```python +subject_index = build_subject_index(original_text, top_n=100) +write_subject_index_csv(subject_index, output_dir / "uk_rf_subject_index.csv") +write_subject_index_json(subject_index, output_dir / "uk_rf_subject_index.json") +``` + +Входные данные — исходный текст статей до лемматизации (`original_text`), а не подготовленный. Лемматизация происходит внутри через `processor.py`. diff --git a/instructions/LOADER_README.md b/instructions/LOADER_README.md index 2781881..9adbc7a 100644 --- a/instructions/LOADER_README.md +++ b/instructions/LOADER_README.md @@ -89,14 +89,9 @@ download_html(url, retries=3, timeout=30.0) Логика выбора загрузчика: -- если домен заканчивается на `consultant.ru`, сначала пробуется `curl`, затем `urllib`; -- для остальных сайтов сначала пробуется `urllib`, затем `curl`. +- всегда сначала пробуется `curl`, затем `urllib`. -Это задается строкой: - -```python -is_consultant = urlparse(url).netloc.endswith("consultant.ru") -``` +`curl` надёжнее обходит защиты ConsultantPlus, поэтому стоит первым для любых URL. ## Повторные попытки diff --git a/instructions/PROCESSOR_README.md b/instructions/PROCESSOR_README.md index 75510ed..906d97c 100644 --- a/instructions/PROCESSOR_README.md +++ b/instructions/PROCESSOR_README.md @@ -211,47 +211,6 @@ prepare_text(text) output/uk_rf_prepared.txt ``` -## Предметный указатель +## Где используется processor -Функция: - -```python -build_subject_index(text, top_n=100) -``` - -строит индекс так: - -1. Проходит по всем подготовленным словам. -2. Считает частоты через `Counter`. -3. Запоминает первое появление каждого слова: номер строки, номер символа и исходную форму слова. -4. Возвращает 100 самых частотных слов. - -Одна запись индекса выглядит так: - -```python -{ - "word": "срок", - "count": 3342, - "line": 34, - "char": 151, - "source_word": "срок", -} -``` - -## Запись результатов - -Для сохранения индекса есть две функции: - -```python -write_subject_index_csv(entries, path) -write_subject_index_json(entries, path) -``` - -Они создают: - -```text -output/uk_rf_subject_index.csv -output/uk_rf_subject_index.json -``` - -CSV удобен для просмотра в таблице, JSON удобен для дальнейшей обработки программой. +`good_words_from_line` экспортируется в `modules/index.py`, где используется для построения предметного указателя. Логика подсчёта частот и записи результатов находится в `index.py`, а не здесь. diff --git a/main.py b/main.py index 0d6677e..5154d18 100644 --- a/main.py +++ b/main.py @@ -12,13 +12,18 @@ def main(): output_dir.mkdir(exist_ok=True) pages = crawl_document(SOURCE_URL) # Вызов краулера на исходный url для рекурсивного обхода - original_text = "\n".join(page["text"] for page in pages) # Складываем результат работы краулера в единую строку + original_text = "\n".join(page["text"] for page in pages) # Складываем результат работы краулера в единую строку prepared_text = prepare_text(original_text) # Вызов препаратора на полученную строку для обработки + + + + subject_index = build_subject_index(original_text, top_n=100) # Строим предметный указатель (output_dir / "uk_rf_original.txt").write_text(original_text, encoding="utf-8") # Вывод исходного текста в файл (output_dir / "uk_rf_prepared.txt").write_text(prepared_text, encoding="utf-8") # Вывод токенизированного и обработанного текста в файл + write_subject_index_csv(subject_index, output_dir / "uk_rf_subject_index.csv") # Вывод предметного указателя в csv write_subject_index_json(subject_index, output_dir / "uk_rf_subject_index.json") # Предметные указатель в json diff --git a/modules/index.py b/modules/index.py index 6c449ed..6dda9c7 100644 --- a/modules/index.py +++ b/modules/index.py @@ -14,7 +14,8 @@ def prepared_terms(text: str): def build_subject_index(text: str, top_n: int = 100) -> list[dict]: - """Строит предметный указатель: топ-N лемм по частоте с позицией первого вхождения. + """ + Строит предметный указатель: топ-N лемм по частоте с позицией первого вхождения. first_place.setdefault гарантирует, что запоминается именно первое вхождение — prepared_terms обходит текст сверху вниз, поэтому первый же setdefault выигрывает. diff --git a/modules/loader.py b/modules/loader.py index 2f73b39..a595c83 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -54,7 +54,8 @@ def download_with_urllib(url: str, timeout: float) -> str: def download_html(url: str, retries: int = 3, timeout: float = 30.0) -> str: - """Загружает HTML-страницу с retry-логикой. + """ + Загружает HTML-страницу с retry-логикой. Порядок загрузчиков: curl → urllib (curl обходит часть защит consultant.ru). На каждой итерации retry пробует оба; пауза 0.5 с между попытками. diff --git a/modules/processor.py b/modules/processor.py index 12a91f7..0ad1027 100644 --- a/modules/processor.py +++ b/modules/processor.py @@ -83,7 +83,8 @@ else: @lru_cache(maxsize=100_000) def parse_word(word: str): - """Возвращает (лемма, тег) для слова. Кешируем, чтобы не прогонять одно и то же через модуль постоянно. + """ + Возвращает (лемма, тег) для слова. Кешируем, чтобы не прогонять одно и то же через модуль постоянно. «ё» → «е» для единообразия леммы в словаре и при сравнении. Если pymorphy3 не установлен, тег пустой, лемма = lower(). @@ -103,7 +104,9 @@ def parse_word(word: str): @lru_cache(maxsize=10_000) def _tag_parts(tag: str) -> frozenset[str]: - """Разбивает строку тега pymorphy3 на множество граммем для быстрого поиска.""" + """ + Разбивает строку тега pymorphy3 на множество граммем для быстрого поиска. + """ return frozenset(re.split(r"[, ]+", tag))