"""Парсеры источников: text/pdf/epub/web → чанки.""" from __future__ import annotations import hashlib from dataclasses import dataclass, field @dataclass class ParsedChunk: seq: int content: str locator: dict = field(default_factory=dict) @dataclass class ParsedDoc: kind: str uri: str | None title: str | None chunks: list[ParsedChunk] content_hash: str = "" def parse_text(text: str, *, chunk_size: int = 2000, overlap: int = 200, title: str | None = None, uri: str | None = None) -> ParsedDoc: """Разбить сырой текст на чанки с перекрытием.""" text = text.strip() chunks: list[ParsedChunk] = [] start = 0 seq = 0 while start < len(text): end = min(start + chunk_size, len(text)) # не резать посреди слова if end < len(text): space = text.rfind(" ", start, end) if space > start + chunk_size // 2: end = space chunks.append(ParsedChunk(seq=seq, content=text[start:end].strip())) seq += 1 if end >= len(text): break start = end - overlap return ParsedDoc(kind="text", uri=uri, title=title, chunks=chunks, content_hash=hashlib.sha256(text.encode()).hexdigest()) def parse_pdf(path: str) -> ParsedDoc: from pypdf import PdfReader reader = PdfReader(path) text = "\n\n".join((page.extract_text() or "") for page in reader.pages) doc = parse_text(text, title=path.split("/")[-1], uri=path) doc.kind = "pdf" return doc def parse_epub(path: str) -> ParsedDoc: import ebooklib from ebooklib import epub book = epub.read_epub(path) parts = [] for item in book.get_items_of_type(ebooklib.ITEM_DOCUMENT): parts.append(item.get_content().decode("utf-8", errors="ignore")) text = "\n\n".join(parts) doc = parse_text(text, title=path.split("/")[-1], uri=path) doc.kind = "epub" return doc def _http_client(proxy=None, timeout: int = 30): """httpx.Client с единым User-Agent. proxy: явный прокси (None = прямой запрос). Если проси не передан, но config.PROXY_URL задан — используется он. """ import httpx from . import config kwargs = dict(timeout=timeout, follow_redirects=True, headers={"User-Agent": config.USER_AGENT}) use_proxy = proxy or (config.PROXY_URL or None) if use_proxy: kwargs["proxy"] = use_proxy return httpx.Client(**kwargs) def _fetch_text(url: str, proxy: str | None = None) -> str | None: """Fetch + extract main content. Tries direct; on error/empty retries `proxy`. trafilatura.extract has no timeout -> fetch via httpx with timeout. """ import trafilatura for _lbl, _prx in (("direct", None), ("proxy", proxy)): try: with _http_client(proxy=_prx, timeout=30) as client: r = client.get(url) r.raise_for_status() text = trafilatura.extract(r.text, include_comments=False, include_tables=True, url=url) if text: return text except Exception: continue return None def parse_web(url: str) -> ParsedDoc: from . import config # direct fetch + socks5h fallback on block/timeout text = _fetch_text(url, proxy=config.SOCKS_FALLBACK_PROXY) if not text: raise ValueError( f"no main content for {url} " f"(direct + {config.SOCKS_FALLBACK_PROXY} fallback failed)" ) doc = parse_text(text, uri=url, title=url) doc.kind = "web" return doc