out_line_extractor.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. @author: bidikeji
  5. @time: 2025/9/29 11:17
  6. """
  7. from lxml import etree
  8. import re
  9. from typing import List, Dict, Tuple
  10. import time
  11. import logging
  12. # 配置日志
  13. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  14. logger = logging.getLogger(__name__)
  15. class OutlineExtractor:
  16. """
  17. 大纲提取工具类,支持处理不规范HTML文档,能识别短大纲和编号被拆分的情况
  18. 可多次调用,适用于批量处理多个HTML文件
  19. """
  20. def __init__(self, max_merge_short=2, max_merge_normal=5):
  21. """初始化提取器,预编译正则表达式"""
  22. # 配置参数
  23. self.max_merge_short = max_merge_short # 短标题最大合并数量
  24. self.max_merge_normal = max_merge_normal # 正常标题最大合并数量
  25. # 基础编号模式(可能被拆分的部分)
  26. self.number_patterns = {
  27. 'cn_number': re.compile(r'^[一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾]+、?$'),
  28. 'cn_number_with_paren': re.compile(r'^([一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾]+)$'),
  29. 'digit': re.compile(r'^\d+$'),
  30. 'digit_with_paren': re.compile(r'^(\d+)$'),
  31. }
  32. # 符号模式(可能与编号分离的符号)
  33. self.symbol_patterns = {
  34. 'colon': re.compile(r'^[::]$'),
  35. 'comma': re.compile(r'^[、,.]'),
  36. 'dot': re.compile(r'^\.$'),
  37. 'brackets ': re.compile(r'^[()]$'),
  38. }
  39. # 完整编号规则(用于匹配合并后的完整标题)
  40. self.full_outline_patterns = [
  41. (re.compile(r'^[一二三四五六七八九十壹贰叁拾]+[、.].*$'), 1, 6),
  42. (re.compile(r'^([一二三四五六七八九十壹贰叁拾]+).*$'), 2, 6),
  43. (re.compile(r'^\d+[、.\s].*$'), 3, 5),
  44. (re.compile(r'^(\d+).*$'), 4, 5),
  45. (re.compile(r'^\d+\.\d+.*$'), 4, 4),
  46. ]
  47. # 预编译完整模式
  48. self.compiled_full_patterns = [
  49. (re.compile(p.pattern), l, prio) for p, l, prio in self.full_outline_patterns
  50. ]
  51. # 排除规则使用的模式
  52. self.money_patterns = [
  53. re.compile(r'^[¥$]\d+[,.\d]*'),
  54. re.compile(r'^\d+[,.\d]*\s*元'),
  55. re.compile(r'^\d+[,.\d]*\s*([万亿]元)'),
  56. re.compile(r'^\d+[,.\d]*\s*[万亿]'),
  57. re.compile(r'^0+\.'),
  58. ]
  59. self.date_patterns = [
  60. re.compile(r'^\d{4}[年./-]\d{1,2}[月./-]\d{1,2}[日]?'),
  61. re.compile(r'^\d{2}[/-]\d{2}[/-]\d{4}'),
  62. re.compile(r'^\d{4}年\d{1,2}月'),
  63. re.compile(r'^\d{2,4}[年月日]'),
  64. re.compile(r'^(\d{4})')
  65. ]
  66. self.quantity_patterns = [
  67. re.compile(r'^\d+[个只台件套次]'),
  68. re.compile(r'^\d+[.,]\d+[米千克吨亩平立方mkgtMKGT]'),
  69. re.compile(r'^[\d.]+%')
  70. ]
  71. # 缓存
  72. self.level_cache = {}
  73. self.identifier_cache = {}
  74. def _get_node_identifier(self, elem) -> str:
  75. """生成节点的唯一标识字符串,带缓存"""
  76. if elem in self.identifier_cache:
  77. return self.identifier_cache[elem]
  78. if elem is None:
  79. result = "None"
  80. self.identifier_cache[elem] = result
  81. return result
  82. tag = elem.tag
  83. if tag.startswith('{'):
  84. tag = tag.split('}')[-1]
  85. parent_id = self._get_node_identifier(elem.getparent())
  86. siblings = list(elem.getparent()) if elem.getparent() is not None else []
  87. index = siblings.index(elem) if elem in siblings else -1
  88. result = f"{parent_id}->{tag}[{index}]"
  89. self.identifier_cache[elem] = result
  90. return result
  91. def _get_level(self, elem) -> int:
  92. """计算节点层级,带缓存"""
  93. if elem in self.level_cache:
  94. return self.level_cache[elem]
  95. level = 0
  96. current = elem
  97. while current is not None and current.tag != 'body':
  98. level += 1
  99. current = current.getparent()
  100. self.level_cache[elem] = level
  101. return level
  102. def _is_non_outline(self, text: str, is_final: bool = False) -> bool:
  103. """判断是否为非大纲内容(金额、日期、数量等)"""
  104. text = text.strip()
  105. # if len(text) < 2: # 过滤过短文本
  106. # return True
  107. # 检查金额模式
  108. for p in self.money_patterns:
  109. if p.match(text):
  110. return True
  111. # 检查日期模式
  112. for p in self.date_patterns:
  113. if p.match(text):
  114. return True
  115. # 检查数量模式
  116. for p in self.quantity_patterns:
  117. if p.match(text):
  118. return True
  119. if is_final:
  120. # 检查最终句子是否包含中文
  121. if re.search('[\u4e00-\u9fa5]{2,}', text) == None:
  122. # print('检查最终句子是否包含中文', text)
  123. return True
  124. # 检查数字是否3位数以上
  125. elif re.search('^(?\d{3,}|^(\d{1,}\.){3,}\d|^\d{1,}\.\d{3,}', text):
  126. # print('检查数字是否3位数以上', text)
  127. return True
  128. return False
  129. def _collect_text_nodes(self, tree) -> List[Dict]:
  130. """收集所有文本节点及其相关信息,优化查询范围"""
  131. text_nodes = []
  132. # 只查询可能包含标题的标签,减少节点数量
  133. # elements = tree.xpath(
  134. # '//*[self::p or self::div or self::h1 or self::h2 or self::h3 or self::h4 or self::h5 or self::h6 or self::li]')
  135. elements = tree.xpath('//*[normalize-space(text()) != ""]')
  136. for elem in elements:
  137. parent = elem.getparent()
  138. parent_identifier = self._get_node_identifier(parent)
  139. # 获取元素在父节点中的位置及相邻元素
  140. parent_children = list(parent) if parent is not None else []
  141. elem_index = parent_children.index(elem) if elem in parent_children else -1
  142. prev_sibling_elem = parent_children[elem_index - 1] if elem_index > 0 else None
  143. next_sibling_elem = parent_children[elem_index + 1] if elem_index != -1 and elem_index + 1 < len(
  144. parent_children) else None
  145. # 提取文本节点
  146. for node in elem.xpath('./text()[normalize-space() != ""]'):
  147. text = re.sub(r'\s+', ' ', node.strip())
  148. text = text.replace('(', '(').replace(')', ')')
  149. if text:
  150. # print('文本节点:', elem.tag,elem_index, self._get_level(elem) ,parent_identifier, text)
  151. text_nodes.append({
  152. 'text': text,
  153. 'level': self._get_level(elem),
  154. 'position': len(text_nodes), # + 1
  155. 'element': elem,
  156. 'parent_identifier': parent_identifier,
  157. 'prev_sibling_elem': prev_sibling_elem,
  158. 'next_sibling_elem': next_sibling_elem,
  159. 'elem_index': elem_index
  160. })
  161. logger.info(f"收集到有效文本节点: {len(text_nodes)}个")
  162. return text_nodes
  163. def _identify_candidates(self, text_nodes: List[Dict]) -> List[Dict]:
  164. """识别潜在的标题片段,包括被拆分的情况"""
  165. candidate_fragments = []
  166. # for i, item in enumerate(text_nodes):
  167. i = 0
  168. while i < len(text_nodes):
  169. item = text_nodes[i]
  170. text = item['text']
  171. # 初步过滤非大纲内容(双重保险)
  172. if self._is_non_outline(text):
  173. # print('初步过滤非大纲内容text', text)
  174. if len(candidate_fragments)>0:
  175. candidate_fragments.append({
  176. **item,
  177. 'outline_level': None,
  178. 'is_start': False,
  179. 'pattern': None,
  180. 'original_position': (i, i)
  181. })
  182. i += 1
  183. continue
  184. # 检查是否为编号部分(可能被拆分)
  185. is_number_part = False
  186. # print('检查是否为编号部分: ', text)
  187. for _, pattern in self.number_patterns.items():
  188. if pattern.match(text):
  189. is_number_part = True
  190. combined_text = text
  191. current_pos = i
  192. # print('编号部分:', text)
  193. # 向前查找(如果当前是符号,前面可能有编号)
  194. if i > 0:
  195. prev_item = text_nodes[i - 1]
  196. for _, s_pattern in self.symbol_patterns.items():
  197. if s_pattern.match(prev_item['text']):
  198. combined_text = prev_item['text'] + combined_text
  199. current_pos = i - 1
  200. # 向后查找(如果当前是编号,后面可能有符号和文本)
  201. j = i + 1
  202. max_lookahead = min(i + 3, len(text_nodes)) # 最多向后找2个节点
  203. while j < max_lookahead:
  204. next_item = text_nodes[j]
  205. # 检查是否是相邻元素
  206. if (item['next_sibling_elem'] == next_item['element'] or
  207. item['parent_identifier'] == next_item['parent_identifier']):
  208. # 检查是否是符号
  209. is_symbol = False
  210. for _, s_pattern in self.symbol_patterns.items():
  211. if s_pattern.match(next_item['text']):
  212. combined_text += next_item['text']
  213. is_symbol = True
  214. break
  215. if is_symbol:
  216. i = j
  217. j += 1
  218. continue
  219. # 合并文本后停止
  220. combined_text += next_item['text']
  221. i = j
  222. j += 1
  223. break
  224. elif re.match('^[一二三四五六七八九十]+$', item['text']) and re.match('、', next_item['text']):
  225. combined_text += next_item['text']
  226. i = j
  227. j += 1
  228. break
  229. elif re.match('^[一二三四五六七八九十]+、$', item['text']) and re.match('[\u4e00-\u9fa5]', next_item['text']): # 优化 573063077 被多重标签包裹,不是直接相邻标签
  230. combined_text += next_item['text']
  231. i = j
  232. j += 1
  233. break
  234. else:
  235. break
  236. j += 1
  237. # 检查合并后的文本是否符合大纲模式
  238. for pattern, level, priority in self.compiled_full_patterns:
  239. if pattern.match(combined_text):
  240. candidate_fragments.append({
  241. **item,
  242. 'text': combined_text,
  243. 'outline_level': level,
  244. 'is_start': True,
  245. 'pattern': pattern,
  246. 'original_position': (current_pos, j - 1)
  247. })
  248. break
  249. break
  250. # 如果不是编号部分,检查是否为完整标题或普通文本
  251. if not is_number_part:
  252. # print('不是编号一部分:', text)
  253. # 检查是否为完整标题
  254. best_match = None
  255. highest_priority = -1
  256. for pattern, level, priority in self.compiled_full_patterns:
  257. if pattern.match(text):
  258. if priority > highest_priority:
  259. highest_priority = priority
  260. best_match = (pattern, level)
  261. break
  262. if best_match:
  263. candidate_fragments.append({**item,
  264. 'outline_level': best_match[1],
  265. 'is_start': True,
  266. 'pattern': best_match[0],
  267. 'original_position': (i, i)
  268. })
  269. else:
  270. # 可能是标题的一部分
  271. if len(text) > 1 and len(candidate_fragments)>0: # not re.match(r'^[\d\W]+$', text)
  272. candidate_fragments.append({
  273. **item,
  274. 'outline_level': None,
  275. 'is_start': False,
  276. 'pattern': None,
  277. 'original_position': (i, i)
  278. })
  279. i += 1
  280. return candidate_fragments
  281. def _should_merge(self, current_frag, next_frag, current_level, current_parent_id):
  282. """判断是否应该合并两个片段"""
  283. if next_frag['is_start'] and not re.search(r'^\d+\.$|^\d+$', current_frag['text']):
  284. return False
  285. if abs(next_frag['level'] - current_level) > 1:
  286. return False
  287. if next_frag['parent_identifier'] != current_parent_id:
  288. if not (current_frag['elem_index'] + 1 == next_frag['elem_index'] and
  289. current_frag['level'] == next_frag['level']):
  290. return False
  291. if re.search('[\u4e00-\u9fa5]{2,}', current_frag['text']) and next_frag['element'].tag in ['div', 'p' , 'h1', 'h2', 'h3', 'td', 'th']:
  292. return False
  293. return True
  294. def _merge_fragments(self, candidates: List[Dict]) -> List[Dict]:
  295. """合并标题片段,处理短标题情况"""
  296. merged_outlines = []
  297. i = 0
  298. frag_count = len(candidates)
  299. while i < frag_count:
  300. fragment = candidates[i]
  301. if fragment['is_start']:
  302. merged_text = fragment['text']
  303. current_level = fragment['level']
  304. current_parent_id = fragment['parent_identifier']
  305. # start_position = fragment['position']
  306. start_position = fragment['original_position'][0]
  307. end_position = fragment['original_position'][1]
  308. elem_index = fragment['elem_index']
  309. elem_tag = fragment['element'].tag
  310. # 根据标题长度决定最大合并数量
  311. max_merge = self.max_merge_short if len(merged_text) < 5 else self.max_merge_normal
  312. j = i + 1
  313. merge_count = 0
  314. # 优化合并循环
  315. while j < frag_count and merge_count < max_merge:
  316. next_frag = candidates[j]
  317. if self._should_merge(fragment, next_frag, current_level, current_parent_id):
  318. print('需合并大纲: ', merged_text, next_frag['text'])
  319. merged_text += next_frag['text']
  320. end_position = next_frag['original_position'][1]
  321. j += 1
  322. merge_count += 1
  323. else:
  324. break
  325. # 接受有效标题
  326. if len(merged_text) >= 2 and not self._is_non_outline(merged_text, is_final=True):
  327. merged_outlines.append({
  328. 'text': merged_text,
  329. 'level': fragment['level'],
  330. 'position': start_position,
  331. 'start_position': start_position,
  332. 'end_position': end_position,
  333. 'original_fragments': (i, j - 1),
  334. 'outline_level': fragment['outline_level'],
  335. 'pattern': fragment['pattern']
  336. })
  337. i = j
  338. else:
  339. i += 1
  340. return merged_outlines
  341. def wrap_free_text_with_span(self, tree):
  342. """处理直接文本内容,减少重复判断"""
  343. # 去除多列表格
  344. tables = tree.xpath('//table')
  345. # print('表格数量:', len(tables))
  346. for table in tables:
  347. trs = table.xpath('./tr|./tbody/tr')
  348. if trs:
  349. tds = trs[0].xpath('./td|./th')
  350. # print('表格,行数: %d, 列数:%d'%(len(trs), len(tds)))
  351. # print('第一行', trs[0].xpath('string(.)'))
  352. if len(tds) > 2:
  353. parent = table.getparent()
  354. if parent is not None:
  355. parent.remove(table)
  356. # print('去除多列表格,行数: %d, 列数:%d'%(len(trs), len(tds)))
  357. skip_tags = {'script', 'style'}
  358. for element in tree.iter():
  359. if element.tag in skip_tags:
  360. continue
  361. # 处理元素的直接文本
  362. if element.text and element.text.strip():
  363. # 有子元素时文本是游离文本
  364. if len(element) > 0:
  365. span = etree.Element("span")
  366. span.text = element.text
  367. element.text = None
  368. element.insert(0, span)
  369. # 处理tail文本(总是游离文本)
  370. if element.tail and element.tail.strip():
  371. parent = element.getparent()
  372. if parent is not None:
  373. # span = etree.Element("span")
  374. span = etree.Element("p") if element.tag == "br" else etree.Element("span")
  375. span.text = element.tail
  376. element.tail = None
  377. index = parent.index(element) + 1
  378. parent.insert(index, span)
  379. def extract(self, html_content: str) -> List[Dict]:
  380. """
  381. 提取HTML内容中的大纲
  382. Args:
  383. html_content: HTML字符串内容
  384. Returns:
  385. 包含大纲信息的列表,每个元素包含标题、分级、位置等信息
  386. """
  387. start_time = time.time()
  388. self.level_cache.clear() # 清空缓存
  389. self.identifier_cache.clear()
  390. try:
  391. # 解析HTML
  392. tree = etree.HTML(html_content)
  393. # 修复游离文本标签
  394. self.wrap_free_text_with_span(tree)
  395. # 1. 收集文本节点
  396. text_nodes = self._collect_text_nodes(tree)
  397. if not text_nodes:
  398. logger.warning("未找到有效文本节点")
  399. return []
  400. # print('所有文本节点:', [it['text'] for it in text_nodes])
  401. # 2. 识别候选标题片段
  402. candidates = self._identify_candidates(text_nodes)
  403. logger.info(f"识别到候选标题片段: {len(candidates)}个")
  404. # print('候选标题:', [(it['text'], it['is_start']) for it in candidates])
  405. # 3. 合并标题片段
  406. merged_outlines = self._merge_fragments(candidates)
  407. logger.info(f"合并后得到标题: {len(merged_outlines)}个")
  408. # print('合并后标题:', [(it['text'],it['level'], it['outline_level']) for it in merged_outlines])
  409. # 4. 优化分级
  410. optimized_outlines = []
  411. for outline in merged_outlines:
  412. base_level = outline['outline_level']
  413. if outline['level'] < 4:
  414. adjusted_level = max(1, base_level - 1)
  415. elif 4 <= outline['level'] <= 7:
  416. adjusted_level = base_level
  417. else:
  418. adjusted_level = min(5, base_level + 1)
  419. optimized_outlines.append({**outline, 'final_level': adjusted_level})
  420. # print('优化分级后大纲:', [(it['text'],it['final_level']) for it in optimized_outlines])
  421. # 5. 确定作用范围和下属文本
  422. result = []
  423. outline_count = len(optimized_outlines)
  424. for i in range(outline_count):
  425. outline = optimized_outlines[i]
  426. end_pos = len(text_nodes)
  427. # 寻找当前标题的结束位置
  428. for j in range(i + 1, outline_count):
  429. if optimized_outlines[j]['final_level'] <= outline['final_level']:
  430. # end_pos = optimized_outlines[j]['position'] - 1
  431. end_pos = optimized_outlines[j]['start_position'] - 1
  432. break
  433. # 提取下属文本(优化列表推导性能)
  434. sub_text = []
  435. # 拆分大纲中包含的内容 例子:一、采购单编号:P-XJ-24-00044544
  436. if re.search('[::]', outline['text']):
  437. text1, text2 = re.split('[::]', outline['text'], maxsplit=1)
  438. outline['text'] = text1
  439. if text2 != "":
  440. sub_text.append(text2)
  441. # pos = outline['position']
  442. pos = outline['end_position']
  443. for item in text_nodes:
  444. if pos < item['position'] <= end_pos:
  445. sub_text.append(item['text'])
  446. sub_text = ''.join(sub_text)
  447. result.append({
  448. '标题': outline['text'],
  449. '分级': outline['final_level'],
  450. '位置': outline['position'],
  451. '作用范围': f"{outline['position']}-{end_pos}",
  452. '下属文本': sub_text,
  453. '合并信息': f"合并了{outline['original_fragments'][1]-outline['original_fragments'][0]+1}个片段"
  454. })
  455. # result.append((outline['text'], sub_text))
  456. # 输出性能指标
  457. end_time = time.time()
  458. logger.info(f"提取完成,耗时: {end_time - start_time:.4f}秒,处理文本节点: {len(text_nodes)}个")
  459. return result
  460. except Exception as e:
  461. logger.error(f"提取过程出错: {str(e)}", exc_info=True)
  462. return []
  463. def extract_from_file(self, file_path: str, encoding: str = 'utf-8') -> List[Dict]:
  464. """
  465. 从HTML文件中提取大纲
  466. Args:
  467. file_path: HTML文件路径
  468. encoding: 文件编码,默认为utf-8
  469. Returns:
  470. 包含大纲信息的列表
  471. """
  472. try:
  473. with open(file_path, 'r', encoding=encoding) as f:
  474. html_content = f.read()
  475. return self.extract(html_content)
  476. except Exception as e:
  477. logger.error(f"从文件提取失败 {file_path}: {str(e)}")
  478. return []
  479. # 使用示例
  480. if __name__ == "__main__":
  481. # 创建提取器实例(可重复使用)
  482. extractor = OutlineExtractor()
  483. # # 示例1:从文件提取
  484. # outline = extractor.extract_from_file('d:/html/2.html') # YC2023-10-0027中标招标错误 大纲范例1表格内容大纲
  485. # print('大纲:', outline)
  486. # if outline and isinstance(outline[0], tuple):
  487. # print([it[0] for it in outline])
  488. # print('内容:', outline)
  489. # # 打印结果
  490. # print(f"共提取到 {len(outline)} 个大纲标题\n")
  491. # for item in outline:
  492. # print(f"**{item['分级']}级标题: {item['标题']}")
  493. # print(f"下属文本: {item['下属文本'][:100]}...") # 只显示前100字符
  494. # print(f"分级: {item['分级']}")
  495. # print(f"位置: {item['位置']}")
  496. # print(f"作用范围: {item['作用范围']}")
  497. # print("-" * 80)
  498. import pandas as pd
  499. import time
  500. def get_out_line_text(html_str):
  501. outline = extractor.extract(html_str)
  502. outline = [it['标题'][:20] for it in outline]
  503. # print(outline)
  504. return outline
  505. # df = pd.read_csv('E:\待检查数据/待检查_一周待筛选数据的公告_类别站源筛选数据_html.csv')
  506. # print('公告数量:', len(df))
  507. # datas = []
  508. # t1 = time.time()
  509. # for docid, html_str in zip(df['docid'], df['dochtmlcon']):
  510. # outline = get_out_line_text(html_str)
  511. # if outline:
  512. # # print(outline)
  513. # datas.append((docid, '\n'.join(outline)))
  514. # # break
  515. #
  516. # df = pd.DataFrame(datas, columns=['docid', '大纲'])
  517. # print('len(df)', len(df))
  518. # print('耗时:', time.time()-t1)
  519. # df.to_excel('E:\待检查数据/待检查_一周待筛选数据的公告_类别站源筛选数据_大纲提取结果0928.xlsx', index=False)
  520. df = pd.read_csv('E:/地区匹配/20241213-19导出数据_站源地区筛选数据_html.csv')[:]
  521. # df2 = pd.read_excel('E:\大纲提取内容/20241213-19导出数据_站源地区筛选数据_大纲提取0425.xlsx')
  522. df2 = pd.read_excel('E:\大纲提取内容/20241213-19导出数据_站源地区筛选数据_大纲提取新旧方法对比0928.xlsx')
  523. print('公告数量:', len(df), len(df2))
  524. datas = []
  525. t1 = time.time()
  526. for docid, html_str in zip(df['docid'], df['dochtmlcon']):
  527. outline = get_out_line_text(html_str)
  528. # if outline:
  529. # print(outline)
  530. datas.append((docid, '\n'.join(outline), len(outline)))
  531. df = pd.DataFrame(datas, columns=['docid', '调整候选', '大纲数量4'])
  532. print('len(df)', len(df))
  533. print('耗时:', time.time()-t1)
  534. df = df.merge(df2, on='docid', how='inner')
  535. df.to_excel('E:\大纲提取内容/20241213-19导出数据_站源地区筛选数据_大纲提取新旧方法对比0928.xlsx', index=False)
  536. # df = pd.read_csv('E:/模版提取/中国南方电网-站源公告_html.csv')
  537. # print('公告数量:', len(df))
  538. # datas = []
  539. # t1 = time.time()
  540. # for docid, html_str in zip(df['docid'], df['dochtmlcon']):
  541. # outline = get_out_line_text(html_str)
  542. # if outline:
  543. # # print(outline)
  544. # datas.append((docid, '\n'.join(outline[:5])))
  545. # # break
  546. #
  547. # df = pd.DataFrame(datas, columns=['docid', '大纲'])
  548. # print('len(df)', len(df))
  549. # print('耗时:', time.time()-t1)
  550. #
  551. # from collections import Counter
  552. # c = Counter(df['大纲'])
  553. # print(c.most_common(200))
  554. # df.to_excel('E:\模版提取/中国南方电网-站源公告_大纲提取结果.xlsx', index=False)
  555. from copy import deepcopy
  556. html_content = """
  557. <div>
  558. <p>正文内容:</p>
  559. <p> 项目名称:</p>
  560. </div>
  561. <div class='fujian'>
  562. 附件文本, 相关附件内容
  563. </div>
  564. """
  565. # def extract_div_with_metadata(html_content, div_xpath):
  566. # """提取div并保留元数据信息"""
  567. # doc = etree.HTML(html_content)
  568. #
  569. # target_div = doc.xpath(div_xpath)
  570. # if not target_div:
  571. # return None, None
  572. #
  573. # target_div = target_div[0]
  574. #
  575. # # 创建新树
  576. # new_doc = etree.Element("div") # extracted-content
  577. # new_doc.append(deepcopy(target_div))
  578. #
  579. # # # 添加元数据
  580. # # metadata = etree.SubElement(new_doc, "metadata")
  581. # # etree.SubElement(metadata, "original-xpath").text = div_xpath
  582. # # etree.SubElement(metadata, "extraction-time").text = "2024-01-01"
  583. #
  584. # # # 添加内容
  585. # # content = etree.SubElement(new_doc, "content")
  586. # # content.append(deepcopy(target_div))
  587. #
  588. # # 从原文档移除
  589. # parent = target_div.getparent()
  590. # if parent is not None:
  591. # parent.remove(target_div)
  592. #
  593. # return etree.ElementTree(new_doc), etree.ElementTree(doc)
  594. #
  595. #
  596. # # 使用示例
  597. # new_tree, original_tree = extract_div_with_metadata(
  598. # html_content,
  599. # '//div[@class="fujian"]'
  600. # )
  601. # if new_tree:
  602. # print('附件:', new_tree.xpath('string(.)'))
  603. # print('正文:', original_tree.xpath('string(.)'))
  604. # # if new_tree:
  605. # # print("带元数据的提取树:")
  606. # # print(etree.tostring(new_tree, encoding='unicode', pretty_print=True))
  607. # # print('original_tree: ', etree.tostring(original_tree, encoding='unicode', pretty_print=True))