"""文件解析工具 将上传的简历文件解析为「文本单元数组」,供 AI 按行号定位 description/summary,避免照抄长文本。 - PDF:使用 LiteParse(JSON 模式)按文本块的阅读顺序提取,每个文本块作为一个单元。 关闭 OCR —— 文本型简历无需 OCR,且 OCR 默认会联网下载字库导致严重阻塞。 - Word(.docx):按段落(paragraph)切分,表格行用 \t 拼成一个单元。 - TXT / Markdown(.md):自动检测编码,按换行切分。 """ import io from collections import Counter from docx import Document from liteparse import LiteParse from app.core.logger import log # LiteParse 解析器复用实例(关闭 OCR、静默日志)。线程安全由调用方的 to_thread 串行保证。 _PDF_PARSER = LiteParse(output_format="json", ocr_enabled=False, quiet=True) def _drop_repeated(segments: list[str], threshold: int = 3) -> list[str]: """过滤重复出现的行(水印/页眉页脚特征):完全相同且出现次数 >= threshold 的行整体丢弃""" counts = Counter(segments) return [s for s in segments if counts[s] < threshold] def _pdf_segments(content: bytes) -> list[str]: """解析 PDF:使用 LiteParse 按阅读顺序遍历每页文本块,每块作为一个独立单元,过滤空行。 不在预处理阶段合并行——是否合并/分段/排除噪声全部交给 AI 的行号区间决定。 """ result = _PDF_PARSER.parse(content) segments: list[str] = [] for page in result.pages: for item in page.text_items: text = item.text.strip() if text: segments.append(text) return segments def _docx_segments(content: bytes) -> list[str]: """解析 Word (.docx),按段落切分为单元,表格行用 \t 拼为一个单元""" try: doc = Document(io.BytesIO(content)) except Exception: raise ValueError("无法解析该 Word 文件,请确认为有效的 .docx 格式") segments: list[str] = [] for para in doc.paragraphs: text = para.text.strip() if text: segments.append(text) for table in doc.tables: for row in table.rows: row_text = "\t".join(cell.text.strip() for cell in row.cells) if row_text.strip(): segments.append(row_text) return segments def _txt_segments(content: bytes) -> list[str]: """解析纯文本 (.txt / .md),自动检测编码,按换行切分为单元""" text = None for encoding in ("utf-8", "gbk", "gb2312", "latin-1"): try: text = content.decode(encoding) break except (UnicodeDecodeError, LookupError): continue if text is None: text = content.decode("utf-8", errors="replace") return [line.strip() for line in text.splitlines() if line.strip()] # 后缀 → 解析函数。新增格式只需在此登记一行。 _PARSERS = { ".pdf": _pdf_segments, ".docx": _docx_segments, ".txt": _txt_segments, ".md": _txt_segments, } def parse_to_segments(filename: str, content: bytes) -> list[str]: """根据文件名后缀选择解析器,返回去重后的「文本单元数组」""" suffix = filename[filename.rfind("."):].lower() if "." in filename else "" log.info(f"解析文件: {filename},类型: {suffix}") parser = _PARSERS.get(suffix) if parser is None: raise ValueError(f"不支持的文件类型: {suffix},支持: {', '.join(_PARSERS)}") return _drop_repeated(parser(content))