Files

167 lines
6.1 KiB
Python
Raw Permalink Normal View History

2026-05-25 01:12:43 +03:00
from __future__ import annotations
import re
from pathlib import Path
from bs4 import BeautifulSoup
from parser.config import NORMALIZED_ROOT
from parser.utils import (
chunk_paragraphs,
ensure_dir,
normalize_text,
parse_russian_date,
read_json,
write_json,
)
DOCUMENT_NUMBER_RE = re.compile(r"N\s*([0-9А-Яа-я\-ФКЗA-Z]+)")
def parse_root_metadata(root_html: str) -> dict:
soup = BeautifulSoup(root_html, "html.parser")
title_node = soup.select_one(".document-page__title h1")
title_text = title_node.get_text("\n", strip=True) if title_node else ""
content_node = soup.select_one(".document-page__content")
first_text = content_node.get_text("\n", strip=True) if content_node else ""
document_number = None
number_match = DOCUMENT_NUMBER_RE.search(first_text or title_text)
if number_match:
document_number = number_match.group(1)
adoption_date = parse_russian_date(first_text or title_text)
effective_date = None
effective_match = re.search(r"вступ\.\s*в силу с\s*(\d{2}\.\d{2}\.\d{4})", title_text)
if effective_match:
day, month, year = effective_match.group(1).split(".")
effective_date = f"{year}-{month}-{day}"
lines = [line.strip() for line in title_text.splitlines() if line.strip()]
return {
"title": lines[0] if lines else title_text.strip(),
"version_note": "\n".join(lines[1:]).strip() or None,
"document_number": document_number,
"adoption_date": adoption_date,
"effective_date": effective_date,
"publication_date": None,
}
def parse_article_page(article_html: str) -> list[str]:
soup = BeautifulSoup(article_html, "html.parser")
content = soup.select_one(".document-page__content")
if content is None:
return []
for selector in [
".document__insert",
".document__edit",
".document-page__notes",
".document-page__title-link",
".document-page__title",
]:
for node in content.select(selector):
node.decompose()
heading = content.select_one("h1")
if heading is not None:
heading.decompose()
paragraphs: list[str] = []
for node in content.select("p"):
text = normalize_text(node.get_text("\n", strip=True))
if not text:
continue
paragraphs.append(text)
return paragraphs
def normalize_document(raw_payload: dict) -> dict:
raw_dir = Path(raw_payload["raw_dir"])
root_html = (raw_dir / raw_payload["root_file"]).read_text(encoding="utf-8")
metadata = parse_root_metadata(root_html)
articles = []
for article_ref in raw_payload["articles"]:
article_html = (raw_dir / "articles" / article_ref["file_name"]).read_text(encoding="utf-8")
paragraphs = parse_article_page(article_html)
article_text = "\n\n".join(paragraphs).strip()
articles.append(
{
"article_number": article_ref["article_number"],
"article_title": article_ref["article_title"],
"article_url": article_ref["article_url"],
"section_title": article_ref.get("section_title"),
"chapter_title": article_ref.get("chapter_title"),
"part_title": article_ref.get("part_title"),
"breadcrumb": article_ref.get("breadcrumb", []),
"text": article_text,
"paragraphs": paragraphs,
}
)
normalized = {
"key": raw_payload["key"],
"title": metadata["title"],
"source_url": raw_payload["source_url"],
"source_type": "consultant_document",
"document_number": metadata["document_number"],
"adoption_date": metadata["adoption_date"],
"publication_date": metadata["publication_date"],
"effective_date": metadata["effective_date"],
"version_note": metadata["version_note"],
"version_hash": raw_payload["version_hash"],
"law_type": raw_payload["law_type"],
"consultant_category": raw_payload["consultant_category"],
"source_short_name": raw_payload["source_short_name"],
"articles": articles,
}
return normalized
def build_chunks(normalized_document: dict) -> list[dict]:
chunks: list[dict] = []
for article in normalized_document["articles"]:
paragraphs = article["paragraphs"] or ([article["text"]] if article["text"] else [])
text_chunks = chunk_paragraphs(paragraphs)
if not text_chunks and article["text"]:
text_chunks = [article["text"]]
for chunk_text in text_chunks:
chunks.append(
{
"chunk_index": len(chunks),
"article_number": article["article_number"],
"article_title": article["article_title"],
"chunk_text": chunk_text,
"metadata": {
"source_title": normalized_document["title"],
"source_short_name": normalized_document["source_short_name"],
"consultant_category": normalized_document["consultant_category"],
"chapter_title": article.get("chapter_title"),
"section_title": article.get("section_title"),
"article_number": article["article_number"],
"article_title": article["article_title"],
"document_url": article["article_url"],
"breadcrumb": article.get("breadcrumb", []),
"version_hash": normalized_document["version_hash"],
},
}
)
return chunks
def write_normalized_document(normalized_document: dict, dry_run: bool = False) -> Path:
output_path = NORMALIZED_ROOT / f"{normalized_document['key']}.json"
if not dry_run:
ensure_dir(output_path.parent)
write_json(output_path, normalized_document)
return output_path
def load_normalized_document(document_key: str) -> dict | None:
return read_json(NORMALIZED_ROOT / f"{document_key}.json", default=None)