mirror of
https://github.com/ada-dmitry/CourseWork_IRFM.git
synced 2026-09-24 01:10:18 +00:00
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:
@@ -8,3 +8,4 @@ wheels/
|
|||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
cache/
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
## Обработка УК РФ
|
||||||
|
|
||||||
|
Источник данных: 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.
|
||||||
|
|||||||
@@ -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`, где строятся подготовленный текст и предметный указатель.
|
||||||
@@ -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`.
|
||||||
@@ -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 удобен для дальнейшей обработки программой.
|
||||||
@@ -1,8 +1,38 @@
|
|||||||
from modules.crawler import crawl_document
|
from modules.crawler import crawl_document
|
||||||
|
from modules.processor import (
|
||||||
|
build_subject_index,
|
||||||
|
prepare_text,
|
||||||
|
write_subject_index_csv,
|
||||||
|
write_subject_index_json,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
SOURCE_URL = "https://www.consultant.ru/document/cons_doc_LAW_10699/"
|
||||||
|
OUTPUT_DIR = "output"
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
url = "https://www.consultant.ru/document/cons_doc_LAW_10699/"
|
from pathlib import Path
|
||||||
print(crawl_document(url))
|
|
||||||
|
output_dir = Path(OUTPUT_DIR)
|
||||||
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
pages = crawl_document(SOURCE_URL)
|
||||||
|
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")
|
||||||
|
write_subject_index_json(subject_index, output_dir / "uk_rf_subject_index.json")
|
||||||
|
|
||||||
|
print(f"Загружено статей: {len(pages)}")
|
||||||
|
print(f"Строк исходного текста: {len(original_text.splitlines())}")
|
||||||
|
print(f"Лемм после обработки: {len(prepared_text.split())}")
|
||||||
|
print("Топ-10 предметного указателя:")
|
||||||
|
for entry in subject_index[:10]:
|
||||||
|
print(f"{entry['word']}: {entry['count']} (строка {entry['line']}, символ {entry['char']})")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
+197
-46
@@ -1,64 +1,215 @@
|
|||||||
from urllib.parse import urljoin, urldefrag
|
import re
|
||||||
|
import time
|
||||||
|
from urllib.parse import urljoin, urldefrag, urlparse
|
||||||
|
|
||||||
from lxml import html
|
from lxml import html
|
||||||
|
|
||||||
from modules.loader import download_html
|
from modules.loader import download_html
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
SPACE_RE = re.compile(r"[ \t\r\f\v]+")
|
||||||
|
|
||||||
|
|
||||||
def normalize_url(base_url: str, href: str) -> str:
|
def normalize_url(base_url: str, href: str) -> str:
|
||||||
full = urljoin(base_url, href)
|
url = urljoin(base_url, href)
|
||||||
full, _ = urldefrag(full)
|
url, _ = urldefrag(url)
|
||||||
return full
|
return url
|
||||||
|
|
||||||
def extract_links(page_html: str, base_url: str) -> list[str]:
|
|
||||||
"""
|
|
||||||
Извлекает все уникальные ссылки из HTML-страницы.
|
|
||||||
:param page_html: HTML-код страницы
|
|
||||||
:param base_url: Базовый URL для нормализации ссылок
|
|
||||||
:return: Список уникальных нормализованных ссылок
|
|
||||||
"""
|
|
||||||
tree = html.fromstring(page_html)
|
|
||||||
links = []
|
|
||||||
|
|
||||||
for a in tree.xpath("//a[@href]"):
|
|
||||||
href = a.attrib["href"].strip()
|
|
||||||
full_url = normalize_url(base_url, href)
|
|
||||||
links.append(full_url)
|
|
||||||
|
|
||||||
return list(dict.fromkeys(links))
|
|
||||||
|
|
||||||
|
|
||||||
def is_terminal_page(page_html: str, url: str) -> bool:
|
def document_prefix(url: str) -> str:
|
||||||
tree = html.fromstring(page_html)
|
parts = [part for part in urlparse(url).path.split("/") if part]
|
||||||
links = tree.xpath("//a[contains(text(), 'УК РФ Статья')]")
|
return f"/{parts[0]}/{parts[1]}/"
|
||||||
return len(links) == 0
|
|
||||||
|
|
||||||
def extract_page_text(page_html: str, url: str) -> str:
|
|
||||||
tree = html.fromstring(page_html)
|
def is_same_document(url: str, prefix: str) -> bool:
|
||||||
text = tree.text_content()
|
parsed = urlparse(url)
|
||||||
|
return parsed.netloc in {"", "consultant.ru", "www.consultant.ru"} and parsed.path.startswith(prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def download_document_page(url: str, allow_partial: bool = False) -> str:
|
||||||
|
last_html = ""
|
||||||
|
|
||||||
|
for timeout in (8, 30, 30):
|
||||||
|
try:
|
||||||
|
page_html = download_html(url, retries=1, timeout=timeout)
|
||||||
|
except RuntimeError:
|
||||||
|
time.sleep(0.5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if allow_partial or "</html>" in page_html.lower():
|
||||||
|
return page_html
|
||||||
|
|
||||||
|
last_html = page_html
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if last_html:
|
||||||
|
return last_html
|
||||||
|
|
||||||
|
raise RuntimeError(f"Failed to download page: {url}")
|
||||||
|
|
||||||
|
|
||||||
|
def clean_text(text: str) -> str:
|
||||||
|
text = text.replace("\xa0", " ")
|
||||||
|
text = SPACE_RE.sub(" ", text)
|
||||||
|
text = STRUCTURE_PREFIX_RE.sub("", text)
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
def crawl_document(start_url, is_terminal_page=is_terminal_page, extract_document_links=extract_links, extract_page_text=extract_page_text):
|
|
||||||
visited = set()
|
|
||||||
pages = []
|
|
||||||
|
|
||||||
def dfs(url):
|
def main_content(tree):
|
||||||
if url in visited:
|
nodes = tree.xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' document-page__content ')]")
|
||||||
return
|
return nodes[0] if nodes else None
|
||||||
visited.add(url)
|
|
||||||
|
|
||||||
|
def article_title(page_html: str) -> str:
|
||||||
|
tree = html.fromstring(page_html)
|
||||||
|
content = main_content(tree)
|
||||||
|
if content is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
h1 = content.xpath(".//h1")
|
||||||
|
if h1 and clean_text(h1[0].text_content()):
|
||||||
|
return clean_text(h1[0].text_content())
|
||||||
|
|
||||||
|
styles = content.xpath(".//div[contains(concat(' ', normalize-space(@class), ' '), ' doc-style ')]")
|
||||||
|
return clean_text(styles[0].text_content()) if styles else ""
|
||||||
|
|
||||||
|
|
||||||
|
def is_article_page(page_html: str) -> bool:
|
||||||
|
return bool(ARTICLE_RE.match(article_title(page_html)))
|
||||||
|
|
||||||
|
|
||||||
|
def service_line(line: str) -> bool:
|
||||||
|
low = line.lower()
|
||||||
|
|
||||||
|
if low.startswith("(см. текст") or low.startswith("(в ред.") or low.startswith("(введен"):
|
||||||
|
return True
|
||||||
|
if low.startswith("(част") and ("в ред." in low or "введен" in low):
|
||||||
|
return True
|
||||||
|
if "федеральн" in low and any(word in low for word in ("ред.", "введен", "утратил", "утратила")):
|
||||||
|
return True
|
||||||
|
if low in {"президент", "российской федерации", "б.ельцин", "москва, кремль"}:
|
||||||
|
return True
|
||||||
|
if re.fullmatch(r"\d{1,2}\s+[а-яё]+\s+\d{4}\s+года", low):
|
||||||
|
return True
|
||||||
|
if re.fullmatch(r"n\s+\d+\s*-\s*фз", low):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def extract_page_text(page_html: str, url: str = "") -> str:
|
||||||
|
tree = html.fromstring(page_html)
|
||||||
|
content = main_content(tree)
|
||||||
|
if content is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
for node in content.xpath(
|
||||||
|
".//h1"
|
||||||
|
" | .//div[contains(concat(' ', normalize-space(@class), ' '), ' doc-style ')]"
|
||||||
|
" | .//div[contains(concat(' ', normalize-space(@class), ' '), ' doc-insert ')]"
|
||||||
|
" | .//div[contains(concat(' ', normalize-space(@class), ' '), ' doc-roll ')]"
|
||||||
|
):
|
||||||
|
node.getparent().remove(node)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for p in content.xpath(".//p"):
|
||||||
|
line = clean_text(p.text_content())
|
||||||
|
if line and not service_line(line):
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_article_urls(page_html: str, base_url: str) -> list[str]:
|
||||||
|
tree = html.fromstring(page_html)
|
||||||
|
prefix = document_prefix(base_url)
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for link in tree.xpath("//a[@href]"):
|
||||||
|
text = clean_text(link.text_content())
|
||||||
|
url = normalize_url(base_url, link.attrib["href"])
|
||||||
|
if ARTICLE_RE.match(text) and is_same_document(url, prefix) and url not in result:
|
||||||
|
result.append(url)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def extract_next_document_url(page_html: str, base_url: str) -> str | None:
|
||||||
|
tree = html.fromstring(page_html)
|
||||||
|
prefix = document_prefix(base_url)
|
||||||
|
links = tree.xpath("//a[contains(concat(' ', normalize-space(@class), ' '), ' pages__right ')][@href]")
|
||||||
|
if not links:
|
||||||
|
return None
|
||||||
|
|
||||||
|
url = normalize_url(base_url, links[0].attrib["href"])
|
||||||
|
return url if is_same_document(url, prefix) else None
|
||||||
|
|
||||||
|
|
||||||
|
def next_known_article(current_url: str, known_urls: list[str], visited: set[str]) -> str | None:
|
||||||
|
if current_url not in known_urls:
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_index = known_urls.index(current_url)
|
||||||
|
for url in known_urls[current_index + 1:]:
|
||||||
|
if url not in visited:
|
||||||
|
return url
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def add_known_urls(known_urls: list[str], urls: list[str]) -> None:
|
||||||
|
for url in urls:
|
||||||
|
if url not in known_urls:
|
||||||
|
known_urls.append(url)
|
||||||
|
|
||||||
|
|
||||||
|
def crawl_document(start_url: str = START_URL, max_pages: int = 800) -> list[dict]:
|
||||||
|
pages = []
|
||||||
|
visited = set()
|
||||||
|
known_urls = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_html = download_document_page(start_url, allow_partial=True)
|
||||||
|
add_known_urls(known_urls, extract_article_urls(start_html, start_url))
|
||||||
|
current_url = known_urls[0] if known_urls else FIRST_ARTICLE_URL
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"[crawler] start page is unavailable: {exc}")
|
||||||
|
current_url = FIRST_ARTICLE_URL
|
||||||
|
add_known_urls(known_urls, [FIRST_ARTICLE_URL])
|
||||||
|
|
||||||
|
while current_url and current_url not in visited and len(visited) < max_pages:
|
||||||
|
visited.add(current_url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
page_html = download_html(url)
|
page_html = download_document_page(current_url)
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
print(f"[crawler] skip unreachable page: {url} ({exc})")
|
print(f"[crawler] skip page {current_url}: {exc}")
|
||||||
return
|
break
|
||||||
|
|
||||||
if is_terminal_page(page_html, url):
|
add_known_urls(known_urls, extract_article_urls(page_html, current_url))
|
||||||
text = extract_page_text(page_html, url)
|
|
||||||
pages.append({"url": url, "text": text})
|
|
||||||
return
|
|
||||||
|
|
||||||
child_links = extract_document_links(page_html, url)
|
if is_article_page(page_html):
|
||||||
for link in child_links:
|
text = extract_page_text(page_html, current_url)
|
||||||
dfs(link)
|
if text:
|
||||||
|
pages.append(
|
||||||
|
{
|
||||||
|
"url": current_url,
|
||||||
|
"title": article_title(page_html),
|
||||||
|
"text": text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "Статья 361." in article_title(page_html):
|
||||||
|
break
|
||||||
|
|
||||||
|
next_url = extract_next_document_url(page_html, current_url)
|
||||||
|
if next_url is None:
|
||||||
|
next_url = next_known_article(current_url, known_urls, visited)
|
||||||
|
current_url = next_url
|
||||||
|
|
||||||
dfs(start_url)
|
|
||||||
return pages
|
return pages
|
||||||
+52
-53
@@ -1,63 +1,62 @@
|
|||||||
from http.client import IncompleteRead
|
|
||||||
import socket
|
|
||||||
from urllib.error import URLError
|
|
||||||
from urllib.request import Request, urlopen
|
|
||||||
import ssl
|
import ssl
|
||||||
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
def _read_response_bytes(response, chunk_size: int = 64 * 1024, min_partial_bytes: int = 4096) -> bytes:
|
HEADERS = {
|
||||||
"""Читает ответ по частям; при таймауте возвращает уже полученные байты, если их достаточно."""
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36",
|
||||||
chunks = []
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
total = 0
|
"Accept-Language": "ru,en-US;q=0.9,en;q=0.8",
|
||||||
|
}
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
chunk = response.read(chunk_size)
|
|
||||||
except (TimeoutError, socket.timeout, OSError):
|
|
||||||
if total >= min_partial_bytes:
|
|
||||||
return b"".join(chunks)
|
|
||||||
raise
|
|
||||||
|
|
||||||
if not chunk:
|
def download_with_curl(url: str, timeout: float) -> str:
|
||||||
break
|
command = [
|
||||||
|
"curl",
|
||||||
chunks.append(chunk)
|
"-L",
|
||||||
total += len(chunk)
|
"--compressed",
|
||||||
|
"--silent",
|
||||||
return b"".join(chunks)
|
"--show-error",
|
||||||
|
"--max-time",
|
||||||
def download_html(url: str, retries: int = 5, timeout: float = 40.0) -> str:
|
str(int(timeout)),
|
||||||
"""Загружает HTML с повторными попытками при временных сетевых ошибках."""
|
"-A",
|
||||||
ctx = ssl.create_default_context()
|
HEADERS["User-Agent"],
|
||||||
ctx.check_hostname = False
|
|
||||||
ctx.verify_mode = ssl.CERT_NONE
|
|
||||||
|
|
||||||
req = Request(
|
|
||||||
url,
|
url,
|
||||||
headers={
|
]
|
||||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
result = subprocess.run(command, capture_output=True, check=False)
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
||||||
"Accept-Encoding": "identity",
|
if result.stdout:
|
||||||
"Accept-Language": "ru,en-US;q=0.9,en;q=0.8",
|
return result.stdout.decode("utf-8", errors="ignore")
|
||||||
"Connection": "close",
|
|
||||||
},
|
message = result.stderr.decode("utf-8", errors="ignore").strip()
|
||||||
)
|
raise RuntimeError(message or f"curl finished with code {result.returncode}")
|
||||||
|
|
||||||
|
|
||||||
|
def download_with_urllib(url: str, timeout: float) -> str:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
request = Request(url, headers=HEADERS)
|
||||||
|
with urlopen(request, context=context, timeout=timeout) as response:
|
||||||
|
return response.read().decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
def download_html(url: str, retries: int = 3, timeout: float = 30.0) -> str:
|
||||||
|
"""Загружает HTML. Для ConsultantPlus сначала пробует curl, для остальных сайтов urllib."""
|
||||||
|
is_consultant = urlparse(url).netloc.endswith("consultant.ru")
|
||||||
|
loaders = [download_with_curl, download_with_urllib] if is_consultant else [download_with_urllib, download_with_curl]
|
||||||
last_error = None
|
last_error = None
|
||||||
|
|
||||||
for attempt in range(retries):
|
for _ in range(retries):
|
||||||
try:
|
for loader in loaders:
|
||||||
with urlopen(req, context=ctx, timeout=timeout) as response:
|
try:
|
||||||
raw = _read_response_bytes(response)
|
return loader(url, timeout)
|
||||||
return raw.decode("utf-8", errors="ignore")
|
except Exception as exc:
|
||||||
except IncompleteRead as exc:
|
last_error = exc
|
||||||
if exc.partial:
|
|
||||||
return exc.partial.decode("utf-8", errors="ignore")
|
|
||||||
last_error = exc
|
|
||||||
except (TimeoutError, socket.timeout, URLError, OSError) as exc:
|
|
||||||
last_error = exc
|
|
||||||
if attempt == retries - 1:
|
|
||||||
break
|
|
||||||
time.sleep(0.5 * (2 ** attempt))
|
|
||||||
|
|
||||||
raise RuntimeError(f"Failed to download URL after {retries} attempts: {url}") from last_error
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Failed to download URL: {url}") from last_error
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
from collections import Counter
|
||||||
|
from functools import lru_cache
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
WORD_RE = re.compile(r"[А-Яа-яЁё]+(?:-[А-Яа-яЁё]+)?")
|
||||||
|
DIGIT_BEFORE_WORD_RE = re.compile(r"\d[\d\s.,-]*$")
|
||||||
|
|
||||||
|
STOP_WORDS = set(
|
||||||
|
"""
|
||||||
|
а без более бы бывший был была были было быть в во весь вместе вне вновь все всего
|
||||||
|
всей всем всеми всех вследствие вы где да для до его ее если есть же за из или им
|
||||||
|
ими их к как ко когда который ли либо между менее мочь на над надо наиболее
|
||||||
|
настоящий не него нее нет ни них но о об оба однако он она они оно от перед по под
|
||||||
|
после при про с со свой себя так также такой там то того той только том тот у уже
|
||||||
|
чем через что чтобы эта эти это этот являться абзац глава данный кодекс пункт
|
||||||
|
раздел статья часть
|
||||||
|
""".split()
|
||||||
|
)
|
||||||
|
|
||||||
|
NUMERAL_WORDS = set(
|
||||||
|
"""
|
||||||
|
ноль один два три четыре пять шесть семь восемь девять десять одиннадцать
|
||||||
|
двенадцать тринадцать четырнадцать пятнадцать шестнадцать семнадцать
|
||||||
|
восемнадцать девятнадцать двадцать тридцать сорок пятьдесят шестьдесят
|
||||||
|
семьдесят восемьдесят девяносто сто двести триста четыреста пятьсот шестьсот
|
||||||
|
семьсот восемьсот девятьсот тысяча миллион миллиард триллион полтора
|
||||||
|
полтораста оба
|
||||||
|
""".split()
|
||||||
|
)
|
||||||
|
|
||||||
|
NUMBER_UNITS = set(
|
||||||
|
"""
|
||||||
|
год месяц неделя день сутки час минута рубль копейка процент метр километр грамм
|
||||||
|
килограмм литр
|
||||||
|
""".split()
|
||||||
|
)
|
||||||
|
|
||||||
|
PROPER_WORDS = set(
|
||||||
|
"""
|
||||||
|
ельцин интернет конституция кремль москва россия рф снг ссср
|
||||||
|
""".split()
|
||||||
|
)
|
||||||
|
|
||||||
|
PROPER_PHRASES = [
|
||||||
|
phrase.split()
|
||||||
|
for phrase in [
|
||||||
|
"арбитражный процессуальный кодекс",
|
||||||
|
"верховный суд",
|
||||||
|
"вооруженный сила",
|
||||||
|
"государственный дума",
|
||||||
|
"гражданский кодекс",
|
||||||
|
"евразийский экономический союз",
|
||||||
|
"земельный кодекс",
|
||||||
|
"конституционный суд",
|
||||||
|
"конституция российский федерация",
|
||||||
|
"международный уголовный суд",
|
||||||
|
"налоговый кодекс",
|
||||||
|
"организация объединить нация",
|
||||||
|
"правительство российский федерация",
|
||||||
|
"президент российский федерация",
|
||||||
|
"российский федерация",
|
||||||
|
"совет безопасность",
|
||||||
|
"совет федерация",
|
||||||
|
"содружество независимый государство",
|
||||||
|
"таможенный союз",
|
||||||
|
"трудовой кодекс",
|
||||||
|
"уголовно-исполнительный кодекс",
|
||||||
|
"уголовно-процессуальный кодекс",
|
||||||
|
"уголовный кодекс",
|
||||||
|
"федеральный закон",
|
||||||
|
"федеральный собрание",
|
||||||
|
"центральный банк",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pymorphy3
|
||||||
|
except ImportError:
|
||||||
|
MORPH = None
|
||||||
|
else:
|
||||||
|
MORPH = pymorphy3.MorphAnalyzer()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(word: str) -> str:
|
||||||
|
return word.lower().replace("ё", "е")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=100_000)
|
||||||
|
def parse_word(word: str):
|
||||||
|
if MORPH is None:
|
||||||
|
return normalize(word), ""
|
||||||
|
|
||||||
|
parsed = MORPH.parse(normalize(word))
|
||||||
|
if not parsed:
|
||||||
|
return normalize(word), ""
|
||||||
|
|
||||||
|
best = parsed[0]
|
||||||
|
return normalize(best.normal_form), str(best.tag)
|
||||||
|
|
||||||
|
|
||||||
|
def has_tag(tag: str, names: set[str]) -> bool:
|
||||||
|
return bool(set(re.split(r"[, ]+", tag)) & names)
|
||||||
|
|
||||||
|
|
||||||
|
def is_numeral(lemma: str, tag: str) -> bool:
|
||||||
|
return lemma in NUMERAL_WORDS or has_tag(tag, {"NUMR", "Anum"})
|
||||||
|
|
||||||
|
|
||||||
|
def is_proper_name(lemma: str, tag: str) -> bool:
|
||||||
|
return lemma in PROPER_WORDS or has_tag(tag, {"Name", "Surn", "Patr", "Geox", "Orgn"})
|
||||||
|
|
||||||
|
|
||||||
|
def find_phrase_positions(lemmas: list[str]) -> set[int]:
|
||||||
|
positions = set()
|
||||||
|
|
||||||
|
for phrase in PROPER_PHRASES:
|
||||||
|
phrase_len = len(phrase)
|
||||||
|
for start in range(len(lemmas) - phrase_len + 1):
|
||||||
|
if lemmas[start : start + phrase_len] == phrase:
|
||||||
|
positions.update(range(start, start + phrase_len))
|
||||||
|
|
||||||
|
return positions
|
||||||
|
|
||||||
|
|
||||||
|
def words_from_line(line: str):
|
||||||
|
words = []
|
||||||
|
|
||||||
|
for match in WORD_RE.finditer(line):
|
||||||
|
word = match.group()
|
||||||
|
lemma, tag = parse_word(word)
|
||||||
|
before_word = line[: match.start()].rstrip()
|
||||||
|
words.append(
|
||||||
|
{
|
||||||
|
"source": word,
|
||||||
|
"lemma": lemma,
|
||||||
|
"tag": tag,
|
||||||
|
"char": match.start() + 1,
|
||||||
|
"after_digit": bool(DIGIT_BEFORE_WORD_RE.search(before_word)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return words
|
||||||
|
|
||||||
|
|
||||||
|
def is_bad_word(word: dict, phrase_positions: set[int], index: int, after_number: bool) -> bool:
|
||||||
|
lemma = word["lemma"]
|
||||||
|
tag = word["tag"]
|
||||||
|
|
||||||
|
if len(lemma) <= 2 or lemma in STOP_WORDS:
|
||||||
|
return True
|
||||||
|
if index in phrase_positions or is_proper_name(lemma, tag):
|
||||||
|
return True
|
||||||
|
if is_numeral(lemma, tag):
|
||||||
|
return True
|
||||||
|
if lemma in NUMBER_UNITS and (after_number or word["after_digit"]):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def good_words_from_line(line: str):
|
||||||
|
words = words_from_line(line)
|
||||||
|
phrase_positions = find_phrase_positions([word["lemma"] for word in words])
|
||||||
|
after_number = False
|
||||||
|
|
||||||
|
for index, word in enumerate(words):
|
||||||
|
bad = is_bad_word(word, phrase_positions, index, after_number)
|
||||||
|
after_number = is_numeral(word["lemma"], word["tag"]) or (
|
||||||
|
word["lemma"] in NUMBER_UNITS and (after_number or word["after_digit"])
|
||||||
|
)
|
||||||
|
|
||||||
|
if not bad:
|
||||||
|
yield word
|
||||||
|
|
||||||
|
|
||||||
|
def prepared_terms(text: str):
|
||||||
|
for line_number, line in enumerate(text.splitlines(), start=1):
|
||||||
|
for word in good_words_from_line(line):
|
||||||
|
yield word["lemma"], line_number, word["char"], word["source"]
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_text(text: str) -> str:
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
for line in text.splitlines():
|
||||||
|
lemmas = [word["lemma"] for word in good_words_from_line(line)]
|
||||||
|
if lemmas:
|
||||||
|
lines.append(" ".join(lemmas))
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def build_subject_index(text: str, top_n: int = 100) -> list[dict]:
|
||||||
|
counts = Counter()
|
||||||
|
first_place = {}
|
||||||
|
|
||||||
|
for lemma, line, char, source in prepared_terms(text):
|
||||||
|
counts[lemma] += 1
|
||||||
|
first_place.setdefault(lemma, (line, char, source))
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for lemma, count in counts.most_common(top_n):
|
||||||
|
line, char, source = first_place[lemma]
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"word": lemma,
|
||||||
|
"count": count,
|
||||||
|
"line": line,
|
||||||
|
"char": char,
|
||||||
|
"source_word": source,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def write_subject_index_csv(entries: list[dict], path: Path) -> None:
|
||||||
|
with path.open("w", encoding="utf-8", newline="") as file:
|
||||||
|
writer = csv.writer(file, delimiter=";")
|
||||||
|
writer.writerow(["word", "count", "line", "char", "source_word"])
|
||||||
|
for entry in entries:
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
entry["word"],
|
||||||
|
entry["count"],
|
||||||
|
entry["line"],
|
||||||
|
entry["char"],
|
||||||
|
entry["source_word"],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_subject_index_json(entries: list[dict], path: Path) -> None:
|
||||||
|
with path.open("w", encoding="utf-8") as file:
|
||||||
|
json.dump(entries, file, ensure_ascii=False, indent=2)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
|||||||
|
word;count;line;char;source_word
|
||||||
|
срок;3342;34;151;срок
|
||||||
|
размер;2024;136;25;размере
|
||||||
|
лишение;1843;31;183;лишения
|
||||||
|
свобода;1563;3;53;свобод
|
||||||
|
определенный;1399;120;24;определенные
|
||||||
|
иной;1267;4;241;иные
|
||||||
|
лицо;1224;7;1;Лица
|
||||||
|
наказываться;1084;493;1;наказывается
|
||||||
|
работа;1080;122;14;работы
|
||||||
|
осудить;973;25;746;осуждены
|
||||||
|
таковой;942;409;174;таковых
|
||||||
|
штраф;899;119;1;штраф
|
||||||
|
период;873;136;135;период
|
||||||
|
доход;864;136;113;дохода
|
||||||
|
деятельность;824;53;752;деятельности
|
||||||
|
заработный;808;136;86;заработной
|
||||||
|
плата;808;136;97;платы
|
||||||
|
право;797;2;118;права
|
||||||
|
должность;704;120;37;должности
|
||||||
|
преступление;696;3;309;преступлений
|
||||||
|
занимать;694;120;15;занимать
|
||||||
|
заниматься;682;120;51;заниматься
|
||||||
|
принудительный;659;56;135;принудительные
|
||||||
|
деяние;657;4;174;деяния
|
||||||
|
совершенный;578;20;15;совершенные
|
||||||
|
предусмотренный;474;14;113;предусмотренного
|
||||||
|
ограничение;447;116;223;ограничении
|
||||||
|
группа;410;85;144;группу
|
||||||
|
наказание;406;4;229;наказаний
|
||||||
|
средство;353;53;541;средством
|
||||||
|
крупный;338;848;122;крупных
|
||||||
|
совершение;329;4;283;совершение
|
||||||
|
обязательный;261;114;116;обязательных
|
||||||
|
организация;250;53;870;организацию
|
||||||
|
повлечь;248;546;68;повлекшие
|
||||||
|
тяжкий;237;30;185;тяжкие
|
||||||
|
использование;234;84;221;использования
|
||||||
|
исправительный;229;123;1;исправительные
|
||||||
|
совершить;228;7;7;совершившие
|
||||||
|
отношение;213;7;213;отношения
|
||||||
|
действие;206;8;74;действия
|
||||||
|
цель;204;13;121;целью
|
||||||
|
государственный;188;53;2031;государственного
|
||||||
|
равно;187;85;107;равно
|
||||||
|
сумма;185;136;229;сумме
|
||||||
|
применение;184;6;1;Применение
|
||||||
|
предусмотреть;184;18;133;предусмотренных
|
||||||
|
организовать;183;85;32;организовавшее
|
||||||
|
уголовный;180;1;1;Уголовное
|
||||||
|
особо;178;30;207;особо
|
||||||
|
суд;175;23;349;суда
|
||||||
|
предварительный;165;93;124;предварительного
|
||||||
|
военный;163;21;401;военном
|
||||||
|
незаконный;162;53;1199;незаконном
|
||||||
|
случай;161;22;141;случае
|
||||||
|
сговор;161;74;174;сговор
|
||||||
|
примечание;159;296;1;Примечание
|
||||||
|
ответственность;156;1;122;ответственность
|
||||||
|
арест;156;127;1;арест
|
||||||
|
причинение;151;9;71;причинение
|
||||||
|
неосторожность;143;60;78;неосторожности
|
||||||
|
ущерб;142;204;135;ущерба
|
||||||
|
признаваться;141;4;181;признаются
|
||||||
|
заведомо;141;53;1130;заведомо
|
||||||
|
другой;138;7;297;других
|
||||||
|
несовершеннолетний;138;54;6;несовершеннолетний
|
||||||
|
вред;137;9;82;вреда
|
||||||
|
положение;134;7;184;положения
|
||||||
|
орган;133;80;135;органам
|
||||||
|
имущество;131;53;621;имущества
|
||||||
|
вещество;131;53;1607;веществ
|
||||||
|
вид;127;4;224;виды
|
||||||
|
служба;126;124;24;службе
|
||||||
|
информация;125;87;107;информации
|
||||||
|
здоровье;124;53;179;здоровью
|
||||||
|
указанный;118;35;337;указанного
|
||||||
|
нарушение;118;201;29;нарушении
|
||||||
|
сеть;117;230;147;сетях
|
||||||
|
человек;116;3;60;человека
|
||||||
|
последствие;112;5;72;последствия
|
||||||
|
служебный;110;104;139;служебного
|
||||||
|
административный;107;298;288;административной
|
||||||
|
гражданин;106;3;71;гражданина
|
||||||
|
подкуп;106;86;97;подкупа
|
||||||
|
превышать;105;31;164;превышает
|
||||||
|
денежный;105;135;12;денежное
|
||||||
|
смерть;104;492;41;смерти
|
||||||
|
законодательство;101;1;11;законодательство
|
||||||
|
число;101;17;274;числе
|
||||||
|
насилие;101;101;295;насилием
|
||||||
|
иностранный;98;22;68;иностранных
|
||||||
|
характер;94;4;270;характера
|
||||||
|
соответствие;92;22;234;соответствии
|
||||||
|
отбывание;92;27;246;отбывания
|
||||||
|
угроза;92;28;109;угрозой
|
||||||
|
возраст;89;48;48;возрасте
|
||||||
|
медицинский;87;56;155;медицинского
|
||||||
|
должностной;86;7;171;должностного
|
||||||
|
условие;86;35;296;условии
|
||||||
|
оружие;86;53;1764;оружия
|
||||||
|
@@ -0,0 +1,702 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"word": "срок",
|
||||||
|
"count": 3342,
|
||||||
|
"line": 34,
|
||||||
|
"char": 151,
|
||||||
|
"source_word": "срок"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "размер",
|
||||||
|
"count": 2024,
|
||||||
|
"line": 136,
|
||||||
|
"char": 25,
|
||||||
|
"source_word": "размере"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "лишение",
|
||||||
|
"count": 1843,
|
||||||
|
"line": 31,
|
||||||
|
"char": 183,
|
||||||
|
"source_word": "лишения"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "свобода",
|
||||||
|
"count": 1563,
|
||||||
|
"line": 3,
|
||||||
|
"char": 53,
|
||||||
|
"source_word": "свобод"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "определенный",
|
||||||
|
"count": 1399,
|
||||||
|
"line": 120,
|
||||||
|
"char": 24,
|
||||||
|
"source_word": "определенные"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "иной",
|
||||||
|
"count": 1267,
|
||||||
|
"line": 4,
|
||||||
|
"char": 241,
|
||||||
|
"source_word": "иные"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "лицо",
|
||||||
|
"count": 1224,
|
||||||
|
"line": 7,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "Лица"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "наказываться",
|
||||||
|
"count": 1084,
|
||||||
|
"line": 493,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "наказывается"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "работа",
|
||||||
|
"count": 1080,
|
||||||
|
"line": 122,
|
||||||
|
"char": 14,
|
||||||
|
"source_word": "работы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "осудить",
|
||||||
|
"count": 973,
|
||||||
|
"line": 25,
|
||||||
|
"char": 746,
|
||||||
|
"source_word": "осуждены"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "таковой",
|
||||||
|
"count": 942,
|
||||||
|
"line": 409,
|
||||||
|
"char": 174,
|
||||||
|
"source_word": "таковых"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "штраф",
|
||||||
|
"count": 899,
|
||||||
|
"line": 119,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "штраф"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "период",
|
||||||
|
"count": 873,
|
||||||
|
"line": 136,
|
||||||
|
"char": 135,
|
||||||
|
"source_word": "период"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "доход",
|
||||||
|
"count": 864,
|
||||||
|
"line": 136,
|
||||||
|
"char": 113,
|
||||||
|
"source_word": "дохода"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "деятельность",
|
||||||
|
"count": 824,
|
||||||
|
"line": 53,
|
||||||
|
"char": 752,
|
||||||
|
"source_word": "деятельности"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "заработный",
|
||||||
|
"count": 808,
|
||||||
|
"line": 136,
|
||||||
|
"char": 86,
|
||||||
|
"source_word": "заработной"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "плата",
|
||||||
|
"count": 808,
|
||||||
|
"line": 136,
|
||||||
|
"char": 97,
|
||||||
|
"source_word": "платы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "право",
|
||||||
|
"count": 797,
|
||||||
|
"line": 2,
|
||||||
|
"char": 118,
|
||||||
|
"source_word": "права"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "должность",
|
||||||
|
"count": 704,
|
||||||
|
"line": 120,
|
||||||
|
"char": 37,
|
||||||
|
"source_word": "должности"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "преступление",
|
||||||
|
"count": 696,
|
||||||
|
"line": 3,
|
||||||
|
"char": 309,
|
||||||
|
"source_word": "преступлений"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "занимать",
|
||||||
|
"count": 694,
|
||||||
|
"line": 120,
|
||||||
|
"char": 15,
|
||||||
|
"source_word": "занимать"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "заниматься",
|
||||||
|
"count": 682,
|
||||||
|
"line": 120,
|
||||||
|
"char": 51,
|
||||||
|
"source_word": "заниматься"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "принудительный",
|
||||||
|
"count": 659,
|
||||||
|
"line": 56,
|
||||||
|
"char": 135,
|
||||||
|
"source_word": "принудительные"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "деяние",
|
||||||
|
"count": 657,
|
||||||
|
"line": 4,
|
||||||
|
"char": 174,
|
||||||
|
"source_word": "деяния"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "совершенный",
|
||||||
|
"count": 578,
|
||||||
|
"line": 20,
|
||||||
|
"char": 15,
|
||||||
|
"source_word": "совершенные"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "предусмотренный",
|
||||||
|
"count": 474,
|
||||||
|
"line": 14,
|
||||||
|
"char": 113,
|
||||||
|
"source_word": "предусмотренного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "ограничение",
|
||||||
|
"count": 447,
|
||||||
|
"line": 116,
|
||||||
|
"char": 223,
|
||||||
|
"source_word": "ограничении"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "группа",
|
||||||
|
"count": 410,
|
||||||
|
"line": 85,
|
||||||
|
"char": 144,
|
||||||
|
"source_word": "группу"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "наказание",
|
||||||
|
"count": 406,
|
||||||
|
"line": 4,
|
||||||
|
"char": 229,
|
||||||
|
"source_word": "наказаний"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "средство",
|
||||||
|
"count": 353,
|
||||||
|
"line": 53,
|
||||||
|
"char": 541,
|
||||||
|
"source_word": "средством"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "крупный",
|
||||||
|
"count": 338,
|
||||||
|
"line": 848,
|
||||||
|
"char": 122,
|
||||||
|
"source_word": "крупных"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "совершение",
|
||||||
|
"count": 329,
|
||||||
|
"line": 4,
|
||||||
|
"char": 283,
|
||||||
|
"source_word": "совершение"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "обязательный",
|
||||||
|
"count": 261,
|
||||||
|
"line": 114,
|
||||||
|
"char": 116,
|
||||||
|
"source_word": "обязательных"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "организация",
|
||||||
|
"count": 250,
|
||||||
|
"line": 53,
|
||||||
|
"char": 870,
|
||||||
|
"source_word": "организацию"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "повлечь",
|
||||||
|
"count": 248,
|
||||||
|
"line": 546,
|
||||||
|
"char": 68,
|
||||||
|
"source_word": "повлекшие"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "тяжкий",
|
||||||
|
"count": 237,
|
||||||
|
"line": 30,
|
||||||
|
"char": 185,
|
||||||
|
"source_word": "тяжкие"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "использование",
|
||||||
|
"count": 234,
|
||||||
|
"line": 84,
|
||||||
|
"char": 221,
|
||||||
|
"source_word": "использования"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "исправительный",
|
||||||
|
"count": 229,
|
||||||
|
"line": 123,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "исправительные"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "совершить",
|
||||||
|
"count": 228,
|
||||||
|
"line": 7,
|
||||||
|
"char": 7,
|
||||||
|
"source_word": "совершившие"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "отношение",
|
||||||
|
"count": 213,
|
||||||
|
"line": 7,
|
||||||
|
"char": 213,
|
||||||
|
"source_word": "отношения"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "действие",
|
||||||
|
"count": 206,
|
||||||
|
"line": 8,
|
||||||
|
"char": 74,
|
||||||
|
"source_word": "действия"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "цель",
|
||||||
|
"count": 204,
|
||||||
|
"line": 13,
|
||||||
|
"char": 121,
|
||||||
|
"source_word": "целью"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "государственный",
|
||||||
|
"count": 188,
|
||||||
|
"line": 53,
|
||||||
|
"char": 2031,
|
||||||
|
"source_word": "государственного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "равно",
|
||||||
|
"count": 187,
|
||||||
|
"line": 85,
|
||||||
|
"char": 107,
|
||||||
|
"source_word": "равно"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "сумма",
|
||||||
|
"count": 185,
|
||||||
|
"line": 136,
|
||||||
|
"char": 229,
|
||||||
|
"source_word": "сумме"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "применение",
|
||||||
|
"count": 184,
|
||||||
|
"line": 6,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "Применение"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "предусмотреть",
|
||||||
|
"count": 184,
|
||||||
|
"line": 18,
|
||||||
|
"char": 133,
|
||||||
|
"source_word": "предусмотренных"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "организовать",
|
||||||
|
"count": 183,
|
||||||
|
"line": 85,
|
||||||
|
"char": 32,
|
||||||
|
"source_word": "организовавшее"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "уголовный",
|
||||||
|
"count": 180,
|
||||||
|
"line": 1,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "Уголовное"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "особо",
|
||||||
|
"count": 178,
|
||||||
|
"line": 30,
|
||||||
|
"char": 207,
|
||||||
|
"source_word": "особо"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "суд",
|
||||||
|
"count": 175,
|
||||||
|
"line": 23,
|
||||||
|
"char": 349,
|
||||||
|
"source_word": "суда"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "предварительный",
|
||||||
|
"count": 165,
|
||||||
|
"line": 93,
|
||||||
|
"char": 124,
|
||||||
|
"source_word": "предварительного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "военный",
|
||||||
|
"count": 163,
|
||||||
|
"line": 21,
|
||||||
|
"char": 401,
|
||||||
|
"source_word": "военном"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "незаконный",
|
||||||
|
"count": 162,
|
||||||
|
"line": 53,
|
||||||
|
"char": 1199,
|
||||||
|
"source_word": "незаконном"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "случай",
|
||||||
|
"count": 161,
|
||||||
|
"line": 22,
|
||||||
|
"char": 141,
|
||||||
|
"source_word": "случае"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "сговор",
|
||||||
|
"count": 161,
|
||||||
|
"line": 74,
|
||||||
|
"char": 174,
|
||||||
|
"source_word": "сговор"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "примечание",
|
||||||
|
"count": 159,
|
||||||
|
"line": 296,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "Примечание"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "ответственность",
|
||||||
|
"count": 156,
|
||||||
|
"line": 1,
|
||||||
|
"char": 122,
|
||||||
|
"source_word": "ответственность"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "арест",
|
||||||
|
"count": 156,
|
||||||
|
"line": 127,
|
||||||
|
"char": 1,
|
||||||
|
"source_word": "арест"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "причинение",
|
||||||
|
"count": 151,
|
||||||
|
"line": 9,
|
||||||
|
"char": 71,
|
||||||
|
"source_word": "причинение"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "неосторожность",
|
||||||
|
"count": 143,
|
||||||
|
"line": 60,
|
||||||
|
"char": 78,
|
||||||
|
"source_word": "неосторожности"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "ущерб",
|
||||||
|
"count": 142,
|
||||||
|
"line": 204,
|
||||||
|
"char": 135,
|
||||||
|
"source_word": "ущерба"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "признаваться",
|
||||||
|
"count": 141,
|
||||||
|
"line": 4,
|
||||||
|
"char": 181,
|
||||||
|
"source_word": "признаются"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "заведомо",
|
||||||
|
"count": 141,
|
||||||
|
"line": 53,
|
||||||
|
"char": 1130,
|
||||||
|
"source_word": "заведомо"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "другой",
|
||||||
|
"count": 138,
|
||||||
|
"line": 7,
|
||||||
|
"char": 297,
|
||||||
|
"source_word": "других"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "несовершеннолетний",
|
||||||
|
"count": 138,
|
||||||
|
"line": 54,
|
||||||
|
"char": 6,
|
||||||
|
"source_word": "несовершеннолетний"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "вред",
|
||||||
|
"count": 137,
|
||||||
|
"line": 9,
|
||||||
|
"char": 82,
|
||||||
|
"source_word": "вреда"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "положение",
|
||||||
|
"count": 134,
|
||||||
|
"line": 7,
|
||||||
|
"char": 184,
|
||||||
|
"source_word": "положения"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "орган",
|
||||||
|
"count": 133,
|
||||||
|
"line": 80,
|
||||||
|
"char": 135,
|
||||||
|
"source_word": "органам"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "имущество",
|
||||||
|
"count": 131,
|
||||||
|
"line": 53,
|
||||||
|
"char": 621,
|
||||||
|
"source_word": "имущества"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "вещество",
|
||||||
|
"count": 131,
|
||||||
|
"line": 53,
|
||||||
|
"char": 1607,
|
||||||
|
"source_word": "веществ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "вид",
|
||||||
|
"count": 127,
|
||||||
|
"line": 4,
|
||||||
|
"char": 224,
|
||||||
|
"source_word": "виды"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "служба",
|
||||||
|
"count": 126,
|
||||||
|
"line": 124,
|
||||||
|
"char": 24,
|
||||||
|
"source_word": "службе"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "информация",
|
||||||
|
"count": 125,
|
||||||
|
"line": 87,
|
||||||
|
"char": 107,
|
||||||
|
"source_word": "информации"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "здоровье",
|
||||||
|
"count": 124,
|
||||||
|
"line": 53,
|
||||||
|
"char": 179,
|
||||||
|
"source_word": "здоровью"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "указанный",
|
||||||
|
"count": 118,
|
||||||
|
"line": 35,
|
||||||
|
"char": 337,
|
||||||
|
"source_word": "указанного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "нарушение",
|
||||||
|
"count": 118,
|
||||||
|
"line": 201,
|
||||||
|
"char": 29,
|
||||||
|
"source_word": "нарушении"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "сеть",
|
||||||
|
"count": 117,
|
||||||
|
"line": 230,
|
||||||
|
"char": 147,
|
||||||
|
"source_word": "сетях"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "человек",
|
||||||
|
"count": 116,
|
||||||
|
"line": 3,
|
||||||
|
"char": 60,
|
||||||
|
"source_word": "человека"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "последствие",
|
||||||
|
"count": 112,
|
||||||
|
"line": 5,
|
||||||
|
"char": 72,
|
||||||
|
"source_word": "последствия"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "служебный",
|
||||||
|
"count": 110,
|
||||||
|
"line": 104,
|
||||||
|
"char": 139,
|
||||||
|
"source_word": "служебного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "административный",
|
||||||
|
"count": 107,
|
||||||
|
"line": 298,
|
||||||
|
"char": 288,
|
||||||
|
"source_word": "административной"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "гражданин",
|
||||||
|
"count": 106,
|
||||||
|
"line": 3,
|
||||||
|
"char": 71,
|
||||||
|
"source_word": "гражданина"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "подкуп",
|
||||||
|
"count": 106,
|
||||||
|
"line": 86,
|
||||||
|
"char": 97,
|
||||||
|
"source_word": "подкупа"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "превышать",
|
||||||
|
"count": 105,
|
||||||
|
"line": 31,
|
||||||
|
"char": 164,
|
||||||
|
"source_word": "превышает"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "денежный",
|
||||||
|
"count": 105,
|
||||||
|
"line": 135,
|
||||||
|
"char": 12,
|
||||||
|
"source_word": "денежное"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "смерть",
|
||||||
|
"count": 104,
|
||||||
|
"line": 492,
|
||||||
|
"char": 41,
|
||||||
|
"source_word": "смерти"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "законодательство",
|
||||||
|
"count": 101,
|
||||||
|
"line": 1,
|
||||||
|
"char": 11,
|
||||||
|
"source_word": "законодательство"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "число",
|
||||||
|
"count": 101,
|
||||||
|
"line": 17,
|
||||||
|
"char": 274,
|
||||||
|
"source_word": "числе"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "насилие",
|
||||||
|
"count": 101,
|
||||||
|
"line": 101,
|
||||||
|
"char": 295,
|
||||||
|
"source_word": "насилием"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "иностранный",
|
||||||
|
"count": 98,
|
||||||
|
"line": 22,
|
||||||
|
"char": 68,
|
||||||
|
"source_word": "иностранных"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "характер",
|
||||||
|
"count": 94,
|
||||||
|
"line": 4,
|
||||||
|
"char": 270,
|
||||||
|
"source_word": "характера"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "соответствие",
|
||||||
|
"count": 92,
|
||||||
|
"line": 22,
|
||||||
|
"char": 234,
|
||||||
|
"source_word": "соответствии"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "отбывание",
|
||||||
|
"count": 92,
|
||||||
|
"line": 27,
|
||||||
|
"char": 246,
|
||||||
|
"source_word": "отбывания"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "угроза",
|
||||||
|
"count": 92,
|
||||||
|
"line": 28,
|
||||||
|
"char": 109,
|
||||||
|
"source_word": "угрозой"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "возраст",
|
||||||
|
"count": 89,
|
||||||
|
"line": 48,
|
||||||
|
"char": 48,
|
||||||
|
"source_word": "возрасте"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "медицинский",
|
||||||
|
"count": 87,
|
||||||
|
"line": 56,
|
||||||
|
"char": 155,
|
||||||
|
"source_word": "медицинского"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "должностной",
|
||||||
|
"count": 86,
|
||||||
|
"line": 7,
|
||||||
|
"char": 171,
|
||||||
|
"source_word": "должностного"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "условие",
|
||||||
|
"count": 86,
|
||||||
|
"line": 35,
|
||||||
|
"char": 296,
|
||||||
|
"source_word": "условии"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"word": "оружие",
|
||||||
|
"count": 86,
|
||||||
|
"line": 53,
|
||||||
|
"char": 1764,
|
||||||
|
"source_word": "оружия"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -6,4 +6,5 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lxml>=6.0.4",
|
"lxml>=6.0.4",
|
||||||
|
"pymorphy3>=2.0.6",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,10 +8,23 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "lxml" },
|
{ name = "lxml" },
|
||||||
|
{ name = "pymorphy3" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "lxml", specifier = ">=6.0.4" }]
|
requires-dist = [
|
||||||
|
{ name = "lxml", specifier = ">=6.0.4" },
|
||||||
|
{ name = "pymorphy3", specifier = ">=2.0.6" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dawg2-python"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2d/03/85171ce1e59088237aebf21943d1136463f6422820f096ac8cf9322aa851/dawg2_python-0.9.0.tar.gz", hash = "sha256:adea0312acd1a958659e8448ce6899046c0858d0b6c8949a51eebdeb5a113e4a", size = 10278, upload-time = "2025-02-17T13:22:24.261Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/3b/7fb4c1a8df59cb80f5f7ecb9646280e000f9ba2ccff8710205dc9aa4604f/dawg2_python-0.9.0-py3-none-any.whl", hash = "sha256:4fab6fc097bd176cd783cd8421b757348ea5a460789e53b0f6bb64831380bab5", size = 9331, upload-time = "2025-02-17T13:22:22.858Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lxml"
|
name = "lxml"
|
||||||
@@ -74,3 +87,35 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/3d/5e/2231f34cc54b8422b793593138d86d3fa4588fb2297d4ea0472390f25627/lxml-6.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:25bad2d8438f4ef5a7ad4a8d8bcaadde20c0daced8bdb56d46236b0a7d1cbdd0", size = 4391037, upload-time = "2026-04-12T16:26:54.398Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/5e/2231f34cc54b8422b793593138d86d3fa4588fb2297d4ea0472390f25627/lxml-6.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:25bad2d8438f4ef5a7ad4a8d8bcaadde20c0daced8bdb56d46236b0a7d1cbdd0", size = 4391037, upload-time = "2026-04-12T16:26:54.398Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/53/8ba3cd5984f8363635450c93f63e541a0721b362bb32ae0d8237d9674aee/lxml-6.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:1dcd9e6cb9b7df808ea33daebd1801f37a8f50e8c075013ed2a2343246727838", size = 3816184, upload-time = "2026-04-12T16:26:57.011Z" },
|
{ url = "https://files.pythonhosted.org/packages/39/53/8ba3cd5984f8363635450c93f63e541a0721b362bb32ae0d8237d9674aee/lxml-6.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:1dcd9e6cb9b7df808ea33daebd1801f37a8f50e8c075013ed2a2343246727838", size = 3816184, upload-time = "2026-04-12T16:26:57.011Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pymorphy3"
|
||||||
|
version = "2.0.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "dawg2-python" },
|
||||||
|
{ name = "pymorphy3-dicts-ru" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/17/63/3a1eabd3a7e6e060b69a87fe9c28fe89f75f4d49e55f0caf2e29c943c003/pymorphy3-2.0.6.tar.gz", hash = "sha256:1603df3bc9e116967c990607f5b97d42fb1c572d6839b851af3501e51d7f5493", size = 97681, upload-time = "2025-10-09T16:06:18.718Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/4b/59bac03278033e293d1405ed42fb6c6252c25f40c50f509c615caeaa3b71/pymorphy3-2.0.6-py3-none-any.whl", hash = "sha256:0254317c02ce3ea17e080b7fc9d675e44662b3a5296bae68605b7a41d25b36c3", size = 53900, upload-time = "2025-10-09T16:06:17.721Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pymorphy3-dicts-ru"
|
||||||
|
version = "2.4.417150.4580142"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/13/02ffe6893a777add5c8a43f212f31a3f6a03e7d44a484cf7b5ac5381fddb/pymorphy3-dicts-ru-2.4.417150.4580142.tar.gz", hash = "sha256:39ab379d4ca905bafed50f5afc3a3de6f9643605776fbcabc4d3088d4ed382b0", size = 8381569, upload-time = "2022-01-08T22:17:37.581Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/67/469e9e52d046863f5959928794d3067d455a77f580bf4a662630a43eb426/pymorphy3_dicts_ru-2.4.417150.4580142-py2.py3-none-any.whl", hash = "sha256:718bac64c73c10c16073a199402657283d9b64c04188b694f6d3e9b0d85440f4", size = 8442043, upload-time = "2022-01-08T22:17:34.282Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "82.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user