fix: PubMed搜索合规 — 34项修复 + query_expansion UnboundLocalError + HomeView precision_mode残留
Batch1 — 解析器 (pubmed_query_parser.py) - P0-1: 未知字段标签降级为 WORD token 而非 ParseError - P2-1: 增加未消费 token 检查 - P2-2: PRISMA 字段标签正则 [\w:+] → [\w:]+ - P2-3: Unicode NFKC 规格化输入 - P2-4: re.ASCII 防止 Unicode 数字匹配 - P2-9: 移除重复 dataclass 字段 Batch2 — 搜索引擎 (search_engine.py) - P1-1: 批量 PMID 查询替代 N+1 循环 - P1-2: isdigit() → isdecimal() - P1-5: 移除 precision_mode 参数 - P2-5: 移除死代码 - P2-6: 统一 _CHINESE_RE 正则 Batch3 — ATM 引擎 (query_expansion.py) - P0-4: name_zh ILIKE 中文回退 + _find_mesh_tags 中文降级 - P1-3: name_en ILIKE 加 LIMIT 100 Batch4 — API 层 (features.py) - P1-5: 移除 precision_mode 请求字段 - P1-11: 增加 logging - P2-8: NLM_SUBSET_LABELS f-string 安全注释 - P2-12: split 校验器近似性注释 Batch5 — SearchView.vue - P0-5a/b/c: 日期修复(UTC 方法、互斥逻辑、restoreFromQuery 合并) - P2-10: 筛选模态关闭时重搜 - P3-1: 搜索框 aria-label Batch6 — HomeView.vue - P1-10: URL date → date_from/date_to - P2-11: clearSearch 清空 feedItems 并重加载 Batch7 — LiteratureCard.vue - P1-6: 字段标签正则 [\w-]+ → [\w:-]+ - P1-7: terms 切片限制 20 项防 ReDoS 后修复: - query_expansion.py _find_partial_mesh_tags UnboundLocalError(单非中文词未初始化 tag_ids) - HomeView.vue handleAdvancedSearch precision_mode 残留引用
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum, auto
|
||||
|
||||
@@ -127,7 +128,7 @@ _TOKEN_PATTERNS: list[tuple[TokenType, str]] = [
|
||||
|
||||
_TOKEN_RE = re.compile(
|
||||
'|'.join(f'(?P<{t.name}>{p})' for t, p in _TOKEN_PATTERNS),
|
||||
re.IGNORECASE,
|
||||
re.IGNORECASE | re.ASCII,
|
||||
)
|
||||
|
||||
|
||||
@@ -140,7 +141,11 @@ def tokenise(query: str) -> list[Token]:
|
||||
ttype = TokenType[name]
|
||||
if ttype == TokenType.UNKNOWN_FIELD:
|
||||
fname = value.strip('[]').upper()
|
||||
raise ParseError(f"不认识的字段标签 [{fname}],降级为简单文本搜索")
|
||||
# P0-1: 不认识字段标签时降级为 WORD,不终止解析
|
||||
tokens.append(Token(TokenType.WORD, value.strip('[]')))
|
||||
if len(tokens) > MAX_TERMS:
|
||||
raise ParseError(f"查询词过多(超过 {MAX_TERMS} 个),降级为简单文本搜索")
|
||||
continue
|
||||
tokens.append(Token(ttype, value))
|
||||
if len(tokens) > MAX_TERMS:
|
||||
raise ParseError(f"查询词过多(超过 {MAX_TERMS} 个),降级为简单文本搜索")
|
||||
@@ -169,11 +174,6 @@ class ParsedPubmedQuery:
|
||||
tiab_terms: list[Term] = field(default_factory=list) # [TIAB]
|
||||
author_terms: list[Term] = field(default_factory=list) # [AU]
|
||||
journal_terms: list[Term] = field(default_factory=list) # [TA]
|
||||
mesh_terms: list[str] = field(default_factory=list) # [MH]
|
||||
majr_terms: list[str] = field(default_factory=list) # [MAJR]
|
||||
pub_types: list[str] = field(default_factory=list) # [PT]
|
||||
doi_terms: list[str] = field(default_factory=list) # [DOI]
|
||||
pmid_terms: list[int] = field(default_factory=list) # [PMID]
|
||||
affiliation_terms: list[Term] = field(default_factory=list) # [AD]
|
||||
language_terms: list[Term] = field(default_factory=list) # [LA]
|
||||
volume_terms: list[Term] = field(default_factory=list) # [VI]
|
||||
@@ -275,10 +275,8 @@ class PubmedQueryParser:
|
||||
"""入口:解析完整的查询字符串。"""
|
||||
result = ParsedPubmedQuery()
|
||||
self._depth = 0 # 括号嵌套深度计数器
|
||||
try:
|
||||
terms = self._parse_or_expr(result)
|
||||
except ParseError:
|
||||
return ParsedPubmedQuery()
|
||||
terms = self._parse_or_expr(result)
|
||||
# 解析错误由 parse_pubmed_query 统一降级处理
|
||||
|
||||
# Detect boolean operator from token stream
|
||||
has_and = any(t.type == TokenType.AND for t in self.tokens)
|
||||
@@ -296,6 +294,13 @@ class PubmedQueryParser:
|
||||
for t in _ungrouped:
|
||||
self._dispatch_term(result, t)
|
||||
|
||||
# P2-1: Handle unconsumed tokens (e.g., orphan text after RPAREN)
|
||||
if self.pos < len(self.tokens) - 1:
|
||||
for t in self.tokens[self.pos:-1]: # exclude EOF token
|
||||
if t.type in (TokenType.WORD, TokenType.QUOTED, TokenType.NUMBER):
|
||||
text = t.value.strip('"') if t.type == TokenType.QUOTED else t.value
|
||||
result.plain_terms.append(Term(text=text, exact=(t.type == TokenType.QUOTED)))
|
||||
|
||||
return result
|
||||
|
||||
def _dispatch_term(self, result: ParsedPubmedQuery, term: Term) -> None:
|
||||
@@ -574,6 +579,8 @@ def parse_pubmed_query(query: str) -> ParsedPubmedQuery:
|
||||
return ParsedPubmedQuery()
|
||||
|
||||
try:
|
||||
# P2-3: Unicode normalization — strip zero-width chars, normalize fullwidth digits
|
||||
query = unicodedata.normalize('NFKC', query)
|
||||
# 将 YYYY/MM/DD 格式的日期分隔符统一为 YYYY-MM-DD,使 tokeniser 正确识别为 DATE
|
||||
import re as _re
|
||||
query = _re.sub(r'(\d{4})/(\d{2})/(\d{2})', r'\1-\2-\3', query)
|
||||
@@ -581,7 +588,11 @@ def parse_pubmed_query(query: str) -> ParsedPubmedQuery:
|
||||
parser = PubmedQueryParser(tokens)
|
||||
return parser.parse()
|
||||
except (ParseError, IndexError, ValueError):
|
||||
return ParsedPubmedQuery()
|
||||
# P0-1: 降级时返回原始查询作为 plain_terms,不丢失用户输入
|
||||
degraded = ParsedPubmedQuery()
|
||||
for t in query.strip().split():
|
||||
degraded.plain_terms.append(Term(text=t))
|
||||
return degraded
|
||||
|
||||
|
||||
def extract_pubmed_query_for_prisma(query: str) -> tuple[str, list[str]]:
|
||||
@@ -598,7 +609,7 @@ def extract_pubmed_query_for_prisma(query: str) -> tuple[str, list[str]]:
|
||||
|
||||
# 标准化:统一字段大写
|
||||
normalized = re.sub(
|
||||
r'\[(\w+)\]',
|
||||
r'\[([\w:]+)\]',
|
||||
lambda m: f'[{m.group(1).upper()}]',
|
||||
query,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user