From 868fa9268a87f89e84a68bd3e6a7bb230751fdfa Mon Sep 17 00:00:00 2001 From: Dmitry Kokorin Date: Wed, 16 Sep 2026 09:59:32 +0300 Subject: [PATCH] tools/mgu.py: MGU Scientific Library search (GET-discovery, fields/methods/pagination, ISB dead, SER goldmine); AGENTS.md source 22 + Google low-volume warning --- AGENTS.md | 18 ++++ tools/__pycache__/mgu.cpython-312.pyc | Bin 0 -> 7193 bytes tools/mgu.py | 128 ++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 tools/__pycache__/mgu.cpython-312.pyc create mode 100644 tools/mgu.py diff --git a/AGENTS.md b/AGENTS.md index f21efa7..70da68f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,6 +139,10 @@ sections/0N-/ # one .md per BOOK (chapter-level list items are merged int - `books.google.com/books?vid=ISBN<13digits>`: **plain curl works** — 200 + `"T - A - Google Книги"` if indexed, 404 if not. Good ISBN validator incl. RU (АСТ/Эксмо/БукСМарт hit; small-press 404). googleapis.com/books API stays 429 without a key. +- **Keep volume LOW**: Google may block agents on sustained querying (user warning 2026-07-15). + Budget: a few dozen requests per session, 2–5s random pauses, never in tight loops; treat as + an auxiliary cross-check, not a bulk source. If 429/403-redirect walls appear — stop and fall + back to OL/libgen/RSL. - `fetch_content` on the same URL → richer metadata: full author/editor names, edition, series. Resolved Ottmann=Klaus, Goldstein=Ralph, Brutsche=Paul, Elder=George, Moon=Beverly, Killick=Katherine, Pennington/Staples, Rowland=Susan, Bolander=Karen, Acton=Mary via this or OL. @@ -213,6 +217,20 @@ sections/0N-<name>/ # one .md per BOOK (chapter-level list items are merged int - **isbnsearch.org**: nice per-ISBN pages (author/publisher/year) but rate-walls after ~10 requests ("Please Verify to Continue", no recovery in 7 min) — not for batches. +### 22. MGU Scientific Library (nbmgu.ru) — server-rendered, curl-able (2026-07-15) +- Tool: `tools/mgu.py "query" [FIELD] [pages] [method]` or `--multi "AUT:X" "TIT:Y"`. + The "JS" advanced search is a plain GET: `/search/?adv=1&q1=<q>&f1=<F>&v1=<M>&cat=BOOK[&p=N]`. +- Fields: ANY/AUT/COA/TIT/KEY/RUB/YEA/PLA/PUB/SER/ISB/ISS/NBM; rows AND between q1..q3. + Method v: 0=Слова (stemming, default) · 1=Словосочетание · 2=Начинается с · 3=Дословно. +- "Всего: N" + GOST row (title/authors/notes) + "City : Publisher, Year" + shelf code + uid link. + 20 rows/page, p=0-based; **out-of-range p silently falls back to page 0** (dedupe by uid!). +- **ISB field NOT populated** (0 hits, 3 formats × control ISBNs) — no ISBN search; use + AUT/TIT/SER/PUB. `storing.aspx?uid=` page = holdings/order only (no full record). +- Value: second RU catalog (complements RSL/РНБ), strong **SER** sweep (e.g. «Библиотека + аналитической психологии» → full series list in one query), GOST rows w/ translators. +- Anchor titles in result rows contain a raw `>` (title="Хранение<br/>Заказ") — regexes must + not use [^>]+ across the anchor attrs. + ## Not usable / low value - libgen biblioservice worldcat/googlebooks/isbndb/udc (broken on this mirror) - imaton.com (МААП educational publisher, not translations) diff --git a/tools/__pycache__/mgu.cpython-312.pyc b/tools/__pycache__/mgu.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34acd795378033a15ecb1c38859257daaf75ec39 GIT binary patch literal 7193 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 Jz#;{<0swmkF`xhd literal 0 HcmV?d00001 diff --git a/tools/mgu.py b/tools/mgu.py new file mode 100644 index 0000000..a4a9433 --- /dev/null +++ b/tools/mgu.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""MGU Scientific Library (nbmgu.ru) catalog search — server-rendered, curl-able. + +Discovered 2026-07-15 from the advanced-search page JS (functions.js +CreateClientRequest): the "JS" search is a plain GET: + + https://nbmgu.ru/search/?adv=1&q1=<query>&f1=<FIELD>&v1=0&cat=BOOK[&p=<page>] + +Fields (f1..f3, AND-combined): + Fields (f1..f3, AND-combined between rows): + ANY все · AUT автор · COA колл. автор · TIT заглавие · KEY ключ. слова + RUB рубрики · YEA год · PLA место · PUB изд-во · SER серия + ISB ISBN · ISS ISSN · NBM индексы НБ МГУ +Method (v1..v3, per row): 0=Слова (stemming, default) · 1=Словосочетание (phrase) + · 2=Начинается с · 3=Дословно (exact). +Results: "Всего: N", 20 rows/page (p=0-based). Row = GOST description text +(title / authors; notes - [edition]) + "City : Publisher, Year" + shelf code. +Detail page /order/storing.aspx?uid=... is holdings/order only (no full record). +**ISB field NOT populated** (0 hits for 3 formats x 2 control ISBNs) — search by +AUT/TIT/SER/PUB instead; ISBN validation stays with РГБ/Google/OL (tools/isbnval.py). + +Usage: + tools/mgu.py "query" [FIELD=ANY] [pages=1] [method=0] + tools/mgu.py --multi "AUT:Франц" "TIT:Число" ["TIT:точное:3"] (AND rows) +""" +import re, subprocess, sys, time + +UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +BASE = "https://nbmgu.ru/search/" + + +def search_url(pairs, p=0, cat="BOOK"): + for attempt in range(3): + args = ["curl", "-s", "-G", "-A", UA, "--max-time", "90", "-e", BASE] + args += ["--data-urlencode", "adv=1"] + # mirror the browser: always send slots 1..N (empties included) + for i, pr in enumerate(pairs, 1): + f, q = pr[0], pr[1] + v = str(pr[2]) if len(pr) > 2 else "0" + args += ["--data-urlencode", f"q{i}={q}", + "--data-urlencode", f"f{i}={f}", + "--data-urlencode", f"v{i}={v}"] + if p: + args += ["--data-urlencode", f"p={p}"] + args += ["--data-urlencode", f"cat={cat}", BASE] + try: + out = subprocess.run(args, capture_output=True, timeout=120) + if out.returncode != 0 or len(out.stdout) < 5000: + print(f" (curl rc={out.returncode} size={len(out.stdout)} err={out.stderr[:120]!r})", + file=sys.stderr) + body = out.stdout.decode("utf-8", "replace") + except Exception as e: + print(f" (subprocess error: {e})", file=sys.stderr) + body = "" + if len(body) > 5000: + return body + time.sleep(4 * (attempt + 1)) + return body + + +def parse(html): + total = "" + m = re.search(r"Всего:\s*(\d+)", html) + if m: + total = m.group(1) + rows = [] + # NOTE: anchor title attr contains a raw '>' ("<br/>"), so plain [^>]+ fails; + # split on row start and rely on href being the LAST attribute of the anchor. + for chunk in html.split('<li class="result">')[1:]: + cm = re.search(r'<h2>\s*(\d+)\.\s*<a\b.*?href="(/order/storing\.aspx\?[^\"]+)"\s*>(.*?)</a>', + chunk, re.S) + pm = re.search(r"</h2>\s*<p>(.*?)</p>", chunk, re.S) + sm = re.search(r"Шифр:\s*([^<]+)", chunk) + if not (cm and pm): + continue + clean = lambda s: re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", s)).strip() + rows.append({ + "n": cm.group(1), + "desc": clean(cm.group(3)), + "pubyear": clean(pm.group(1)), + "shelf": clean(sm.group(1)) if sm else "", + "url": "https://nbmgu.ru" + cm.group(2), + }) + return total, rows + + +def main(): + argv = sys.argv[1:] + if not argv: + sys.exit(__doc__) + if argv[0] == "--multi": + pairs = [] + for spec in argv[1:]: + parts = spec.split(":") + f = parts[0].upper() + q = parts[1] if len(parts) > 1 else "" + v = int(parts[2]) if len(parts) > 2 else 0 + pairs.append((f, q, v) if v else (f, q)) + pmax = 1 + else: + q = argv[0] + f = argv[1].upper() if len(argv) > 1 else "ANY" + v = int(argv[3]) if len(argv) > 3 else 0 + pairs = [(f, q, v) if v else (f, q)] + pmax = int(argv[2]) if len(argv) > 2 else 1 + all_rows = [] + total = "" + seen = set() + for p in range(pmax): + html = search_url(pairs, p=p) + total, rows = parse(html) + # out-of-range p falls back to page 0 on the server side — dedupe by url + new = [r for r in rows if r["url"] not in seen] + if not new: + break + seen.update(r["url"] for r in new) + all_rows += new + if total and len(all_rows) >= int(total): + break + time.sleep(3) + print(f"### MGU: {pairs} — total: {total} (fetched {len(all_rows)})") + for r in all_rows: + print(f"{r['n']:>3}. {r['desc']}") + print(f" {r['pubyear']} | шифр: {r['shelf']} | {r['url']}") + + +if __name__ == "__main__": + main()