Files

123 lines
5.0 KiB
Python
Raw Permalink Normal View History

#!/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())