paper_analysis_helpers.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """论文分析辅助函数 —— 方法名提取、缩写识别与文本预处理."""
  2. from __future__ import annotations
  3. import re
  4. from ...core.paper_paths import normalize_library_category_display
  5. from ...utils import parse_llm_json, tokenize_for_keywords, truncate_text
  6. def clip_text(text: str | None, n: int) -> str:
  7. t = (text or "").strip()
  8. if len(t) <= n:
  9. return t
  10. t = t[:n]
  11. while t:
  12. try:
  13. t.encode('utf-8')
  14. break
  15. except UnicodeEncodeError:
  16. t = t[:-1]
  17. return t
  18. def dedupe_tags(tags: list[str], max_tags: int = 12) -> list[str]:
  19. out: list[str] = []
  20. seen: set[str] = set()
  21. for t in tags:
  22. s = str(t).strip()
  23. k = s.lower()
  24. if s and k not in seen:
  25. out.append(s)
  26. seen.add(k)
  27. if len(out) >= max_tags:
  28. break
  29. return out
  30. def clean_library_tag(tag: str) -> str | None:
  31. s = str(tag).strip()[:24]
  32. if not s:
  33. return None
  34. if any(c in s for c in '|\\:*?"<>/'):
  35. return None
  36. return s
  37. def sanitize_major_name(raw: str) -> str | None:
  38. t = str(raw or "").strip()
  39. if not t or len(t) > 16:
  40. return None
  41. t = "".join(ch for ch in t if ch not in '/\\:*?"<>|')
  42. t = t.strip()
  43. if len(t) < 2:
  44. return None
  45. return t[:10]
  46. def nearest_major_in(raw: str, whitelist: tuple[str, ...]) -> str:
  47. s = (raw or "").strip()
  48. if "/" not in s:
  49. s = normalize_library_category_display(s)
  50. if s in whitelist:
  51. return s
  52. for m in whitelist:
  53. if m and (m in s or s in m):
  54. return m
  55. if "未分类" in whitelist:
  56. return "未分类"
  57. return whitelist[0] if whitelist else "未分类"
  58. def parse_taxonomy_majors(raw: str) -> list[str | None]:
  59. d = parse_llm_json(raw)
  60. if not isinstance(d, dict):
  61. return None
  62. arr = d.get("majors")
  63. if not isinstance(arr, list):
  64. return None
  65. out: list[str] = []
  66. seen: set[str] = set()
  67. for x in arr:
  68. c = sanitize_major_name(str(x))
  69. if not c:
  70. continue
  71. k = c.lower()
  72. if k in seen:
  73. continue
  74. seen.add(k)
  75. out.append(c)
  76. if "未分类" not in out:
  77. out.append("未分类")
  78. if len(out) < 4:
  79. return None
  80. return out[:28]
  81. def top_similar_categories(seed: str, categories: list[str], k: int = 18) -> list[str]:
  82. cats = [str(x).strip() for x in categories if str(x).strip()]
  83. if len(cats) <= k:
  84. return cats
  85. tokens = tokenize_for_keywords(seed)
  86. if not tokens:
  87. return cats[:k]
  88. scored: list[tuple[float, str]] = []
  89. for c in cats:
  90. ct = tokenize_for_keywords(c)
  91. inter = len(tokens & ct)
  92. if inter == 0:
  93. continue
  94. union = len(tokens | ct) or 1
  95. scored.append((inter / union, c))
  96. scored.sort(key=lambda x: x[0], reverse=True)
  97. picked = [c for _, c in scored[:k]]
  98. if len(picked) < max(8, k // 2):
  99. for c in cats:
  100. if c not in picked:
  101. picked.append(c)
  102. if len(picked) >= k:
  103. break
  104. return picked[:k]
  105. def prioritize_reader_context(block: str, max_chars: int) -> str:
  106. b = (block or "").strip()
  107. if len(b) <= max_chars:
  108. return b
  109. def grab(label_pat: str, cap: int) -> str:
  110. m = re.search(label_pat, b, flags=re.IGNORECASE | re.DOTALL)
  111. if not m:
  112. return ""
  113. seg = (m.group(1) if m.groups() else m.group(0)).strip()
  114. return truncate_text(seg, cap, suffix="…")
  115. abstract = grab(
  116. r"(?:摘要|Abstract)\s*[::]?\s*([\s\S]{20,}?)(?=\n\s*(?:相关工作|Related\s*work|关键词|Key\s*words|PDF|【)|\Z)",
  117. 1400,
  118. )
  119. related = grab(
  120. r"(?:相关工作|Related\s*work)\s*[::]?\s*([\s\S]{20,}?)(?=\n\s*(?:关键词|Key\s*words|参考文献|PDF|【)|\Z)",
  121. 1200,
  122. )
  123. artifact = grab(
  124. r"(【结构化阅读档案[\s\S]{80,}?)(?=\n【PDF 正文|\Z)",
  125. min(2600, max(1400, max_chars - 900)),
  126. )
  127. ref_blob = grab(
  128. r"【参考文献区 PDF 原文摘录[^\n]*\n([\s\S]{10,}?)(?=\n【结构化阅读档案|\n【PDF 正文|\Z)",
  129. min(10000, max(2400, max_chars - 1200)),
  130. )
  131. budget = max_chars - len(abstract) - len(related) - len(artifact) - len(ref_blob) - 30
  132. if budget < 400:
  133. budget = 400
  134. head = truncate_text(b, budget, suffix="…")
  135. parts = [p for p in (abstract, related, artifact, ref_blob, head) if p]
  136. merged = "\n\n---\n\n".join(parts)
  137. return truncate_text(merged, max_chars, suffix="…")
  138. def clip_reader_history(hist: str, max_chars: int) -> str:
  139. h = (hist or "").strip()
  140. if len(h) <= max_chars:
  141. return h
  142. lines = h.splitlines()
  143. out: list[str] = []
  144. size = 0
  145. for line in reversed(lines):
  146. if size + len(line) + 1 > max_chars:
  147. break
  148. out.append(line)
  149. size += len(line) + 1
  150. return "\n".join(reversed(out)) if out else truncate_text(h, max_chars, suffix="…")