Add loader and processor modules with documentation and output files

- Implemented loader in `modules/loader.py` to download HTML pages using curl and urllib.
- Created processor in `modules/processor.py` for processing the text of the Russian Criminal Code (УК РФ).
- Added README files for both loader and processor explaining their functionality and usage.
- Generated output files including original text, prepared text, and subject index in CSV and JSON formats.
This commit is contained in:
Dmitry
2026-04-16 21:38:00 +03:00
parent 502f48a279
commit 0b41577a42
15 changed files with 8770 additions and 103 deletions
+1
View File
@@ -8,3 +8,4 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
cache/
+24
View File
@@ -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.
+176
View File
@@ -0,0 +1,176 @@
## Как работает crawler
Crawler находится в `modules/crawler.py`. Его задача простая: пройти по страницам статей УК РФ на ConsultantPlus и вернуть только содержательный текст статей.
В коде используются: `urllib`/`curl` для загрузки, `lxml` и `XPath` для разбора HTML, регулярные выражения для очистки строк.
## Общий алгоритм
1. Загружаем стартовую страницу УК РФ:
```python
START_URL = "https://www.consultant.ru/document/cons_doc_LAW_10699/"
```
2. Ищем ссылки на статьи вида `УК РФ Статья 1...`.
3. Берем первую статью.
4. Загружаем страницу статьи.
5. Извлекаем текст из блока `document-page__content`.
6. Удаляем заголовки, служебные блоки и редакционные пометки.
7. Переходим к следующей странице по ссылке `pages__right`.
8. Повторяем, пока не дойдем до последней статьи или до лимита `max_pages`.
Главная функция:
```python
pages = crawl_document()
```
Она возвращает список словарей:
```python
{
"url": "адрес страницы",
"title": "заголовок статьи",
"text": "очищенный текст статьи",
}
```
## Загрузка страницы
Загрузка вынесена в `modules/loader.py`.
Для обычных сайтов используется `urllib`, а для ConsultantPlus сначала пробуется `curl`, потому что сайт иногда нестабильно отдает большие HTML-страницы.
В crawler используется функция:
```python
download_document_page(url)
```
Она делает несколько попыток с таймаутами `8`, `30`, `30` секунд. Для страниц статей проверяется, что HTML полный и содержит `</html>`.
Для стартовой страницы допускается частичный HTML:
```python
download_document_page(start_url, allow_partial=True)
```
Это нужно потому, что оглавление может успеть прийти даже тогда, когда сайт оборвал конец HTML.
## Определение страницы статьи
Страница считается статьей, если ее заголовок подходит под регулярное выражение:
```python
ARTICLE_RE = re.compile(r"^\s*(?:УК РФ,?\s+)?Статья\s+\d+(?:\.\d+)?\.")
```
То есть подходят такие варианты:
```text
УК РФ Статья 1. ...
УК РФ, Статья 53.1. ...
Статья 361. ...
```
Проверка выполняется функцией:
```python
is_article_page(page_html)
```
## Извлечение текста статьи
Текст достается функцией:
```python
extract_page_text(page_html)
```
Она делает четыре шага:
1. Парсит HTML через `lxml.html.fromstring`.
2. Находит основной блок:
```text
document-page__content
```
3. Удаляет служебные элементы:
- `h1`;
- `doc-style`;
- `doc-insert`;
- `doc-roll`.
4. Берет текст из абзацев `p`.
Дополнительно функция `clean_text()`:
- заменяет неразрывные пробелы;
- схлопывает лишние пробелы;
- убирает номера пунктов вроде `1.`, `2.`, `а)`.
## Удаление служебных строк
Функция:
```python
service_line(line)
```
убирает строки, которые не относятся к содержанию статьи:
```text
(в ред. Федерального закона ...)
(см. текст в предыдущей редакции)
(часть третья введена ...)
Президент
Б.ЕЛЬЦИН
13 июня 1996 года
N 63-ФЗ
```
## Переход к следующей статье
Основной способ перехода:
```python
extract_next_document_url(page_html, current_url)
```
Функция ищет правую ссылку ConsultantPlus:
```text
pages__right
```
Если такой ссылки нет, crawler использует запасной список `known_urls`. В него заранее складываются все найденные ссылки на статьи. Это помогает продолжить обход, если нижняя навигация не попала в загруженный HTML.
## Защита от лишних ссылок
Crawler проверяет, что каждая ссылка относится именно к УК РФ:
```python
is_same_document(url, prefix)
```
Для УК РФ правильный префикс:
```text
/document/cons_doc_LAW_10699/
```
Поэтому crawler не уходит в новости, другие кодексы, комментарии и внешние документы.
## Где используется результат
В `main.py` результат объединяется в один текст:
```python
pages = crawl_document(SOURCE_URL)
original_text = "\n".join(page["text"] for page in pages)
```
После этого `original_text` передается в `modules/processor.py`, где строятся подготовленный текст и предметный указатель.
+127
View File
@@ -0,0 +1,127 @@
## Как работает loader
Loader находится в `modules/loader.py`. Его задача одна: скачать HTML-страницу и вернуть ее как строку.
Есть загрузка через `curl`, загрузка через `urllib` и общая функция `download_html()`, которая выбирает порядок попыток.
## Заголовки запроса
В начале файла задан словарь:
```python
HEADERS
```
Он содержит HTTP-заголовки:
```text
User-Agent
Accept
Accept-Language
```
Они нужны, чтобы сайт отдавал обычную HTML-страницу, как браузеру.
## Загрузка через curl
Функция:
```python
download_with_curl(url, timeout)
```
запускает системную команду `curl` через `subprocess.run`.
Используются параметры:
```text
-L переходить по редиректам
--compressed принимать сжатый ответ
--silent не печатать лишний прогресс
--show-error показать ошибку, если она есть
--max-time ограничить время загрузки
-A передать User-Agent
```
Если `curl` что-то скачал в `stdout`, функция декодирует байты как UTF-8 и возвращает HTML-строку.
Для ConsultantPlus этот способ оказался надежнее, чем чистый `urllib`, потому что сайт иногда нестабильно отдает большие страницы.
## Загрузка через urllib
Функция:
```python
download_with_urllib(url, timeout)
```
использует стандартную библиотеку Python:
```python
urllib.request
```
Алгоритм:
1. Создается SSL-контекст.
2. Создается `Request` с заголовками `HEADERS`.
3. Вызывается `urlopen`.
4. Ответ читается и декодируется как UTF-8.
SSL-проверка отключена:
```python
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
```
Это сделано для учебной устойчивости загрузки, чтобы сертификаты сайта не мешали выполнению задания.
## Главная функция
Основная функция модуля:
```python
download_html(url, retries=3, timeout=30.0)
```
Она возвращает HTML-код страницы.
Логика выбора загрузчика:
- если домен заканчивается на `consultant.ru`, сначала пробуется `curl`, затем `urllib`;
- для остальных сайтов сначала пробуется `urllib`, затем `curl`.
Это задается строкой:
```python
is_consultant = urlparse(url).netloc.endswith("consultant.ru")
```
## Повторные попытки
В `download_html()` есть цикл:
```python
for _ in range(retries):
```
На каждой попытке функция пробует все доступные загрузчики. Если оба способа не сработали, программа ждет `0.5` секунды и пробует еще раз.
Если после всех попыток страница не загрузилась, выбрасывается ошибка:
```python
RuntimeError
```
Эту ошибку затем обрабатывает crawler.
## Где используется loader
Crawler вызывает loader через функцию:
```python
download_html(url, retries=1, timeout=timeout)
```
То есть loader ничего не знает про УК РФ, статьи или обработку текста. Он только скачивает HTML, а вся логика обхода находится в `modules/crawler.py`.
+257
View File
@@ -0,0 +1,257 @@
## Как работает processor
Processor находится в `modules/processor.py`. Он получает уже очищенный исходный текст УК РФ и готовит его к анализу.
В коде используются простые инструменты: регулярные выражения, списки стоп-слов, `pymorphy3` для лемматизации, `Counter` для подсчета частот, `csv` и `json` для записи результата.
## Что делает processor
Основные задачи:
1. Найти слова в тексте.
2. Привести каждое слово к нормальной форме.
3. Удалить лишние слова: стоп-слова, служебные слова структуры закона, числительные, единицы измерения после чисел, имена собственные и названия объектов.
4. Собрать подготовленный текст.
5. Построить предметный указатель на 100 самых частых слов.
## Поиск слов
Слова ищутся регулярным выражением:
```python
WORD_RE = re.compile(r"[А-Яа-яЁё]+(?:-[А-Яа-яЁё]+)?")
```
Оно находит русские слова, в том числе слова с дефисом:
```text
уголовно-правовой
социально-опасный
```
Цифры и пунктуация этим выражением не выбираются.
## Лемматизация
Лемматизация выполняется через `pymorphy3`.
Функция:
```python
parse_word(word)
```
возвращает:
```python
lemma, tag
```
Например:
```text
преступлений -> преступление
лишением -> лишение
осужденного -> осудить
```
`tag` нужен для фильтрации числительных и имен собственных. Чтобы одно и то же слово не разбирать много раз, используется `lru_cache`.
## Стоп-слова
Стоп-слова лежат в множестве:
```python
STOP_WORDS
```
Туда входят обычные служебные слова:
```text
и, в, на, что, этот, который
```
и структурные слова закона:
```text
статья, часть, глава, раздел, пункт, кодекс
```
Они часто встречаются, но не являются полезными терминами предметного указателя.
## Удаление числительных
Числительные удаляются двумя способами.
Первый способ: по списку слов:
```python
NUMERAL_WORDS
```
Например:
```text
один, два, три, пятьсот, тысяча, миллион
```
Второй способ: по морфологическим тегам `pymorphy3`:
```python
NUMR
Anum
```
Так удаляются формы вроде:
```text
трех
пяти
первой
второго
```
## Удаление единиц после чисел
Есть слова, которые сами по себе могут быть полезными, но после числа обычно являются частью числительного выражения.
Например:
```text
до трех лет
500 рублей
на срок шесть месяцев
```
Для этого используется список:
```python
NUMBER_UNITS
```
Туда входят:
```text
год, месяц, день, час, рубль, процент, метр
```
Если такое слово стоит после числительного или после цифры, оно удаляется.
## Удаление имен собственных
Имена собственные удаляются тремя способами.
Первый способ: по морфологическим тегам:
```python
Name, Surn, Patr, Geox, Orgn
```
Так удаляются имена, фамилии, отчества, географические названия и организации, если их распознал `pymorphy3`.
Второй способ: по списку отдельных слов:
```python
PROPER_WORDS
```
Например:
```text
москва, россия, рф, ельцин, интернет
```
Третий способ: по устойчивым словосочетаниям:
```python
PROPER_PHRASES
```
Например:
```text
российская федерация
государственная дума
федеральный закон
уголовный кодекс
центральный банк
```
Сначала слова в строке лемматизируются, затем функция `find_phrase_positions()` ищет такие фразы среди лемм и помечает их позиции как лишние.
## Фильтрация одной строки
Основная функция для одной строки:
```python
good_words_from_line(line)
```
Она делает следующее:
1. Разбивает строку на слова.
2. Для каждого слова находит лемму и морфологические теги.
3. Находит позиции слов, которые входят в имена собственные из нескольких слов.
4. Проверяет каждое слово функцией `is_bad_word()`.
5. Возвращает только подходящие слова.
## Подготовленный текст
Функция:
```python
prepare_text(text)
```
проходит по строкам исходного текста, оставляет только хорошие леммы и склеивает их обратно в строки.
Результат записывается в:
```text
output/uk_rf_prepared.txt
```
## Предметный указатель
Функция:
```python
build_subject_index(text, top_n=100)
```
строит индекс так:
1. Проходит по всем подготовленным словам.
2. Считает частоты через `Counter`.
3. Запоминает первое появление каждого слова: номер строки, номер символа и исходную форму слова.
4. Возвращает 100 самых частотных слов.
Одна запись индекса выглядит так:
```python
{
"word": "срок",
"count": 3342,
"line": 34,
"char": 151,
"source_word": "срок",
}
```
## Запись результатов
Для сохранения индекса есть две функции:
```python
write_subject_index_csv(entries, path)
write_subject_index_json(entries, path)
```
Они создают:
```text
output/uk_rf_subject_index.csv
output/uk_rf_subject_index.json
```
CSV удобен для просмотра в таблице, JSON удобен для дальнейшей обработки программой.
+32 -2
View File
@@ -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
View File
@@ -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: 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: except RuntimeError as exc:
print(f"[crawler] skip unreachable page: {url} ({exc})") print(f"[crawler] start page is unavailable: {exc}")
return current_url = FIRST_ARTICLE_URL
add_known_urls(known_urls, [FIRST_ARTICLE_URL])
if is_terminal_page(page_html, url): while current_url and current_url not in visited and len(visited) < max_pages:
text = extract_page_text(page_html, url) visited.add(current_url)
pages.append({"url": url, "text": text})
return
child_links = extract_document_links(page_html, url) try:
for link in child_links: page_html = download_document_page(current_url)
dfs(link) 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 return pages
+51 -52
View File
@@ -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 = []
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",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "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", "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 last_error = None
for attempt in range(retries): for _ in range(retries):
for loader in loaders:
try: try:
with urlopen(req, context=ctx, timeout=timeout) as response: return loader(url, timeout)
raw = _read_response_bytes(response) except Exception as exc:
return raw.decode("utf-8", errors="ignore")
except IncompleteRead as exc:
if exc.partial:
return exc.partial.decode("utf-8", errors="ignore")
last_error = 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
+239
View File
@@ -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
+101
View File
@@ -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;оружия
1 word count line char source_word
2 срок 3342 34 151 срок
3 размер 2024 136 25 размере
4 лишение 1843 31 183 лишения
5 свобода 1563 3 53 свобод
6 определенный 1399 120 24 определенные
7 иной 1267 4 241 иные
8 лицо 1224 7 1 Лица
9 наказываться 1084 493 1 наказывается
10 работа 1080 122 14 работы
11 осудить 973 25 746 осуждены
12 таковой 942 409 174 таковых
13 штраф 899 119 1 штраф
14 период 873 136 135 период
15 доход 864 136 113 дохода
16 деятельность 824 53 752 деятельности
17 заработный 808 136 86 заработной
18 плата 808 136 97 платы
19 право 797 2 118 права
20 должность 704 120 37 должности
21 преступление 696 3 309 преступлений
22 занимать 694 120 15 занимать
23 заниматься 682 120 51 заниматься
24 принудительный 659 56 135 принудительные
25 деяние 657 4 174 деяния
26 совершенный 578 20 15 совершенные
27 предусмотренный 474 14 113 предусмотренного
28 ограничение 447 116 223 ограничении
29 группа 410 85 144 группу
30 наказание 406 4 229 наказаний
31 средство 353 53 541 средством
32 крупный 338 848 122 крупных
33 совершение 329 4 283 совершение
34 обязательный 261 114 116 обязательных
35 организация 250 53 870 организацию
36 повлечь 248 546 68 повлекшие
37 тяжкий 237 30 185 тяжкие
38 использование 234 84 221 использования
39 исправительный 229 123 1 исправительные
40 совершить 228 7 7 совершившие
41 отношение 213 7 213 отношения
42 действие 206 8 74 действия
43 цель 204 13 121 целью
44 государственный 188 53 2031 государственного
45 равно 187 85 107 равно
46 сумма 185 136 229 сумме
47 применение 184 6 1 Применение
48 предусмотреть 184 18 133 предусмотренных
49 организовать 183 85 32 организовавшее
50 уголовный 180 1 1 Уголовное
51 особо 178 30 207 особо
52 суд 175 23 349 суда
53 предварительный 165 93 124 предварительного
54 военный 163 21 401 военном
55 незаконный 162 53 1199 незаконном
56 случай 161 22 141 случае
57 сговор 161 74 174 сговор
58 примечание 159 296 1 Примечание
59 ответственность 156 1 122 ответственность
60 арест 156 127 1 арест
61 причинение 151 9 71 причинение
62 неосторожность 143 60 78 неосторожности
63 ущерб 142 204 135 ущерба
64 признаваться 141 4 181 признаются
65 заведомо 141 53 1130 заведомо
66 другой 138 7 297 других
67 несовершеннолетний 138 54 6 несовершеннолетний
68 вред 137 9 82 вреда
69 положение 134 7 184 положения
70 орган 133 80 135 органам
71 имущество 131 53 621 имущества
72 вещество 131 53 1607 веществ
73 вид 127 4 224 виды
74 служба 126 124 24 службе
75 информация 125 87 107 информации
76 здоровье 124 53 179 здоровью
77 указанный 118 35 337 указанного
78 нарушение 118 201 29 нарушении
79 сеть 117 230 147 сетях
80 человек 116 3 60 человека
81 последствие 112 5 72 последствия
82 служебный 110 104 139 служебного
83 административный 107 298 288 административной
84 гражданин 106 3 71 гражданина
85 подкуп 106 86 97 подкупа
86 превышать 105 31 164 превышает
87 денежный 105 135 12 денежное
88 смерть 104 492 41 смерти
89 законодательство 101 1 11 законодательство
90 число 101 17 274 числе
91 насилие 101 101 295 насилием
92 иностранный 98 22 68 иностранных
93 характер 94 4 270 характера
94 соответствие 92 22 234 соответствии
95 отбывание 92 27 246 отбывания
96 угроза 92 28 109 угрозой
97 возраст 89 48 48 возрасте
98 медицинский 87 56 155 медицинского
99 должностной 86 7 171 должностного
100 условие 86 35 296 условии
101 оружие 86 53 1764 оружия
+702
View File
@@ -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": "оружия"
}
]
+1
View File
@@ -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",
] ]
Generated
+46 -1
View File
@@ -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" },
]