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
|
||||
.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.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():
|
||||
url = "https://www.consultant.ru/document/cons_doc_LAW_10699/"
|
||||
print(crawl_document(url))
|
||||
from pathlib import Path
|
||||
|
||||
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__":
|
||||
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 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:
|
||||
full = urljoin(base_url, href)
|
||||
full, _ = urldefrag(full)
|
||||
return full
|
||||
|
||||
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))
|
||||
url = urljoin(base_url, href)
|
||||
url, _ = urldefrag(url)
|
||||
return url
|
||||
|
||||
|
||||
def is_terminal_page(page_html: str, url: str) -> bool:
|
||||
tree = html.fromstring(page_html)
|
||||
links = tree.xpath("//a[contains(text(), 'УК РФ Статья')]")
|
||||
return len(links) == 0
|
||||
def document_prefix(url: str) -> str:
|
||||
parts = [part for part in urlparse(url).path.split("/") if part]
|
||||
return f"/{parts[0]}/{parts[1]}/"
|
||||
|
||||
def extract_page_text(page_html: str, url: str) -> str:
|
||||
tree = html.fromstring(page_html)
|
||||
text = tree.text_content()
|
||||
|
||||
def is_same_document(url: str, prefix: str) -> bool:
|
||||
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()
|
||||
|
||||
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):
|
||||
if url in visited:
|
||||
return
|
||||
visited.add(url)
|
||||
def main_content(tree):
|
||||
nodes = tree.xpath("//div[contains(concat(' ', normalize-space(@class), ' '), ' document-page__content ')]")
|
||||
return nodes[0] if nodes else None
|
||||
|
||||
|
||||
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:
|
||||
page_html = download_html(url)
|
||||
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] skip unreachable page: {url} ({exc})")
|
||||
return
|
||||
print(f"[crawler] start page is unavailable: {exc}")
|
||||
current_url = FIRST_ARTICLE_URL
|
||||
add_known_urls(known_urls, [FIRST_ARTICLE_URL])
|
||||
|
||||
if is_terminal_page(page_html, url):
|
||||
text = extract_page_text(page_html, url)
|
||||
pages.append({"url": url, "text": text})
|
||||
return
|
||||
while current_url and current_url not in visited and len(visited) < max_pages:
|
||||
visited.add(current_url)
|
||||
|
||||
child_links = extract_document_links(page_html, url)
|
||||
for link in child_links:
|
||||
dfs(link)
|
||||
try:
|
||||
page_html = download_document_page(current_url)
|
||||
except RuntimeError as exc:
|
||||
print(f"[crawler] skip page {current_url}: {exc}")
|
||||
break
|
||||
|
||||
add_known_urls(known_urls, extract_article_urls(page_html, current_url))
|
||||
|
||||
if is_article_page(page_html):
|
||||
text = extract_page_text(page_html, current_url)
|
||||
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
|
||||
+51
-52
@@ -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 subprocess
|
||||
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:
|
||||
"""Читает ответ по частям; при таймауте возвращает уже полученные байты, если их достаточно."""
|
||||
chunks = []
|
||||
total = 0
|
||||
|
||||
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:
|
||||
break
|
||||
|
||||
chunks.append(chunk)
|
||||
total += len(chunk)
|
||||
|
||||
return b"".join(chunks)
|
||||
|
||||
def download_html(url: str, retries: int = 5, timeout: float = 40.0) -> str:
|
||||
"""Загружает HTML с повторными попытками при временных сетевых ошибках."""
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
req = Request(
|
||||
url,
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Encoding": "identity",
|
||||
"Accept-Language": "ru,en-US;q=0.9,en;q=0.8",
|
||||
"Connection": "close",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def download_with_curl(url: str, timeout: float) -> str:
|
||||
command = [
|
||||
"curl",
|
||||
"-L",
|
||||
"--compressed",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--max-time",
|
||||
str(int(timeout)),
|
||||
"-A",
|
||||
HEADERS["User-Agent"],
|
||||
url,
|
||||
]
|
||||
result = subprocess.run(command, capture_output=True, check=False)
|
||||
|
||||
if result.stdout:
|
||||
return result.stdout.decode("utf-8", errors="ignore")
|
||||
|
||||
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
|
||||
|
||||
for attempt in range(retries):
|
||||
for _ in range(retries):
|
||||
for loader in loaders:
|
||||
try:
|
||||
with urlopen(req, context=ctx, timeout=timeout) as response:
|
||||
raw = _read_response_bytes(response)
|
||||
return raw.decode("utf-8", errors="ignore")
|
||||
except IncompleteRead as exc:
|
||||
if exc.partial:
|
||||
return exc.partial.decode("utf-8", errors="ignore")
|
||||
return loader(url, timeout)
|
||||
except Exception as exc:
|
||||
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"
|
||||
dependencies = [
|
||||
"lxml>=6.0.4",
|
||||
"pymorphy3>=2.0.6",
|
||||
]
|
||||
|
||||
@@ -8,10 +8,23 @@ version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
{ name = "pymorphy3" },
|
||||
]
|
||||
|
||||
[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]]
|
||||
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/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