pdf_reader.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import sys
  2. import os
  3. # 添加当前目录到Python路径
  4. sys.path.append(os.path.dirname(__file__))
  5. from rag_base import RAGBase
  6. from typing import List, Any, Iterator
  7. import pdfplumber
  8. import math
  9. class PdfReader(RAGBase):
  10. """
  11. pdf读取器,读取处理pdf,整理为markdown格式的文本
  12. """
  13. def __init__(self):
  14. self.page_content = [] # 每一页的文本内容
  15. def reset(self):
  16. self.__init__()
  17. def read(self, read_path: str, password:str=None):
  18. """
  19. 读取pdf
  20. Args:
  21. read_path: pdf文件路径
  22. password: 密码
  23. Returns:
  24. """
  25. # 文件存在性检查
  26. self.is_path_exist(read_path)
  27. # 位置元组:x0, top, x1, bottom
  28. # x0:表示该元素矩形区域左边缘到页面最左侧的横向距离
  29. # top:表示该元素矩形区域上边缘到页面最顶部的纵向距离
  30. # x1:表示该元素矩形区域右边缘到页面最左侧的横向距离
  31. # bottom:表示该元素矩形区域下边缘到页面最顶部的纵向距离
  32. with pdfplumber.open(read_path, password=password) as pdf:
  33. # 如果pdf页数太多,应该选择分页处理,
  34. for page_num, page in enumerate(pdf.pages):
  35. # 处理一页pdf内容
  36. one_page_text = self.process(page) # 一页文本
  37. if one_page_text:
  38. self.page_content.append(one_page_text)
  39. def process(self, page)->List[dict]:
  40. """
  41. 处理每页pdf,输出文本
  42. Args:
  43. page:
  44. Returns:
  45. """
  46. # 提取页面中的所有文本行,并获取其位置信息,每行文本是一个list元素
  47. lines = page.extract_text_lines(keep_blank_chars=True,
  48. extra_attrs=["fontname", "size"])
  49. # 先剔除页码行或数字孤行
  50. lines = [_ for _ in lines if not self.is_page_number(_['text'])]
  51. # 处理空白页
  52. if len(lines) == 0:
  53. return ''
  54. # 按照top值对行进行排序(升序)
  55. self.quick_sort(lines, 0, len(lines) - 1)
  56. # 提取页面中的表格,并获取每个表格的位置信息
  57. tables = page.find_tables() # 使用 find_tables() 检测表格
  58. table_data = []
  59. if tables:
  60. for table in tables:
  61. # 处理表格文本
  62. table_text = self.process_table(table)
  63. # 获取表格的边界框 (x0, top, x1, bottom)
  64. bbox = table.bbox
  65. table_data.append({
  66. 'text': table_text,
  67. 'x0': bbox[0], # 表格的位置信息
  68. 'top': bbox[1], # 表格的位置信息
  69. 'x1': bbox[2], # 表格的位置信息
  70. 'bottom': bbox[3], # 表格的位置信息
  71. })
  72. # 将表格内容插入行文本,首先我们需要先剔除行文本中的重复表格
  73. # 规则:如果行文本中的top介于表格位置的top和bottom之间,应该被剔除
  74. new_lines = []
  75. table_count = len(table_data)
  76. for line in lines:
  77. flag = True
  78. for j in range(table_count):
  79. # 剔除冗余表
  80. if (line.get('top') >= table_data[j].get('top')) and (line.get('top') <= table_data[j].get('bottom')):
  81. flag = False
  82. break
  83. if flag:
  84. new_lines.append(line)
  85. # 将表格内容插入新建立的行文本
  86. new_lines += table_data
  87. self.quick_sort(new_lines, 0, len(new_lines) - 1)
  88. # 页面文本融合
  89. #page_text = self.join_(new_lines)
  90. page_text = new_lines
  91. return page_text
  92. @staticmethod
  93. def process_table(table)->str:
  94. """
  95. 处理表格,输出表格文本
  96. Args:
  97. table: pdfplumber.table.Table
  98. Returns: 表格文本
  99. """
  100. # 提取表格数据
  101. extracted_table = table.extract()
  102. # 处理非str值
  103. for i in range(len(extracted_table)):
  104. for j in range(len(extracted_table[i])):
  105. extracted_table[i][j] = str(extracted_table[i][j]).strip()
  106. extracted_table[i][j] = extracted_table[i][j].replace('\n','')
  107. extracted_table[i][j] = extracted_table[i][j].replace('None','')
  108. # 将上述二维list表格处理为纯markdown文本
  109. table_text = ['|' + '|'.join(extracted_table[0]) + '|', '|---' * len(extracted_table[0]) + '|']
  110. for table_line in extracted_table[1:]:
  111. for i in range(len(table_line)):
  112. table_line[i] = table_line[i].replace('\n', ' ').strip()
  113. table_text.append('|' + ' | '.join(table_line) + '|')
  114. table_text = '\n'.join(table_text) # 融合为最终大文本
  115. return table_text
  116. def write(self, write_path: str, mode='w', encoding='utf-8', *args, **kwargs):
  117. # 逐页保存
  118. with open(write_path, mode, encoding=encoding) as f:
  119. for page in self.page_content:
  120. f.write(self.join_(page)) # 融合在此处,更省内存
  121. self.reset()
  122. def join_(self, page_list)->str:
  123. """
  124. 将每一页pdf提取的文字和表格进行融合,输出一个文本text
  125. Args:
  126. page_list: pdf提取的文本和表格
  127. Returns: 拼接后的text
  128. """
  129. text = ''
  130. text_tem_list = page_list.copy()
  131. # 启发式分段,根据文本位置分析
  132. # 左侧页边距统计
  133. statistic_dict = {}
  134. for i in text_tem_list:
  135. d = str(int(i.get('x0'))) # 当前元素左侧页边距
  136. if d not in statistic_dict.keys():
  137. statistic_dict[d] = 1
  138. else:
  139. statistic_dict[d] += 1
  140. # 处理缩进都不一样的页
  141. if max(statistic_dict.values()) == min(statistic_dict.values()):
  142. for i in text_tem_list:
  143. i['text'] = i['text'] + '\n'
  144. else:
  145. # 处理正文页
  146. max_key = max(statistic_dict, key=statistic_dict.get)
  147. page_left_distance = int(max_key) + 1 # 正文非段首文字页边距
  148. for i in text_tem_list:
  149. if self.is_title(i['text']) : # 正则匹配到标题行
  150. # 为标题行增加回车符
  151. i['text'] = i['text'] + '\n'
  152. elif i['x0'] > page_left_distance:
  153. # 段首增加回车符
  154. i['text'] = '\n' + i['text']
  155. # 融合所有行
  156. text = ''.join([_.get('text') for _ in text_tem_list])
  157. # 空格替换
  158. text = text.replace(' ', '')
  159. # 回车符替换
  160. text = text.replace('\n\n\n', '\n')
  161. text = text.replace('\n\n', '\n')
  162. return text
  163. def text_generator(self, *args: Any, **kwargs: Any) -> Iterator[str]:
  164. """每次迭代返回一页文本"""
  165. # 每100页为1组
  166. const_num = 100
  167. for i in range(math.ceil(len(self.page_content) / const_num)):
  168. # 融合行文本
  169. group_content =[self.join_(page_cont) for page_cont in self.page_content[i*const_num:i*const_num + const_num:1]]
  170. group_content = ''.join(group_content)
  171. yield group_content
  172. @staticmethod
  173. def quick_sort_part(arr: list[dict], low: int, high: int):
  174. """
  175. 快速排序内层函数
  176. Args:
  177. arr: 待排序数组, 结构如同list[dict], 每个dict包括{text, x0 top x1 bottom}, 以top值进行排序
  178. low: 左边界
  179. high: 右边界
  180. Returns:排序后的基准值索引
  181. """
  182. if low >= high:
  183. return None
  184. # 设定基准值
  185. left, right = low, high
  186. pivot = arr[low].get('top')
  187. # 右边放大数,左边放小数
  188. while left < right: # 做一趟排序
  189. # 先从右面开始找比基准值小的数
  190. while left < right and arr[right].get('top') >= pivot:
  191. right -= 1
  192. # 在右面找到了比基准值小的数,执行一次交换
  193. if left < right:
  194. arr[left], arr[right] = arr[right], arr[left]
  195. left += 1
  196. # 在左面开始找比基准值大的数
  197. while left < right and arr[left].get('top') <= pivot:
  198. left += 1
  199. # 在左面找到了大于基准值的数, 执行一次交换
  200. if left < right:
  201. arr[left], arr[right] = arr[right], arr[left]
  202. right -= 1
  203. return left # 返回基准值索引
  204. def quick_sort(self, arr: list[dict], low: int, high: int):
  205. """
  206. 快排序外层函数
  207. Args:
  208. arr: 待排序数组
  209. low: 左侧索引
  210. high: 右侧索引
  211. Returns:
  212. """
  213. if low >= high:
  214. return
  215. # 先排一趟
  216. mid = self.quick_sort_part(arr, low, high)
  217. # 排左边
  218. self.quick_sort(arr, low, mid-1)
  219. # 排右边
  220. self.quick_sort(arr, mid+1, high)
  221. if __name__ == '__main__':
  222. path = r'D:\code\tem_1103\(0119)沭阳县循环经济产业园污废水资源化项目方案简述(3).pdf'
  223. #path = r'D:\code\rag_tools\RAG资料库\项目模板-资料整理-to\1、工艺包资料\方案模板、PPT\TP-0 工艺技术包汇总及说明PPT-V2.pdf'
  224. #path = r'D:\code\rag_tools\RAG资料库\工艺数据相关\RO计算2.pdf'
  225. reader = PdfReader()
  226. reader.read(path)
  227. for i in reader.text_generator():
  228. print(i)
  229. reader.write('pdf_test.md', mode='w')