wiki_tools.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. """维基百科开放 API:多语种条目检索与对照(无需密钥,需遵守使用规范)。"""
  2. from __future__ import annotations
  3. from typing import Any
  4. from urllib.parse import urlencode
  5. import requests
  6. _WIKI_UA = (
  7. "HelloAgentsHistoricalReview/1.0 (educational; https://github.com/datawhalechina/hello-agents)"
  8. )
  9. _SESSION = requests.Session()
  10. _SESSION.headers.update({"User-Agent": _WIKI_UA})
  11. def _get(lang: str, params: dict[str, Any]) -> dict[str, Any]:
  12. host = f"https://{lang}.wikipedia.org/w/api.php"
  13. r = _SESSION.get(host, params=params, timeout=25)
  14. r.raise_for_status()
  15. return r.json()
  16. def wiki_search(params: str) -> str:
  17. """
  18. 在指定语言维基中按关键词搜索条目标题。
  19. 参数格式:`语言代码###关键词`
  20. 示例:`zh###安史之乱`、`en###Fall of Constantinople`
  21. """
  22. raw = (params or "").strip()
  23. if "###" not in raw:
  24. return "错误:格式应为 语言代码###关键词,例如 zh###靖康之变"
  25. lang, _, q = raw.partition("###")
  26. lang, q = lang.strip().lower(), q.strip()
  27. if not lang or not q:
  28. return "错误:语言和关键词均不能为空。"
  29. data = _get(
  30. lang,
  31. {
  32. "action": "opensearch",
  33. "search": q,
  34. "limit": 8,
  35. "namespace": 0,
  36. "format": "json",
  37. },
  38. )
  39. # opensearch: [term, [titles], [desc], [urls]]
  40. if not isinstance(data, list) or len(data) < 2:
  41. return f"[{lang}] 搜索无结果或接口异常。"
  42. titles = data[1] if len(data) > 1 else []
  43. descs = data[2] if len(data) > 2 else []
  44. if not titles:
  45. return f"[{lang}] 未找到与「{q}」匹配的条目,可换 en###同一主题的英文检索词再试。"
  46. lines = [f"[{lang}.wikipedia] 关键词「{q}」候选条目:"]
  47. for i, t in enumerate(titles):
  48. d = descs[i] if i < len(descs) else ""
  49. lines.append(f" {i+1}. {t} — {d[:200]}")
  50. lines.append("\n建议:用 wiki_article 拉取全文摘录,或 wiki_multiview 做中英日等多语种对照。")
  51. return "\n".join(lines)
  52. def wiki_article(params: str) -> str:
  53. """
  54. 获取维基条目纯文本摘录(非导语部分也会尽量多取字符)。
  55. 参数格式:`语言代码###条目名`(条目名需与站内标题一致或接近)
  56. 示例:`zh###岳飞`、`en###Qin Shi Huang`
  57. """
  58. raw = (params or "").strip()
  59. if "###" not in raw:
  60. return "错误:格式应为 语言代码###条目名,例如 zh###王安石"
  61. lang, _, title = raw.partition("###")
  62. lang, title = lang.strip().lower(), title.strip()
  63. if not lang or not title:
  64. return "错误:语言或条目名为空。"
  65. data = _get(
  66. lang,
  67. {
  68. "action": "query",
  69. "titles": title,
  70. "prop": "extracts",
  71. "explaintext": 1,
  72. "exchars": 10000,
  73. "format": "json",
  74. },
  75. )
  76. pages = data.get("query", {}).get("pages", {})
  77. out: list[str] = []
  78. for _pid, page in pages.items():
  79. if int(_pid) < 0 or page.get("missing"):
  80. out.append(f"[{lang}] 未找到条目「{title}」。请先用 wiki_search 查准确标题。")
  81. continue
  82. t = page.get("title", title)
  83. ex = (page.get("extract") or "").strip()
  84. if not ex:
  85. out.append(f"[{lang}]「{t}」无正文摘录(可能是消歧义页)。")
  86. continue
  87. if len(ex) > 11000:
  88. ex = ex[:11000] + "\n... [截断]"
  89. url = f"https://{lang}.wikipedia.org/wiki/{title.replace(' ', '_')}"
  90. out.append(f"=== {lang}.wikipedia / {t} ===\n{url}\n\n{ex}")
  91. return "\n\n".join(out) if out else "未获取到内容。"
  92. def wiki_langlinks(params: str) -> str:
  93. """
  94. 列出某条目在其他语言维基中的对应标题(便于横向对比域外叙述)。
  95. 参数格式:`语言代码###条目名`
  96. """
  97. raw = (params or "").strip()
  98. if "###" not in raw:
  99. return "错误:格式应为 语言代码###条目名"
  100. lang, _, title = raw.partition("###")
  101. lang, title = lang.strip().lower(), title.strip()
  102. data = _get(
  103. lang,
  104. {
  105. "action": "query",
  106. "titles": title,
  107. "prop": "langlinks",
  108. "lllimit": 50,
  109. "format": "json",
  110. },
  111. )
  112. pages = data.get("query", {}).get("pages", {})
  113. if not pages:
  114. return "未查询到页面。"
  115. lines: list[str] = []
  116. for _pid, page in pages.items():
  117. if page.get("missing"):
  118. return f"未找到「{title}」。"
  119. resolved = page.get("title", title)
  120. links = page.get("langlinks") or []
  121. if not links:
  122. return f"「{resolved}」暂无其他语言链接,可换 en/zh 起搜或直接用 search 找外国史籍研究。"
  123. lines.append(f"条目「{resolved}」({lang}.wiki) 的部分语种对应:")
  124. for ll in links[:40]:
  125. lines.append(f" - {ll.get('lang')}: {ll.get('*')}")
  126. return "\n".join(lines)
  127. def _query_page_extract_and_links(
  128. lang: str, title: str
  129. ) -> tuple[str | None, str | None, dict[str, str]]:
  130. """返回 (resolved_title, extract_plain, langlinks map lang_code->foreign_title)。"""
  131. data = _get(
  132. lang,
  133. {
  134. "action": "query",
  135. "titles": title,
  136. "prop": "langlinks|extracts",
  137. "lllang": "en|ja|ko|zh|fr|de",
  138. "lllimit": 30,
  139. "explaintext": 1,
  140. "exchars": 5000,
  141. "format": "json",
  142. },
  143. )
  144. pages = data.get("query", {}).get("pages", {})
  145. for _pid, page in pages.items():
  146. if page.get("missing"):
  147. return None, None, {}
  148. resolved = page.get("title", title)
  149. ex = (page.get("extract") or "").strip()
  150. links = {ll["lang"]: ll["*"] for ll in (page.get("langlinks") or [])}
  151. return resolved, ex or None, links
  152. return None, None, {}
  153. def _looks_cjk(text: str) -> bool:
  154. return any("\u4e00" <= c <= "\u9fff" for c in text)
  155. def wiki_multiview(params: str) -> str:
  156. """
  157. 以关键词起搜:含汉字时优先中文维基;纯拉丁字母等则优先英文维基(避免误匹配)。
  158. 再拉取关联语种(如英/日/中与主站交叉)条目摘录并列。
  159. """
  160. q = (params or "").strip()
  161. if not q:
  162. return "错误:请提供历史事件或人物关键词。"
  163. blocks: list[str] = []
  164. targets: list[tuple[str, str]] = []
  165. seen_titles: set[tuple[str, str]] = set()
  166. def add_block(lang: str, title: str, label: str, excerpt: str) -> None:
  167. key = (lang, title)
  168. if key in seen_titles:
  169. return
  170. seen_titles.add(key)
  171. if len(excerpt) > 5500:
  172. excerpt = excerpt[:5500] + "..."
  173. blocks.append(
  174. f"{label}{title}\n"
  175. f"https://{lang}.wikipedia.org/wiki/{title.replace(' ', '_')}\n\n{excerpt}"
  176. )
  177. primary = "zh" if _looks_cjk(q) else "en"
  178. secondary = "en" if primary == "zh" else "zh"
  179. os_primary = _get(
  180. primary,
  181. {
  182. "action": "opensearch",
  183. "search": q,
  184. "limit": 5,
  185. "namespace": 0,
  186. "format": "json",
  187. },
  188. )
  189. p_title = None
  190. if isinstance(os_primary, list) and len(os_primary) > 1 and os_primary[1]:
  191. p_title = os_primary[1][0]
  192. if p_title:
  193. resolved, ex, links = _query_page_extract_and_links(primary, p_title)
  194. if resolved and ex and "may refer to" not in ex.lower() and "消歧义" not in ex[:80]:
  195. add_block(primary, resolved, "【主站维基】", ex)
  196. order = ["en", "ja", "ko", "zh", "fr", "de"] if primary == "zh" else ["zh", "ja", "ko", "en"]
  197. for code in order:
  198. if code == primary:
  199. continue
  200. if code in links:
  201. targets.append((code, links[code]))
  202. if not blocks:
  203. os_sec = _get(
  204. secondary,
  205. {
  206. "action": "opensearch",
  207. "search": q,
  208. "limit": 5,
  209. "namespace": 0,
  210. "format": "json",
  211. },
  212. )
  213. s_title = None
  214. if isinstance(os_sec, list) and len(os_sec) > 1 and os_sec[1]:
  215. s_title = os_sec[1][0]
  216. if s_title:
  217. resolved, ex, links = _query_page_extract_and_links(secondary, s_title)
  218. if resolved and ex:
  219. add_block(secondary, resolved, "【备用语种维基】", ex)
  220. order = ["zh", "en", "ja", "ko"] if secondary == "en" else ["en", "ja", "ko"]
  221. for code in order:
  222. if code == secondary:
  223. continue
  224. if code in links:
  225. targets.append((code, links[code]))
  226. for lang, tit in targets:
  227. key = (lang, tit)
  228. if key in seen_titles:
  229. continue
  230. snippet = wiki_article(f"{lang}###{tit}")
  231. if snippet.startswith("错误") or "未找到条目" in snippet:
  232. continue
  233. blocks.append(f"\n--- 对照语种 {lang} ---\n{snippet}")
  234. if not blocks:
  235. return (
  236. f"未能为「{q}」自动匹配到维基正文。请用 wiki_search 分别试 zh### 与 en###,"
  237. "或使用 search 检索学术/史料网页后再 fetch_url_text。"
  238. )
  239. header = (
  240. f"多语种维基摘录对照(关键词:{q})。注意:维基为二手综述,非原始档案;"
  241. "不同语种条目由不同社群编写,立场与侧重可能不同。\n"
  242. )
  243. body = "\n\n".join(blocks)
  244. if len(header) + len(body) > 28000:
  245. body = body[: 28000 - len(header)] + "\n... [总长度已截断]"
  246. return header + body