table_extractor.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. @author: bidikeji
  5. @time: 2025/10/11 11:48
  6. """
  7. from lxml import etree
  8. import copy
  9. import re
  10. from BiddingKG.dl.template_extract.abstract_template import table2list
  11. def get_html_table(html):
  12. """
  13. 解析html提取表格内容
  14. :param html:
  15. :return: 表格列表
  16. """
  17. table_list = []
  18. tree = etree.HTML(html)
  19. # 拆分正文和附件
  20. richText = tree.xpath('//div[@class="richTextFetch"]')
  21. if richText:
  22. parent = richText[0].getparent()
  23. richText_tree = copy.deepcopy(richText[0])
  24. parent.remove(richText[0])
  25. # richText = re.sub('\s', '', richText_tree.xpath('string(.)'))
  26. # if len(richText) < 2:
  27. # print('异常附件:', docid)
  28. # print('richText:', richText[:20])
  29. # print(tree.xpath('//div[@class="richTextFetch"]'))
  30. tables = tree.xpath('//table')
  31. for table in tables:
  32. # 判断是否有嵌套表格
  33. inner_tb = table.xpath('././/table')
  34. filter_flag = False # 嵌套表格行数大于等于2,不处理外部表格
  35. for tb in inner_tb:
  36. parent = tb.getparent()
  37. # print('嵌套表格列数:', len(tb.xpath('././/tr')))
  38. # 如果嵌套表格行数小于2,直接把表格内容赋予附标签
  39. if 0 < len(tb.xpath('././/tr')) < 2:
  40. parent.text = parent.text.strip() if parent.text else ''
  41. parent.text += ' '.join(tb.xpath('././/text()')).strip()
  42. else:
  43. filter_flag = True
  44. parent.remove(tb)
  45. # print('表格内容:', table.xpath('.//td//text()|.//th//text()'))
  46. if filter_flag:
  47. continue
  48. l = table2list(table)
  49. table_list.append(l)
  50. return table_list
  51. def get_table_preceding_text(table):
  52. caption_text = table.xpath('string(./caption)').strip()
  53. if caption_text:
  54. return caption_text
  55. preceding_text = ''
  56. preceding_tags = table.xpath('./preceding-sibling::*')
  57. if preceding_tags == []:
  58. if table.getparent() is not None:
  59. preceding_tags = table.getparent().xpath('./preceding-sibling::*')
  60. block_num = 0
  61. for tag in reversed(preceding_tags):
  62. if tag.tag == 'table':
  63. break
  64. elif tag.tag == 'div' and (tag.xpath('.//table') or len(tag.xpath('p'))>2):
  65. break
  66. texts = tag.xpath('.//text()')
  67. current_text = ''
  68. for text in reversed(texts):
  69. text = text.strip()
  70. if text:
  71. current_text = text + current_text
  72. preceding_text = current_text + preceding_text
  73. if tag.tag in ['p', 'div', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']:
  74. block_num += 1
  75. if block_num > 2:
  76. break
  77. if current_text.strip():
  78. break
  79. if len(preceding_text) > 100:
  80. break
  81. return preceding_text
  82. def get_html_table_and_prev_text(html, return_richText=False):
  83. """
  84. 解析html提取表格内容及表格前文
  85. :param html:
  86. :return: 表格列表
  87. """
  88. def extract_table(tree):
  89. '''
  90. 提取所有表格并返回每个表格内容列表矩阵及表格前文
  91. :param tree: etree 根节点
  92. :return:
  93. '''
  94. tables = tree.xpath('//table')
  95. table_list = []
  96. tables_prev_text = []
  97. for table in tables:
  98. # 判断是否有嵌套表格
  99. inner_tb = table.xpath('././/table')
  100. filter_flag = False # 嵌套表格行数大于等于2,不处理外部表格
  101. for tb in inner_tb:
  102. parent = tb.getparent()
  103. # print('嵌套表格列数:', len(tb.xpath('././/tr')))
  104. # 如果嵌套表格行数小于2,直接把表格内容赋予附标签
  105. if 0 < len(tb.xpath('././/tr')) < 2:
  106. parent.text = parent.text.strip() if parent.text else ''
  107. parent.text += ' '.join(tb.xpath('././/text()')).strip()
  108. else:
  109. filter_flag = True
  110. parent.remove(tb)
  111. if filter_flag:
  112. continue
  113. # table_grid = table2list(table)
  114. table_grid = parse_table_to_grid(table)
  115. full_prev_text = get_table_preceding_text(table)
  116. table_list.append(table_grid)
  117. tables_prev_text.append(full_prev_text)
  118. return table_list, tables_prev_text
  119. tree = etree.HTML(html)
  120. table_list_richText = []
  121. tables_prev_text_richText = []
  122. # 拆分正文和附件
  123. richText = tree.xpath('//div[@class="richTextFetch"]')
  124. if richText:
  125. if return_richText:
  126. table_list_richText, tables_prev_text_richText = extract_table(richText[0])
  127. parent = richText[0].getparent()
  128. parent.remove(richText[0])
  129. table_list, tables_prev_text = extract_table(tree)
  130. if return_richText:
  131. return table_list, tables_prev_text, table_list_richText, tables_prev_text_richText
  132. else:
  133. return table_list, tables_prev_text
  134. def normalize_text(text):
  135. """清理文本:去除多余空白,保留单个空格"""
  136. if text is None:
  137. return ""
  138. return re.sub(r'\s+', ' ', text.strip())
  139. def parse_table_to_grid(table_element):
  140. """
  141. 将 lxml 的 table 元素解析为二维网格(list of lists)
  142. 处理 rowspan 和 colspan
  143. """
  144. rows = table_element.xpath('.//tr')
  145. if not rows:
  146. return []
  147. # 第一步:收集所有单元格及其位置信息
  148. grid = [] # 最终的二维网格
  149. row_index = 0
  150. for tr in rows:
  151. # 找到当前行实际起始列(跳过已被 rowspan 占用的格子)
  152. if len(grid) <= row_index:
  153. grid.append([])
  154. col_index = 0
  155. # 跳过已被上方 rowspan 占据的列
  156. while col_index < len(grid[row_index]) and grid[row_index][col_index] is not None:
  157. col_index += 1
  158. cells = tr.xpath('.//td | .//th')
  159. for cell in cells:
  160. # 跳过已被占据的位置
  161. while col_index < len(grid[row_index]) and grid[row_index][col_index] is not None:
  162. col_index += 1
  163. # 获取文本内容
  164. text = normalize_text(''.join(cell.xpath('.//text()')))
  165. # 处理省略号情况:如果有title属性且文本以...结尾,使用title内容
  166. title_attr = cell.get('title')
  167. if (title_attr and text.replace(' ', '').endswith('...') and
  168. title_attr.replace(' ', '').startswith(text.replace(' ', '')[:-3])):
  169. text = title_attr
  170. text = re.sub('\s', '', text).replace('(', '(').replace(')', ')') # 修复 653042182 成交金额 被不同标签拆分中间有空格导致提取失败
  171. # 获取 colspan 和 rowspan(默认为1)
  172. colspan = int(cell.get('colspan', '1')) if cell.get('colspan', '1').isdigit() and int(cell.get('colspan', '1')) > 0 else 1 # 修复 234475886 colspan="0"异常情况
  173. rowspan = int(cell.get('rowspan', '1')) if cell.get('rowspan', '1').isdigit() and int(cell.get('rowspan', '1')) > 0 else 1
  174. # 确保网格足够大
  175. for r in range(row_index, row_index + rowspan):
  176. while len(grid) <= r:
  177. grid.append([])
  178. while len(grid[r]) <= col_index + colspan - 1:
  179. grid[r].append(None)
  180. # 填充主单元格
  181. grid[row_index][col_index] = text
  182. # 标记被 colspan 占据的位置(同一行)
  183. for c in range(col_index + 1, col_index + colspan):
  184. # grid[row_index][c] = '' # 空字符串表示被合并
  185. grid[row_index][c] = text
  186. # 标记被 rowspan 占据的位置(下方行)
  187. for r in range(row_index + 1, row_index + rowspan):
  188. for c in range(col_index, col_index + colspan):
  189. # grid[r][c] = '' # 被上方 rowspan 占据
  190. grid[r][c] = text
  191. col_index += colspan
  192. row_index += 1
  193. # 修复第一行前面有空格异常导致表头内容错位 例:715247564
  194. if len(grid) > 1 and len(grid[0]) > len(grid[1]):
  195. n = 0
  196. for it in grid[0]:
  197. if it == '':
  198. n += 1
  199. else:
  200. break
  201. if n > 0 and len(grid[0]) - n == len(grid[1]):
  202. grid[0] = grid[0][n:]
  203. # 清理:确保所有行长度一致(补 None)
  204. max_cols = max(len(row) for row in grid) if grid else 0
  205. for row in grid:
  206. while len(row) < max_cols:
  207. row.append(None)
  208. # 将 None 替换为空字符串(未定义的单元格)
  209. for i in range(len(grid)):
  210. for j in range(len(grid[i])):
  211. if grid[i][j] is None:
  212. grid[i][j] = ""
  213. # 移除重复的行
  214. filtered_grid = []
  215. for row in grid:
  216. if row not in filtered_grid:
  217. filtered_grid.append(row)
  218. return filtered_grid
  219. if __name__ == "__main__":
  220. with open('d:/html/2.html', encoding='utf-8') as f:
  221. html = f.read()
  222. # table_list = get_html_table(html)
  223. # for table in table_list:
  224. # for data in table:
  225. # print(data)
  226. # print([len(row) for row in table])
  227. table_list, tables_prev_text = get_html_table_and_prev_text(html)
  228. i = 0
  229. for table in table_list:
  230. print('表格前文:', tables_prev_text[i])
  231. i += 1
  232. for data in table:
  233. print(data)
  234. print([len(row) for row in table])