123 lines
5.0 KiB
Python
123 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
"""scripts/fetch_format_rules.py —— 格式规则自动获取(第一批,人工核验用 CLI)。
|
|||
|
|
|
|||
|
|
用法(在 review_service/ 下):
|
|||
|
|
/home/xzq/5090/teamWorkspace/zh/plantai-rag-new-venv/bin/python \
|
|||
|
|
scripts/fetch_format_rules.py <journal_id> <guidelines_url>
|
|||
|
|
|
|||
|
|
流程:
|
|||
|
|
1. fetch_submission_guidelines(url) # robots.txt + 同域 ≥2s 限速 + 正文转 Markdown
|
|||
|
|
2. extract_format_rules(markdown, journal_name) # DeepSeek 抽取(llm.py)
|
|||
|
|
3. 存 data/format_cache/{journal_id}.json(失败也存,便于批量对账)
|
|||
|
|
4. 打印完整结果 JSON 供人工核验
|
|||
|
|
|
|||
|
|
凭证:结果文件顶层 source_url(步骤 1 的实际抓取地址)+ source_quotes(LLM
|
|||
|
|
逐字引证,按字段分组);非 null 字段缺引证 → 抽取判失败(ok=false)。
|
|||
|
|
|
|||
|
|
约束(第一批):只跑指定 3 本;不触 API / 前端 / journals.json(format_rules 只读
|
|||
|
|
查名,不写回)。journal_name 优先取 data/journals/journals.json 的 name(只读)。
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import asyncio
|
|||
|
|
import json
|
|||
|
|
import os
|
|||
|
|
import re
|
|||
|
|
import sys
|
|||
|
|
from datetime import datetime, timezone
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
_ROOT = Path(__file__).resolve().parents[1] # review_service/
|
|||
|
|
sys.path.insert(0, str(_ROOT))
|
|||
|
|
|
|||
|
|
_JOURNALS_JSON = _ROOT / "data" / "journals" / "journals.json"
|
|||
|
|
_CACHE_DIR = _ROOT / "data" / "format_cache"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_env() -> None:
|
|||
|
|
"""镜像 restart.sh 的 `set -a; source .env`:REVIEW_LLM_*/DOCGEN_LLM_* 裸键
|
|||
|
|
由 services/llm.py 在每次调用时读 os.environ(不缓存),故调用前灌入即可。"""
|
|||
|
|
env_path = _ROOT / ".env"
|
|||
|
|
if not env_path.is_file():
|
|||
|
|
return
|
|||
|
|
for raw in env_path.read_text("utf-8").splitlines():
|
|||
|
|
line = raw.strip()
|
|||
|
|
if not line or line.startswith("#") or "=" not in line:
|
|||
|
|
continue
|
|||
|
|
if line.startswith("export "):
|
|||
|
|
line = line[7:].strip()
|
|||
|
|
key, _, val = line.partition("=")
|
|||
|
|
os.environ.setdefault(key.strip(), val.strip().strip("\"'"))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _journal_name(journal_id: str) -> str:
|
|||
|
|
"""journals.json 只读查名;查不到 → id 连字符转空格并首字母大写(LLM 上下文可读)。"""
|
|||
|
|
try:
|
|||
|
|
data = json.loads(_JOURNALS_JSON.read_text("utf-8"))
|
|||
|
|
for j in data.get("journals") or []:
|
|||
|
|
if j.get("id") == journal_id:
|
|||
|
|
return str(j.get("name") or journal_id)
|
|||
|
|
except Exception: # noqa: BLE001
|
|||
|
|
pass
|
|||
|
|
return journal_id.replace("-", " ").title()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> int:
|
|||
|
|
if len(sys.argv) != 3:
|
|||
|
|
print("用法: fetch_format_rules.py <journal_id> <guidelines_url>", file=sys.stderr)
|
|||
|
|
return 2
|
|||
|
|
journal_id = re.sub(r"[^\w.-]", "_", sys.argv[1].strip())
|
|||
|
|
url = sys.argv[2].strip()
|
|||
|
|
_load_env()
|
|||
|
|
|
|||
|
|
from app.services.format_extractor import extract_format_rules
|
|||
|
|
from app.services.format_fetcher import fetch_submission_guidelines
|
|||
|
|
|
|||
|
|
async def run() -> tuple[dict, dict]:
|
|||
|
|
fetched = await fetch_submission_guidelines(url)
|
|||
|
|
print(f"[1/2] 抓取 {url} → ok={fetched['ok']}"
|
|||
|
|
+ (f",markdown={len(fetched['markdown'])} 字符"
|
|||
|
|
if fetched["ok"] else f",error={fetched['error']}"))
|
|||
|
|
if not fetched["ok"]:
|
|||
|
|
return fetched, {"ok": False, "error": f"抓取失败:{fetched['error']}",
|
|||
|
|
"rules": None, "source_quotes": None,
|
|||
|
|
"confidence": None, "notes": None, "raw": None}
|
|||
|
|
extracted = await extract_format_rules(fetched["markdown"], _journal_name(journal_id))
|
|||
|
|
if extracted["ok"]:
|
|||
|
|
filled = sum(1 for v in extracted["rules"].values() if v is not None)
|
|||
|
|
print(f"[2/2] LLM 抽取 → ok=True,8 字段中 {filled} 个非 null")
|
|||
|
|
else:
|
|||
|
|
print(f"[2/2] LLM 抽取 → ok=False,error={extracted['error']}")
|
|||
|
|
return fetched, extracted
|
|||
|
|
|
|||
|
|
fetched, extracted = asyncio.run(run())
|
|||
|
|
|
|||
|
|
result = {
|
|||
|
|
"journal_id": journal_id,
|
|||
|
|
"journal_name": _journal_name(journal_id),
|
|||
|
|
"source_url": url,
|
|||
|
|
"fetched_at": fetched.get("fetched_at"),
|
|||
|
|
"extracted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|||
|
|
"markdown_chars": len(fetched.get("markdown") or ""),
|
|||
|
|
"ok": bool(extracted.get("ok")),
|
|||
|
|
"error": extracted.get("error"),
|
|||
|
|
"format_rules": ({**extracted["rules"], "rules_source": "fetched"}
|
|||
|
|
if extracted.get("ok") else None),
|
|||
|
|
"source_quotes": extracted.get("source_quotes"),
|
|||
|
|
"confidence": extracted.get("confidence"),
|
|||
|
|
"notes": extracted.get("notes"),
|
|||
|
|
"raw_llm": extracted.get("raw"),
|
|||
|
|
}
|
|||
|
|
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|||
|
|
out_path = _CACHE_DIR / f"{journal_id}.json"
|
|||
|
|
out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", "utf-8")
|
|||
|
|
|
|||
|
|
print(f"[save] {out_path.relative_to(_ROOT)}")
|
|||
|
|
print("===== 完整结果(人工核验)=====")
|
|||
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|||
|
|
return 0 if result["ok"] else 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
sys.exit(main())
|