document_worker.py 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. """隔离 PDFium 原生解析;输出有限页数及分辨率的文本和 PNG。"""
  2. import argparse
  3. import json
  4. from pathlib import Path
  5. import re
  6. def text_is_readable(text):
  7. """拒绝空层、零散水印及大量损坏字符;完整文字页可绕过图像模型。"""
  8. compact = re.sub(r"\s+", "", text)
  9. if len(compact) < 100:
  10. return False
  11. meaningful = sum(c.isalpha() or c.isdigit() for c in compact)
  12. broken = sum(c in "\ufffd\u25a1\u25a0" or ord(c) < 32 for c in compact)
  13. return meaningful / len(compact) >= .5 and broken / len(compact) < .03
  14. def main():
  15. import pypdfium2 as pdfium
  16. parser = argparse.ArgumentParser()
  17. parser.add_argument("source", type=Path)
  18. parser.add_argument("output", type=Path)
  19. parser.add_argument("--max-pages", type=int, default=32)
  20. args = parser.parse_args()
  21. args.output.mkdir(parents=True, exist_ok=True)
  22. with pdfium.PdfDocument(args.source) as doc:
  23. if len(doc) > args.max_pages:
  24. raise ValueError("PDF 页数超过识别上限,请拆分报告")
  25. rows = []
  26. for index in range(len(doc)):
  27. page = doc[index]
  28. try:
  29. textpage = page.get_textpage()
  30. try:
  31. content = textpage.get_text_range()
  32. finally:
  33. textpage.close()
  34. coverage = 0.0
  35. for obj in page.get_objects():
  36. if obj.type == pdfium.raw.FPDF_PAGEOBJ_IMAGE:
  37. x0, y0, x1, y1 = obj.get_bounds()
  38. width, height = page.get_size()
  39. coverage += abs((x1-x0)*(y1-y0))/(width*height)
  40. readable = text_is_readable(content)
  41. # 大图仅带少量文字、以及曲线/图页仍走视觉,避免忽略正文图像。
  42. visual = not readable or coverage >= .5 and len(content.strip()) < 400 or bool(re.search("性能曲线|特性曲线", content))
  43. image = args.output / f"page-{index+1}.png"
  44. if visual:
  45. bitmap = page.render(scale=min(3, 2200/max(page.get_size())))
  46. try:
  47. bitmap.to_pil().save(image)
  48. finally:
  49. bitmap.close()
  50. rows.append({"page": index+1, "text": content,
  51. "image": image.name if visual else None,
  52. "route": "vision" if visual else "text",
  53. "text_chars": len(content), "image_coverage": round(min(coverage,1),4)})
  54. finally:
  55. page.close()
  56. (args.output / "pages.json").write_text(json.dumps(rows, ensure_ascii=False), encoding="utf-8")
  57. if __name__ == "__main__":
  58. main()