101 lines
4.9 KiB
Python
101 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
||
"""Полигон, пакет-7 (Z4-добор): словарные статьи БКРС для списка терминов.
|
||
|
||
Зачем. В фазе A «что говорит БКРС» осталось UNCLEAR по ВСЕМ терминам: страница считалась
|
||
JS-рендеримой. На деле bkrs.info ставит одноразовый куки-челлендж (`ca=<hex>`) и перезагружает
|
||
страницу; воспроизведя куку, получаем СЕРВЕРНЫЙ html со статьёй. Словарная улика — сильнейший
|
||
класс из доступных нам (сильнее площадочной и сильнее аннотации), поэтому она закрывает больше
|
||
строк §A4.3, чем весь остальной добор.
|
||
|
||
Лицензия источника (со страницы «Скачать словарь», дамп 2026.06.10): «Базы можно использовать
|
||
свободно в любых целях. Можно указать данный сайт как источник.» — то есть провенанс-класс
|
||
«серый→зелёный при указании источника», и мы источник указываем.
|
||
|
||
$0: сетевой GET, ни одной модели. Вежливость: пауза между запросами, один проход по списку.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import html
|
||
import json
|
||
import re
|
||
import sys
|
||
import time
|
||
import urllib.parse
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||
"Chrome/120.0 Safari/537.36")
|
||
BASE = "https://bkrs.info/slovo.php?ch="
|
||
|
||
|
||
def get(url: str, cookie: str | None = None, timeout: int = 30) -> str:
|
||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||
if cookie:
|
||
req.add_header("Cookie", cookie)
|
||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||
return r.read().decode("utf-8", "replace")
|
||
|
||
|
||
def challenge_cookie(sample_html: str) -> str | None:
|
||
m = re.search(r'"(ca=[0-9a-f]+)', sample_html)
|
||
return m.group(1) if m else None
|
||
|
||
|
||
def extract_entry(page: str, term: str) -> str:
|
||
"""Тело словарной статьи: от заголовка-термина до служебного хвоста страницы."""
|
||
txt = html.unescape(re.sub(r"<(script|style)[^>]*>.*?</\1>", " ", page, flags=re.S))
|
||
txt = re.sub(r"<[^>]+>", "\n", txt)
|
||
lines = [x.strip() for x in txt.split("\n") if x.strip()]
|
||
# Статья начинается ПОСЛЕ последнего вхождения служебного пункта меню «Контакты»/«Войти»
|
||
start = 0
|
||
for i, l in enumerate(lines):
|
||
if l in ("Войти", "Контакты", "Тёмная тема"):
|
||
start = i + 1
|
||
body = lines[start:]
|
||
# и заканчивается перед подвалом
|
||
stop = len(body)
|
||
for i, l in enumerate(body):
|
||
if l.startswith(("В начало", "Обсуждение", "Комментарии", "Ошибки", "Пожаловаться",
|
||
"Добавить", "Правка", "Примеры", "Похожие")):
|
||
stop = i
|
||
break
|
||
return " ".join(body[:stop]).strip()
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--terms", required=True, type=Path, help="файл: по термину в строке")
|
||
ap.add_argument("--out", required=True, type=Path)
|
||
ap.add_argument("--sleep", type=float, default=1.2)
|
||
a = ap.parse_args()
|
||
|
||
terms = [t.strip() for t in a.terms.read_text(encoding="utf-8").splitlines()
|
||
if t.strip() and not t.startswith("#")]
|
||
|
||
first = get(BASE + urllib.parse.quote(terms[0]))
|
||
cookie = challenge_cookie(first)
|
||
if not cookie:
|
||
print("челлендж-кука не найдена — страница отдалась сразу", file=sys.stderr)
|
||
|
||
rows = []
|
||
for i, t in enumerate(terms, 1):
|
||
try:
|
||
page = get(BASE + urllib.parse.quote(t), cookie)
|
||
entry = extract_entry(page, t)
|
||
except Exception as e: # сеть — не повод останавливать проход
|
||
entry = f"__ОШИБКА__ {e}"
|
||
found = bool(entry) and "__ОШИБКА__" not in entry and "Не найдено" not in entry
|
||
rows.append({"src": t, "found": found, "entry": entry})
|
||
print(f"[{i}/{len(terms)}] {t}\t{'OK' if found else 'НЕТ'}\t{entry[:110]}", file=sys.stderr)
|
||
time.sleep(a.sleep)
|
||
|
||
a.out.write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
ok = sum(r["found"] for r in rows)
|
||
print(f"\nитого: {ok}/{len(rows)} статей найдено → {a.out}", file=sys.stderr)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|