diff --git a/README.md b/README.md deleted file mode 100644 index 16cfcf1..0000000 --- a/README.md +++ /dev/null @@ -1,24 +0,0 @@ -## Обработка УК РФ - -Источник данных: https://www.consultant.ru/document/cons_doc_LAW_10699/ - -Запуск: - -```bash -uv run python main.py -``` - -Скрипт загружает страницы статей УК РФ с Consultant, оставляет только содержательный текст статей, убирает заголовки, номера пунктов, редакционные и заключительные блоки, затем выполняет лемматизацию, удаление пунктуации, стоп-слов, числительных и имен собственных. - -Подробные описания модулей: - -- [LOADER_README.md](LOADER_README.md) — загрузка HTML; -- [CRAWLER_README.md](CRAWLER_README.md) — обход страниц ConsultantPlus и извлечение текста; -- [PROCESSOR_README.md](PROCESSOR_README.md) — подготовка текста и построение предметного указателя. - -Результаты сохраняются в `output/`: - -- `uk_rf_original.txt` — исходный содержательный текст до лемматизации; -- `uk_rf_prepared.txt` — подготовленный текст для анализа без числительных, цифровых единиц и имен собственных; -- `uk_rf_subject_index.csv` — предметный указатель на 100 основных лемм с частотой, строкой и символом первого появления; -- `uk_rf_subject_index.json` — тот же указатель в JSON. diff --git a/instructions/CRAWLER_README.md b/instructions/CRAWLER_README.md deleted file mode 100644 index 7735d44..0000000 --- a/instructions/CRAWLER_README.md +++ /dev/null @@ -1,176 +0,0 @@ -## Как работает 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: - -```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`, где строятся подготовленный текст и предметный указатель. diff --git a/instructions/INDEX_README.md b/instructions/INDEX_README.md deleted file mode 100644 index 1cbeecf..0000000 --- a/instructions/INDEX_README.md +++ /dev/null @@ -1,85 +0,0 @@ -## Как работает 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 deleted file mode 100644 index 9adbc7a..0000000 --- a/instructions/LOADER_README.md +++ /dev/null @@ -1,122 +0,0 @@ -## Как работает 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-код страницы. - -Логика выбора загрузчика: - -- всегда сначала пробуется `curl`, затем `urllib`. - -`curl` надёжнее обходит защиты ConsultantPlus, поэтому стоит первым для любых URL. - -## Повторные попытки - -В `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`. diff --git a/instructions/PROCESSOR_README.md b/instructions/PROCESSOR_README.md deleted file mode 100644 index 906d97c..0000000 --- a/instructions/PROCESSOR_README.md +++ /dev/null @@ -1,216 +0,0 @@ -## Как работает 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 -``` - -## Где используется processor - -`good_words_from_line` экспортируется в `modules/index.py`, где используется для построения предметного указателя. Логика подсчёта частот и записи результатов находится в `index.py`, а не здесь. diff --git a/main.py b/main.py index 5154d18..1b44733 100644 --- a/main.py +++ b/main.py @@ -22,7 +22,7 @@ def main(): 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") # Вывод токенизированного и обработанного текста в файл + (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/crawler.py b/modules/crawler.py index 8d0b164..660e281 100644 --- a/modules/crawler.py +++ b/modules/crawler.py @@ -11,7 +11,7 @@ START_URL = "https://www.consultant.ru/document/cons_doc_LAW_10699/" # Базо FIRST_ARTICLE_URL = START_URL + "e8ecf933c52a85d9223094e0e7fbf52f0128d399/" # Первая статья (для детерминированности обхода) ARTICLE_RE = re.compile(r"^\s*(?:УК РФ,?\s+)?Статья\s+\d+(?:\.\d+)?\.", re.IGNORECASE) -STRUCTURE_PREFIX_RE = re.compile(r"^\s*(?:\d+(?:\.\d+)*|[а-яё])[\.)]\s+", re.IGNORECASE) +STRUCTURE_PREFIX_RE = re.compile(r"^\s*(?:\d+(?:\.\d+)*|[а-яё])[\.)]\s+", re.IGNORECASE) # Строки, начинающиеся с нумерованного или буквенного маркера списка. SPACE_RE = re.compile(r"[ \t\r\f\v]+")