Add loader and processor modules with documentation and output files

- Implemented loader in `modules/loader.py` to download HTML pages using curl and urllib.
- Created processor in `modules/processor.py` for processing the text of the Russian Criminal Code (УК РФ).
- Added README files for both loader and processor explaining their functionality and usage.
- Generated output files including original text, prepared text, and subject index in CSV and JSON formats.
This commit is contained in:
Dmitry
2026-04-16 21:38:00 +03:00
parent 502f48a279
commit 0b41577a42
15 changed files with 8770 additions and 103 deletions
+176
View File
@@ -0,0 +1,176 @@
## Как работает crawler
Crawler находится в `modules/crawler.py`. Его задача простая: пройти по страницам статей УК РФ на ConsultantPlus и вернуть только содержательный текст статей.
В коде используются: `urllib`/`curl` для загрузки, `lxml` и `XPath` для разбора HTML, регулярные выражения для очистки строк.
## Общий алгоритм
1. Загружаем стартовую страницу УК РФ:
```python
START_URL = "https://www.consultant.ru/document/cons_doc_LAW_10699/"
```
2. Ищем ссылки на статьи вида `УК РФ Статья 1...`.
3. Берем первую статью.
4. Загружаем страницу статьи.
5. Извлекаем текст из блока `document-page__content`.
6. Удаляем заголовки, служебные блоки и редакционные пометки.
7. Переходим к следующей странице по ссылке `pages__right`.
8. Повторяем, пока не дойдем до последней статьи или до лимита `max_pages`.
Главная функция:
```python
pages = crawl_document()
```
Она возвращает список словарей:
```python
{
"url": "адрес страницы",
"title": "заголовок статьи",
"text": "очищенный текст статьи",
}
```
## Загрузка страницы
Загрузка вынесена в `modules/loader.py`.
Для обычных сайтов используется `urllib`, а для ConsultantPlus сначала пробуется `curl`, потому что сайт иногда нестабильно отдает большие HTML-страницы.
В crawler используется функция:
```python
download_document_page(url)
```
Она делает несколько попыток с таймаутами `8`, `30`, `30` секунд. Для страниц статей проверяется, что HTML полный и содержит `</html>`.
Для стартовой страницы допускается частичный HTML:
```python
download_document_page(start_url, allow_partial=True)
```
Это нужно потому, что оглавление может успеть прийти даже тогда, когда сайт оборвал конец HTML.
## Определение страницы статьи
Страница считается статьей, если ее заголовок подходит под регулярное выражение:
```python
ARTICLE_RE = re.compile(r"^\s*(?:УК РФ,?\s+)?Статья\s+\d+(?:\.\d+)?\.")
```
То есть подходят такие варианты:
```text
УК РФ Статья 1. ...
УК РФ, Статья 53.1. ...
Статья 361. ...
```
Проверка выполняется функцией:
```python
is_article_page(page_html)
```
## Извлечение текста статьи
Текст достается функцией:
```python
extract_page_text(page_html)
```
Она делает четыре шага:
1. Парсит HTML через `lxml.html.fromstring`.
2. Находит основной блок:
```text
document-page__content
```
3. Удаляет служебные элементы:
- `h1`;
- `doc-style`;
- `doc-insert`;
- `doc-roll`.
4. Берет текст из абзацев `p`.
Дополнительно функция `clean_text()`:
- заменяет неразрывные пробелы;
- схлопывает лишние пробелы;
- убирает номера пунктов вроде `1.`, `2.`, `а)`.
## Удаление служебных строк
Функция:
```python
service_line(line)
```
убирает строки, которые не относятся к содержанию статьи:
```text
(в ред. Федерального закона ...)
(см. текст в предыдущей редакции)
(часть третья введена ...)
Президент
Б.ЕЛЬЦИН
13 июня 1996 года
N 63-ФЗ
```
## Переход к следующей статье
Основной способ перехода:
```python
extract_next_document_url(page_html, current_url)
```
Функция ищет правую ссылку ConsultantPlus:
```text
pages__right
```
Если такой ссылки нет, crawler использует запасной список `known_urls`. В него заранее складываются все найденные ссылки на статьи. Это помогает продолжить обход, если нижняя навигация не попала в загруженный HTML.
## Защита от лишних ссылок
Crawler проверяет, что каждая ссылка относится именно к УК РФ:
```python
is_same_document(url, prefix)
```
Для УК РФ правильный префикс:
```text
/document/cons_doc_LAW_10699/
```
Поэтому crawler не уходит в новости, другие кодексы, комментарии и внешние документы.
## Где используется результат
В `main.py` результат объединяется в один текст:
```python
pages = crawl_document(SOURCE_URL)
original_text = "\n".join(page["text"] for page in pages)
```
После этого `original_text` передается в `modules/processor.py`, где строятся подготовленный текст и предметный указатель.
+127
View File
@@ -0,0 +1,127 @@
## Как работает loader
Loader находится в `modules/loader.py`. Его задача одна: скачать HTML-страницу и вернуть ее как строку.
Есть загрузка через `curl`, загрузка через `urllib` и общая функция `download_html()`, которая выбирает порядок попыток.
## Заголовки запроса
В начале файла задан словарь:
```python
HEADERS
```
Он содержит HTTP-заголовки:
```text
User-Agent
Accept
Accept-Language
```
Они нужны, чтобы сайт отдавал обычную HTML-страницу, как браузеру.
## Загрузка через curl
Функция:
```python
download_with_curl(url, timeout)
```
запускает системную команду `curl` через `subprocess.run`.
Используются параметры:
```text
-L переходить по редиректам
--compressed принимать сжатый ответ
--silent не печатать лишний прогресс
--show-error показать ошибку, если она есть
--max-time ограничить время загрузки
-A передать User-Agent
```
Если `curl` что-то скачал в `stdout`, функция декодирует байты как UTF-8 и возвращает HTML-строку.
Для ConsultantPlus этот способ оказался надежнее, чем чистый `urllib`, потому что сайт иногда нестабильно отдает большие страницы.
## Загрузка через urllib
Функция:
```python
download_with_urllib(url, timeout)
```
использует стандартную библиотеку Python:
```python
urllib.request
```
Алгоритм:
1. Создается SSL-контекст.
2. Создается `Request` с заголовками `HEADERS`.
3. Вызывается `urlopen`.
4. Ответ читается и декодируется как UTF-8.
SSL-проверка отключена:
```python
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
```
Это сделано для учебной устойчивости загрузки, чтобы сертификаты сайта не мешали выполнению задания.
## Главная функция
Основная функция модуля:
```python
download_html(url, retries=3, timeout=30.0)
```
Она возвращает HTML-код страницы.
Логика выбора загрузчика:
- если домен заканчивается на `consultant.ru`, сначала пробуется `curl`, затем `urllib`;
- для остальных сайтов сначала пробуется `urllib`, затем `curl`.
Это задается строкой:
```python
is_consultant = urlparse(url).netloc.endswith("consultant.ru")
```
## Повторные попытки
В `download_html()` есть цикл:
```python
for _ in range(retries):
```
На каждой попытке функция пробует все доступные загрузчики. Если оба способа не сработали, программа ждет `0.5` секунды и пробует еще раз.
Если после всех попыток страница не загрузилась, выбрасывается ошибка:
```python
RuntimeError
```
Эту ошибку затем обрабатывает crawler.
## Где используется loader
Crawler вызывает loader через функцию:
```python
download_html(url, retries=1, timeout=timeout)
```
То есть loader ничего не знает про УК РФ, статьи или обработку текста. Он только скачивает HTML, а вся логика обхода находится в `modules/crawler.py`.
+257
View File
@@ -0,0 +1,257 @@
## Как работает processor
Processor находится в `modules/processor.py`. Он получает уже очищенный исходный текст УК РФ и готовит его к анализу.
В коде используются простые инструменты: регулярные выражения, списки стоп-слов, `pymorphy3` для лемматизации, `Counter` для подсчета частот, `csv` и `json` для записи результата.
## Что делает processor
Основные задачи:
1. Найти слова в тексте.
2. Привести каждое слово к нормальной форме.
3. Удалить лишние слова: стоп-слова, служебные слова структуры закона, числительные, единицы измерения после чисел, имена собственные и названия объектов.
4. Собрать подготовленный текст.
5. Построить предметный указатель на 100 самых частых слов.
## Поиск слов
Слова ищутся регулярным выражением:
```python
WORD_RE = re.compile(r"[А-Яа-яЁё]+(?:-[А-Яа-яЁё]+)?")
```
Оно находит русские слова, в том числе слова с дефисом:
```text
уголовно-правовой
социально-опасный
```
Цифры и пунктуация этим выражением не выбираются.
## Лемматизация
Лемматизация выполняется через `pymorphy3`.
Функция:
```python
parse_word(word)
```
возвращает:
```python
lemma, tag
```
Например:
```text
преступлений -> преступление
лишением -> лишение
осужденного -> осудить
```
`tag` нужен для фильтрации числительных и имен собственных. Чтобы одно и то же слово не разбирать много раз, используется `lru_cache`.
## Стоп-слова
Стоп-слова лежат в множестве:
```python
STOP_WORDS
```
Туда входят обычные служебные слова:
```text
и, в, на, что, этот, который
```
и структурные слова закона:
```text
статья, часть, глава, раздел, пункт, кодекс
```
Они часто встречаются, но не являются полезными терминами предметного указателя.
## Удаление числительных
Числительные удаляются двумя способами.
Первый способ: по списку слов:
```python
NUMERAL_WORDS
```
Например:
```text
один, два, три, пятьсот, тысяча, миллион
```
Второй способ: по морфологическим тегам `pymorphy3`:
```python
NUMR
Anum
```
Так удаляются формы вроде:
```text
трех
пяти
первой
второго
```
## Удаление единиц после чисел
Есть слова, которые сами по себе могут быть полезными, но после числа обычно являются частью числительного выражения.
Например:
```text
до трех лет
500 рублей
на срок шесть месяцев
```
Для этого используется список:
```python
NUMBER_UNITS
```
Туда входят:
```text
год, месяц, день, час, рубль, процент, метр
```
Если такое слово стоит после числительного или после цифры, оно удаляется.
## Удаление имен собственных
Имена собственные удаляются тремя способами.
Первый способ: по морфологическим тегам:
```python
Name, Surn, Patr, Geox, Orgn
```
Так удаляются имена, фамилии, отчества, географические названия и организации, если их распознал `pymorphy3`.
Второй способ: по списку отдельных слов:
```python
PROPER_WORDS
```
Например:
```text
москва, россия, рф, ельцин, интернет
```
Третий способ: по устойчивым словосочетаниям:
```python
PROPER_PHRASES
```
Например:
```text
российская федерация
государственная дума
федеральный закон
уголовный кодекс
центральный банк
```
Сначала слова в строке лемматизируются, затем функция `find_phrase_positions()` ищет такие фразы среди лемм и помечает их позиции как лишние.
## Фильтрация одной строки
Основная функция для одной строки:
```python
good_words_from_line(line)
```
Она делает следующее:
1. Разбивает строку на слова.
2. Для каждого слова находит лемму и морфологические теги.
3. Находит позиции слов, которые входят в имена собственные из нескольких слов.
4. Проверяет каждое слово функцией `is_bad_word()`.
5. Возвращает только подходящие слова.
## Подготовленный текст
Функция:
```python
prepare_text(text)
```
проходит по строкам исходного текста, оставляет только хорошие леммы и склеивает их обратно в строки.
Результат записывается в:
```text
output/uk_rf_prepared.txt
```
## Предметный указатель
Функция:
```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 удобен для дальнейшей обработки программой.