优化简历提取相关

This commit is contained in:
zk
2026-06-24 16:16:37 +08:00
parent 556302816b
commit 62a0801a22
5 changed files with 253 additions and 106 deletions
+77 -28
View File
@@ -1,66 +1,115 @@
"""文件解析工具
将上传的简历文件(PDF / Word / TXT)转换为纯文本字符串。
PDF 使用 PyMuPDF (fitz) 按文本块提取,保持段落边界和阅读顺序
将上传的简历文件(PDF / Word / TXT)转换为「文本单元数组」或纯文本字符串。
- PDF使用 LiteParse(JSON 模式)按文本块的阅读顺序提取,每个文本块作为一个单元
关闭 OCR —— 文本型简历无需 OCR,且 OCR 默认会联网下载字库导致严重阻塞。
- Word:按段落(paragraph)切分,表格行用 \t 拼成一个单元。
- TXT:按换行切分。
单元数组用于 AI 提取时按行号定位 description/summary,避免 AI 照抄长文本。
另提供 parse_to_markdown:基于 LiteParse 的 Markdown 结构化输出(保留标题/列表/加粗等
语义层级),可用于需要整篇结构化文本的 AI 场景。
"""
import io
from collections import Counter
import fitz
from docx import Document
from liteparse import LiteParse
from app.core.logger import log
def parse_pdf(content: bytes) -> str:
"""解析 PDF 文件,按文本块提取,过滤图片块,保持阅读顺序"""
text_parts: list[str] = []
with fitz.open(stream=content, filetype="pdf") as doc:
for page in doc:
for b in page.get_text("blocks", sort=True):
if b[6] == 0 and b[4].strip(): # type 0=文本块, 1=图片块
text_parts.append(b[4].strip())
return "\n".join(text_parts)
# LiteParse 解析器复用实例(关闭 OCR、静默日志)。线程安全由调用方的 to_thread 串行保证。
_PDF_SEGMENT_PARSER = LiteParse(output_format="json", ocr_enabled=False, quiet=True)
_PDF_MARKDOWN_PARSER = LiteParse(output_format="markdown", ocr_enabled=False, quiet=True)
def parse_docx(content: bytes) -> str:
"""解析 Word (.docx) 文件,提取段落和表格文本"""
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_SEGMENT_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 文件,如果是旧版 .doc 格式,请另存为 .docx 后重试")
text_parts: list[str] = []
segments: list[str] = []
for para in doc.paragraphs:
text = para.text.strip()
if text:
text_parts.append(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():
text_parts.append(row_text)
return "\n".join(text_parts)
segments.append(row_text)
return segments
def parse_txt(content: bytes) -> str:
"""解析 TXT 文件,自动检测编码"""
def _txt_segments(content: bytes) -> list[str]:
"""解析 TXT,自动检测编码,按换行切分为单元"""
text = None
for encoding in ("utf-8", "gbk", "gb2312", "latin-1"):
try:
return content.decode(encoding)
text = content.decode(encoding)
break
except (UnicodeDecodeError, LookupError):
continue
return content.decode("utf-8", errors="replace")
if text is None:
text = content.decode("utf-8", errors="replace")
return [line.strip() for line in text.splitlines() if line.strip()]
def parse_to_text(filename: str, content: bytes) -> str:
"""根据文件名后缀自动选择解析方法,返回文本"""
def parse_to_segments(filename: str, content: bytes) -> list[str]:
"""根据文件名后缀自动选择解析方法,返回文本单元数组」"""
suffix = filename[filename.rfind("."):].lower() if "." in filename else ""
log.info(f"解析文件: {filename},类型: {suffix}")
if suffix == ".pdf":
return parse_pdf(content)
segments = _pdf_segments(content)
elif suffix in (".docx", ".doc"):
return parse_docx(content)
segments = _docx_segments(content)
elif suffix == ".txt":
return parse_txt(content)
segments = _txt_segments(content)
else:
raise ValueError(f"不支持的文件类型: {suffix},支持: .pdf, .docx, .doc, .txt")
return _drop_repeated(segments)
def parse_to_text(filename: str, content: bytes) -> str:
"""根据文件名后缀自动选择解析方法,返回纯文本(单元用换行拼接)"""
return "\n".join(parse_to_segments(filename, content))
def parse_to_markdown(filename: str, content: bytes) -> str:
"""将简历解析为 Markdown 结构化文本(保留标题/列表/加粗等语义层级)。
PDF 使用 LiteParse 的 Markdown 输出;Word/TXT 无版式信息,退化为换行拼接的纯文本。
适用于需要整篇结构化文本喂给 LLM 的场景。
"""
suffix = filename[filename.rfind("."):].lower() if "." in filename else ""
log.info(f"解析文件(markdown): {filename},类型: {suffix}")
if suffix == ".pdf":
return _PDF_MARKDOWN_PARSER.parse(content).text
if suffix in (".docx", ".doc"):
return "\n".join(_drop_repeated(_docx_segments(content)))
if suffix == ".txt":
return "\n".join(_drop_repeated(_txt_segments(content)))
raise ValueError(f"不支持的文件类型: {suffix},支持: .pdf, .docx, .doc, .txt")