• 获取PDF中的布局信息——如何获取段落


      PDF解析是极其复杂的问题。不可能靠一个工具解决全部问题,尤其是五花八门,格式不统一的PDF文件。除非有钞能力。如果没有那就看看可以分为哪些问题。

      提取文本内容,提取表格内容,提取图片。我认为这些应该是分开做的事情。python有一些组件,是有专长的。

      问题分解以后,最重要的一个事情是,版面分析。怎么确定边界,就是哪一块是什么内容?是正文,还是表格,还是图片?

      文本、图片及形状涵盖了常见的PDF元素,本文介绍利用PyMuPDF提取这些页面元素,及其基本数据结构。本文会提供可运行的代码!

    一、技术选型 PyMuPDF

    PyMuPDFTextpage对象提供的extractDICT()extractRAWDICT()用以获取页面中的所有文本和图片(内容、位置、属性),基本数据结构如下:

    看到这里,有分类,有位置信息。

    二、代码演示

    2.1 安装

    pip install PyMuPDF

    2.2 demo代码 

    1. import fitz # PyMuPDF
    2. def extract_text_blocks(pdf_path):
    3. # 打开 PDF 文件
    4. pdf_document = fitz.open(pdf_path)
    5. # 存储文本块和行块信息
    6. text_blocks = []
    7. line_blocks = []
    8. # 遍历 PDF 中的每一页
    9. for page_number in range(len(pdf_document)):
    10. page = pdf_document.load_page(page_number)
    11. # 获取文本块和行块信息
    12. blocks = page.get_text("dict")["blocks"]
    13. for b in blocks:
    14. for l in b["lines"]:
    15. line_blocks.append({
    16. "line": l["spans"],
    17. "bbox": l["bbox"],
    18. "height": l["bbox"][3] - l["bbox"][1] # 计算行块的高度
    19. })
    20. text_blocks.append({
    21. "block": b["lines"],
    22. "bbox": b["bbox"]
    23. })
    24. # 关闭 PDF 文件
    25. pdf_document.close()
    26. return text_blocks, line_blocks
    27. # 示例用法
    28. pdf_path = "D:\\angus\\py\\困难pdf节选西藏奇正2022.pdf"
    29. text_blocks, line_blocks = extract_text_blocks(pdf_path)
    30. # 打印提取的文本块信息
    31. for index, block in enumerate(text_blocks):
    32. print(f"Text Block {index + 1}:")
    33. for line_index, line in enumerate(block["block"]):
    34. print(f" Line {line_index + 1}: '{line['spans']}' at position {block['bbox']}")
    35. # 打印提取的行块信息
    36. for index, line in enumerate(line_blocks):
    37. print(f"Line {index + 1}: '{line['line']}' at position {line['bbox']}, height={line['height']}")

    三、效果展示

    3.1 原文PDF内容

     3.2 解析后得到的结果

     3.3 分析原文和结果

    对比输出的结果和原文。我们可以发现,我们拿到了行的数据,也拿到了段落的数据。上述的代码中已经给我们分好了块!这样解可以区分段落了。

    3.4 获取更多信息,包括位置

    来看一个文本块:

    1. size: 文本的大小。
    2. flags: 文本的标志。
    3. font: 字体名称。
    4. color: 字体颜色。
    5. ascender: 文本的上升高度。
    6. descender: 文本的下降高度。
    7. text: 文本内容。
    8. origin: 文本的起始位置坐标。
    9. bbox: 文本的边界框坐标,即左下角和右上角的坐标。

    通过这些信息,我们可以获取到每个文本块的具体内容、大小、位置和格式等信息。这些信息对于分析和处理 PDF 文件中的文本内容非常有用。例如,你可以根据文本的大小、位置和格式来识别标题、正文和其他内容,并进行相应的处理和分析。当然,就以这个文档为例,我们可以看到的是,因为文档本身字体大小都一样,所以很难根据字体和大小获取到标题。

    四、错误问题

     但是也发现了问题

    4.1 段落有被分开了

    原文

    错误的问题如下

    4.2 将表格错当成了文本内容

    原文表格内容如下

     

    解析得到的内容如下

    表格的一行为一个块内容,

    这里调试了一版,可以去掉表格。

    逻辑是:判断相邻的block,表格的特征是,当个block内的 lines的 bbox的第四位是相同的。且相邻的block的lines一定是相同的,且lines不为空。逻辑本身没有问题,就怕PDF有问题,识别出来的表格的同一行的bbox中的第四位不一样,这样会错误判断!

    1. import fitz # PyMuPDF
    2. def is_table_block(b1, b2):
    3. # 检查连续相邻的文本块是否具有相同的行数,并且其 bbox 的高度也相同
    4. if len(b1["lines"]) == len(b2["lines"]) and b1["bbox"][3] - b1["bbox"][1] == b2["bbox"][3] - b2["bbox"][1]:
    5. return True
    6. return False
    7. def extract_text_blocks(pdf_path):
    8. # 打开 PDF 文件
    9. pdf_document = fitz.open(pdf_path)
    10. # 存储文本块信息
    11. text_blocks = []
    12. line_blocks = []
    13. # 遍历 PDF 中的每一页
    14. for page_number in range(len(pdf_document)):
    15. page = pdf_document.load_page(page_number)
    16. # 获取文本块和行块信息
    17. blocks = page.get_text("dict")["blocks"]
    18. for i in range(len(blocks)):
    19. if i < len(blocks) - 1 and is_table_block(blocks[i], blocks[i+1]): # 如果是表格,则跳过
    20. continue
    21. for l in blocks[i]["lines"]:
    22. line_blocks.append({
    23. "line": l["spans"],
    24. "bbox": l["bbox"],
    25. "height": l["bbox"][3] - l["bbox"][1] # 计算行块的高度
    26. })
    27. text_blocks.append({
    28. "block": blocks[i]["lines"],
    29. "bbox": blocks[i]["bbox"]
    30. })
    31. # 关闭 PDF 文件
    32. pdf_document.close()
    33. return text_blocks, line_blocks
    34. # 示例用法
    35. pdf_path = "D:\\angus\\py\\困难pdf节选西藏奇正2022.pdf"
    36. text_blocks, line_blocks = extract_text_blocks(pdf_path)
    37. # 打印提取的文本块信息
    38. # 用于检查两个文本块中的行是否相同
    39. def check_lines_same(block1, block2):
    40. num_lines_block1 = len(block1["block"])
    41. num_lines_block2 = len(block2["block"])
    42. return num_lines_block1 == num_lines_block2
    43. for index, block in enumerate(text_blocks):
    44. # 获取当前文本块中行的个数
    45. num_lines = len(block["block"])
    46. # 如果当前文本块是表格,则继续检查下一个文本块是否是表格
    47. if num_lines > 1 and index < len(text_blocks) - 1: # 需要多于一行,并且不是最后一个文本块
    48. next_block = text_blocks[index + 1]
    49. if check_lines_same(block, next_block):
    50. # 如果下一个文本块也是表格,则跳过,不进行打印输出
    51. continue
    52. # 如果当前文本块不是表格,则打印输出
    53. print(f"Text Block {index + 1}:")
    54. for line_index, line in enumerate(block["block"]):
    55. print(f" Line {line_index + 1}: '{line['spans']}' at position {block['bbox']}")
    56. # 打印提取的行块信息
    57. # for index, line in enumerate(line_blocks):
    58. # print(f"Line {index + 1}: '{line['line']}' at position {line['bbox']}, height={line['height']}")

    4.3 解析丢失整行数据

    测试了另外一个法律法规文件。

    发现文件丢失了。原文件内容如下:

    解析后的:

    还没找到bug的原因。 

    五、升级版

    解决了丢行的问题,因为代码bug,在判断表格的时候有问题。

    解决了段落被分开的问题。解决思路是,判断两个段落之间,应该有空白分隔。如果两个块之间没有空白分隔,则为同一个段。

    并将内容输出为json格式

    1. import fitz # PyMuPDF
    2. import json
    3. def is_table_block(b1, b2):
    4. # 检查连续相邻的文本块是否具有相同的行数,并且其 bbox 的高度也相同
    5. if len(b1["lines"]) == len(b2["lines"]) and b1["bbox"][3] - b1["bbox"][1] == b2["bbox"][3] - b2["bbox"][1]:
    6. return True
    7. return False
    8. def extract_text_blocks(pdf_path):
    9. # 打开 PDF 文件
    10. pdf_document = fitz.open(pdf_path)
    11. # 存储文本块信息
    12. text_blocks = []
    13. line_blocks = []
    14. # 遍历 PDF 中的每一页
    15. for page_number in range(len(pdf_document)):
    16. page = pdf_document.load_page(page_number)
    17. # 获取文本块和行块信息
    18. blocks = page.get_text("dict")["blocks"]
    19. # 对当前页面内的文本块按照坐标进行排序
    20. blocks.sort(key=lambda x: (x['bbox'][3], x['bbox'][0]))
    21. for i in range(len(blocks)):
    22. for l in blocks[i]["lines"]:
    23. line_blocks.append({
    24. "line": l["spans"],
    25. "bbox": l["bbox"],
    26. "height": l["bbox"][3] - l["bbox"][1], # 计算行块的高度
    27. "page_number": page_number + 1 # 记录页码信息
    28. })
    29. text_blocks.append({
    30. "block": blocks[i]["lines"],
    31. "bbox": blocks[i]["bbox"],
    32. "page_number": page_number + 1 # 记录页码信息
    33. })
    34. # 关闭 PDF 文件
    35. pdf_document.close()
    36. return text_blocks, line_blocks
    37. def is_same_paragraph(line1, line2):
    38. # 判断相邻行是否属于同一个段落的逻辑
    39. # 这里提供一个简单的示例,你可以根据实际情况调整和扩展
    40. # 判断两行之间的垂直间距是否小于某个阈值
    41. vertical_threshold = 5 # 垂直间距阈值,根据实际情况调整
    42. if abs(line1['bbox'][3] - line2['bbox'][1]) < vertical_threshold:
    43. return True
    44. return False
    45. # 示例用法
    46. pdf_path = "D:\\angus\\py\\困难pdf节选西藏奇正2022.pdf"
    47. text_blocks, line_blocks = extract_text_blocks(pdf_path)
    48. # 用于检查两个文本块中的行是否相同
    49. def check_lines_same(block1, block2):
    50. num_lines_block1 = len(block1["block"])
    51. num_lines_block2 = len(block2["block"])
    52. return num_lines_block1 == num_lines_block2
    53. # 收集打印的 JSON
    54. printed_json_list = []
    55. for index, block in enumerate(text_blocks):
    56. # 获取当前文本块中行的个数
    57. num_lines = len(block["block"])
    58. # 如果当前文本块是表格,则继续检查下一个文本块是否是表格
    59. if num_lines > 1 and index < len(text_blocks) - 1: # 需要多于一行,并且不是最后一个文本块
    60. next_block = text_blocks[index + 1]
    61. if check_lines_same(block, next_block):
    62. # 如果下一个文本块也是表格,则跳过,不进行打印输出
    63. continue
    64. # 如果当前文本块不是表格,则添加到打印的 JSON 列表中
    65. block_info = {
    66. "block_index": index + 1,
    67. "page_number": block['page_number'],
    68. "lines": [line['spans'] for line in block['block']],
    69. "bbox": block['bbox']
    70. }
    71. print(block_info)
    72. printed_json_list.append(block_info)
    73. previous_json = None # 用于记录上一个非空 JSON
    74. for printed_json in printed_json_list:
    75. # 获取 lines 的最后一个对象
    76. last_line_array = printed_json["lines"][-1]
    77. # 获取最后一个对象中的最后一个对象
    78. last_object_in_last_line = last_line_array[-1]
    79. # 获取最后一个对象中的 text 字段的值
    80. text_value = last_object_in_last_line["text"]
    81. # 输出截取到的最后一个text值
    82. #print("text字段的取值为>>>>>>>>>>>>..:", text_value)
    83. if text_value.strip() == "":
    84. # 如果 text_value 为空,则打印当前 JSON
    85. if previous_json is not None:
    86. # 合并当前 JSON 到上一个非空 JSON 上
    87. previous_json["lines"].extend(printed_json["lines"])
    88. previous_json["bbox"] = [min(previous_json["bbox"][0], printed_json["bbox"][0]),
    89. min(previous_json["bbox"][1], printed_json["bbox"][1]),
    90. max(previous_json["bbox"][2], printed_json["bbox"][2]),
    91. max(previous_json["bbox"][3], printed_json["bbox"][3])]
    92. # 更新页码信息
    93. previous_json["page_number"] = printed_json["page_number"]
    94. print(json.dumps(previous_json, ensure_ascii=False))
    95. # 重置json
    96. previous_json = None
    97. else:
    98. print(json.dumps(printed_json, ensure_ascii=False))
    99. else:
    100. # 如果 text_value 不为空,则合并当前 JSON 到上一个非空 JSON 上
    101. if previous_json is not None:
    102. # 合并当前 JSON 到上一个非空 JSON 上
    103. previous_json["lines"].extend(printed_json["lines"])
    104. previous_json["bbox"] = [min(previous_json["bbox"][0], printed_json["bbox"][0]),
    105. min(previous_json["bbox"][1], printed_json["bbox"][1]),
    106. max(previous_json["bbox"][2], printed_json["bbox"][2]),
    107. max(previous_json["bbox"][3], printed_json["bbox"][3])]
    108. # 更新页码信息
    109. previous_json["page_number"] = printed_json["page_number"]
    110. else:
    111. # 如果没有上一个非空 JSON,则将当前 JSON 赋值给上一个非空 JSON
    112. previous_json = printed_json

  • 相关阅读:
    姓名缘分查询易语言代码
    ansible Lineinfile模块
    什么?MySQL 8.0 会同时修改两个ib_logfilesN 文件?
    【数据结构】树和森林(树和森林的存储结构、树森林二叉树的转换、树和森林的遍历
    EB和Varuxn的单字聊天
    Mysql数据库调优
    基于Vue+ElementUI+MySQL+Express的学生管理系统(3)
    jar增量打包
    智能管家“贾维斯”走进现实?AI Agent或成2023科技领域新风向标
    基于 dynamic-datasource 实现 DB 多数据源及事物控制、读写分离、负载均衡解决方案
  • 原文地址:https://blog.csdn.net/star1210644725/article/details/136365870