outline.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. # -*- coding: utf-8 -*-
  2. """大纲提取与分句。
  3. 按 ARCHITECTURE.md Phase 4 拆分建议,合并 ``interface/outline_extractor.py`` 与
  4. ``interface/Preprocessing.py`` 中的大纲相关函数。
  5. 类型:PREPROCESS。
  6. 来源:
  7. - ``interface/Preprocessing.py``: ``get_preprocessed_outline``, ``change2num``, ``re_num``, ``num_dict``
  8. - ``interface/outline_extractor.py``: ``Sentence2``, ``extract_sentence_list``, patterns, ``extract_parameters``, ``extract_addr``
  9. ``interface/Preprocessing.py`` 和 ``interface/outline_extractor.py`` 仍 re-export 以上全部名称。
  10. """
  11. from __future__ import absolute_import
  12. import re
  13. from BiddingKG.dl.interface.htmlparser import ParseDocument, get_childs
  14. __all__ = [
  15. "re_num",
  16. "num_dict",
  17. "change2num",
  18. "get_preprocessed_outline",
  19. "Sentence2",
  20. "extract_sentence_list",
  21. "requirement_pattern",
  22. "winter_pattern",
  23. "aptitude_pattern",
  24. "addr_bidopen_pattern",
  25. "addr_bidsend_pattern",
  26. "pinmu_name_pattern",
  27. "policy_pattern",
  28. "not_policy_pattern",
  29. "correction_pattern",
  30. "extract_parameters",
  31. "extract_addr",
  32. ]
  33. re_num = re.compile("[二三四五六七八九]十[一二三四五六七八九]?|十[一二三四五六七八九]|[一二三四五六七八九十]")
  34. num_dict = {
  35. "一": 1, "二": 2,
  36. "三": 3, "四": 4,
  37. "五": 5, "六": 6,
  38. "七": 7, "八": 8,
  39. "九": 9, "十": 10}
  40. # 一百以内的中文大写转换为数字
  41. def change2num(text):
  42. result_num = -1
  43. # text = text[:6]
  44. match = re_num.search(text)
  45. if match:
  46. _num = match.group()
  47. if num_dict.get(_num):
  48. return num_dict.get(_num)
  49. else:
  50. tenths = 1
  51. the_unit = 0
  52. num_split = _num.split("十")
  53. if num_dict.get(num_split[0]):
  54. tenths = num_dict.get(num_split[0])
  55. if num_dict.get(num_split[1]):
  56. the_unit = num_dict.get(num_split[1])
  57. result_num = tenths * 10 + the_unit
  58. elif re.search("\d{1,2}",text):
  59. _num = re.search("\d{1,2}",text).group()
  60. result_num = int(_num)
  61. return result_num
  62. #大纲分段处理
  63. def get_preprocessed_outline(soup):
  64. pattern_0 = re.compile("^(?:[二三四五六七八九]十[一二三四五六七八九]?|十[一二三四五六七八九]|[一二三四五六七八九十])[、.\.]")
  65. pattern_1 = re.compile("^[\((]?(?:[二三四五六七八九]十[一二三四五六七八九]?|十[一二三四五六七八九]|[一二三四五六七八九十])[\))]")
  66. pattern_2 = re.compile("^\d{1,2}[、.\.](?=[^\d]{1,2}|$)")
  67. pattern_3 = re.compile("^[\((]?\d{1,2}[\))]")
  68. pattern_list = [pattern_0, pattern_1, pattern_2, pattern_3]
  69. body = soup.find("body")
  70. if body == None:
  71. return soup # 修复 无body的报错 例子:264419050
  72. body_child = body.find_all(recursive=False)
  73. deal_part = body
  74. # print(body_child[0]['id'])
  75. if 'id' in body_child[0].attrs:
  76. if len(body_child) <= 2 and body_child[0]['id'] == 'pcontent':
  77. deal_part = body_child[0]
  78. if len(deal_part.find_all(recursive=False))>2:
  79. deal_part = deal_part.parent
  80. skip_tag = ['turntable', 'tbody', 'th', 'tr', 'td', 'table','thead','tfoot']
  81. for part in deal_part.find_all(recursive=False):
  82. # 查找解析文本的主干部分
  83. is_main_text = False
  84. through_text_num = 0
  85. while (not is_main_text and part.find_all(recursive=False)):
  86. while len(part.find_all(recursive=False)) == 1 and part.get_text(strip=True) == \
  87. part.find_all(recursive=False)[0].get_text(strip=True):
  88. part = part.find_all(recursive=False)[0]
  89. max_len = len(part.get_text(strip=True))
  90. is_main_text = True
  91. for t_part in part.find_all(recursive=False):
  92. if t_part.name not in skip_tag and t_part.get_text(strip=True)!="":
  93. through_text_num += 1
  94. if t_part.get_text(strip=True)!="" and len(t_part.get_text(strip=True))/max_len>=0.65:
  95. if t_part.name not in skip_tag:
  96. is_main_text = False
  97. part = t_part
  98. break
  99. else:
  100. while len(t_part.find_all(recursive=False)) == 1 and t_part.get_text(strip=True) == \
  101. t_part.find_all(recursive=False)[0].get_text(strip=True):
  102. t_part = t_part.find_all(recursive=False)[0]
  103. if through_text_num>2:
  104. is_table = True
  105. for _t_part in t_part.find_all(recursive=False):
  106. if _t_part.name not in skip_tag:
  107. is_table = False
  108. break
  109. if not is_table:
  110. is_main_text = False
  111. part = t_part
  112. break
  113. else:
  114. is_main_text = False
  115. part = t_part
  116. break
  117. is_find = False
  118. for _pattern in pattern_list:
  119. last_index = 0
  120. handle_list = []
  121. for _part in part.find_all(recursive=False):
  122. if _part.name not in skip_tag and _part.get_text(strip=True) != "":
  123. # print('text:', _part.get_text(strip=True))
  124. re_match = re.search(_pattern, _part.get_text(strip=True))
  125. if re_match:
  126. outline_index = change2num(re_match.group())
  127. if last_index < outline_index:
  128. # _part.insert_before("##split##")
  129. handle_list.append(_part)
  130. last_index = outline_index
  131. if len(handle_list)>1:
  132. is_find = True
  133. for _part in handle_list:
  134. _part.insert_before("##split##")
  135. if is_find:
  136. break
  137. # print(soup)
  138. return soup
  139. class Sentence2():
  140. def __init__(self,text,sentence_index,wordOffset_begin,wordOffset_end):
  141. self.name = 'sentence2'
  142. self.text = text
  143. self.sentence_index = sentence_index
  144. self.wordOffset_begin = wordOffset_begin
  145. self.wordOffset_end = wordOffset_end
  146. def get_text(self):
  147. return self.text
  148. def extract_sentence_list(sentence_list):
  149. new_sentence2_list = []
  150. new_sentence2_list_attach = []
  151. for sentence in sentence_list:
  152. sentence_index = sentence.sentence_index
  153. sentence_text = sentence.sentence_text
  154. begin_index = 0
  155. end_index = 0
  156. for it in re.finditer('([^一二三四五六七八九十,。][一二三四五六七八九十]{1,3}|[^\d\.、,。a-zA-Z]\d{1,2}(\.\d{1,2}){,2})、', sentence_text): # 例:289699210 1、招标内容:滑触线及配件2、招标品牌:3、参标供应商经营形式要求:厂家4、参标供应商资质要求:5、
  157. temp = it.group(0)
  158. sentence_text = sentence_text.replace(temp, temp[0] + ',' + temp[1:])
  159. for item in re.finditer('[,。:;;!!?]+', sentence_text): # 20240725去掉英文问号,避免网址被分隔
  160. end_index = item.end()
  161. # if end_index!=len(sentence_text):
  162. # # if end_index-begin_index<6 and item.group(0) in [',', ';', ';'] and re.match('[一二三四五六七八九十\d.]+、', sentence_text[begin_index:end_index])==None: # 20240725 注销,避免标题提取错误
  163. # # continue
  164. if end_index != len(sentence_text) and re.match('[一二三四五六七八九十\d.]{1,2}[、,.]+$', sentence_text[begin_index:end_index]): # 避免表格序号和内容在不同表格情况 例:293178161
  165. continue
  166. new_sentence_text = sentence_text[begin_index:end_index]
  167. sentence2 = Sentence2(new_sentence_text,sentence_index,begin_index,end_index)
  168. if sentence.in_attachment:
  169. new_sentence2_list_attach.append(sentence2)
  170. else:
  171. new_sentence2_list.append(sentence2)
  172. begin_index = end_index
  173. if end_index!=len(sentence_text):
  174. end_index = len(sentence_text)
  175. new_sentence_text = sentence_text[begin_index:end_index]
  176. sentence2 = Sentence2(new_sentence_text, sentence_index, begin_index, end_index)
  177. if sentence.in_attachment:
  178. new_sentence2_list_attach.append(sentence2)
  179. else:
  180. new_sentence2_list.append(sentence2)
  181. return new_sentence2_list, new_sentence2_list_attach
  182. requirement_pattern = "(采购需求|需求分析|项目说明|(采购|合同|招标|询比?价|项目|服务|工程|标的|需求|建设)(的?(主要|简要|基本|具体|名称及))?" \
  183. "(内容|概况|概述|范围|信息|规模|简介|介绍|说明|摘要|情况)([及与和]((其它|\w{,2})[要需]求|发包范围|数量))?" \
  184. "|招标项目技术要求|服务要求|服务需求|项目目标|需求内容如下|建设规模)为?([::,]|$)"
  185. winter_pattern = "((乙方|竞得|受让|买受|签约|供货|供应|承做|承包|承建|承销|承保|承接|承制|承担|承修|承租(?:(包))?|入围|入选|竞买|中标|中选|中价|中签|成交|候选)[\u4e00-\u9fa5]{0,5}" \
  186. "(公示)?(信息|概况|情况|名称|联系人|联系方式|负责人)|中标公示单位|合同主体)为?([::,、]|$)"
  187. aptitude_pattern = "资质(资格)要求|资格(资质)要求|单位要求|资质及业绩要求|((资格|资质|准入)[的及]?(要求|条件|标准|限定|门槛)|竞买资格及要求|供应商报价须知)|按以下要求参与竞买|((报名|应征|竞买|投标|竞投|受让|报价|竞价|竞包|竞租|承租|申请|参与|参选|遴选)的?(人|方|单位|企业|客户|机构)?|供应商|受让方)((必?须|需|应[该当]?)(具备|满足|符合|提供)+以?下?)?的?(一般|基本|主要)?(条件|要求|资格(能力)?|资质)+|乙方应当符合下列要求|参与比选条件|合格的投标人|询价要求"
  188. addr_bidopen_pattern = "([开评]标|开启|评选|比选|磋商|遴选|寻源|采购|招标|竞价|议价|委托|询比?价|比价|谈判|邀标|邀请|洽谈|约谈|选取|抽取|抽选|递交\w{,4}文件)[))]?(时间[与及和、])?(地址|地点)([与及和、]时间)?([::,]|$)|开启([::,]|$)"
  189. addr_bidsend_pattern = "((\w{,4}文件)?(提交|递交)(\w{,4}文件)?|投标)(截止时间[与及和、])?地[点址]([与及和、]截止时间)?([::,]|$)"
  190. pinmu_name_pattern = "采购品目(名称)?([::,]|$)"
  191. policy_pattern = "《.+?(通知|办法|条例|规定|规程|规范|须知|规则|标准|细则|意见|协议|条件|要求|手册|法典|方案|指南|指引|法)》"
  192. not_policy_pattern = "(表|函|书|证|\d页|公告|合同|文件|清单)》$|采购合同|响应方须知|响应文件格式|营业执照|开标一览|采购需求"
  193. correction_pattern = "(更正|更改|修正|修改|变更|延期)(信息|内容|事项|详情)"
  194. def extract_parameters(parse_document):
  195. '''
  196. 通过大纲、预处理后文本正则获取需要字段
  197. :param parse_document: ParseDocument() 方法返回结果
  198. :return:
  199. '''
  200. list_data = parse_document.tree
  201. requirement_text = '' # 采购内容
  202. aptitude_text = '' # 资质要求
  203. addr_bidopen_text = '' # 开标地址
  204. addr_bidsend_text = '' # 投标地址
  205. requirement_scope = [] # 采购内容始末位置
  206. winter_scope = [] # 中标信息始末位置
  207. pinmu_name = '' # 品目名称
  208. list_policy = [] # 政策法规
  209. correction_content = "" # 更正内容
  210. out_lines = []
  211. _find_count = 0
  212. _data_i = -1
  213. while _data_i<len(list_data)-1:
  214. _data_i += 1
  215. _data = list_data[_data_i]
  216. _type = _data["type"]
  217. _text = _data["text"].strip()
  218. _text = _text.replace('(', '(').replace(')', ')') # 20250729 统一为中文括号
  219. # print(_data.keys())
  220. if _type=="sentence":
  221. if _data["sentence_title"] is not None:
  222. if re.search('[((][一二三四五六七八九十\d]+[))]|[一二三四五六七八九十]+\s*[..、]|^(\d{1,2}[..、]|(\d\.)+\d)[\u4e00-\u9fa5]', _text[:10]):
  223. idx = _text.replace(':', ':').find(':')
  224. outline_text = _text[:idx] if idx >= 4 else _text
  225. # out_lines.append((outline_text, _data['sentence_index'], _data['wordOffset_begin']))
  226. childs = get_childs([_data])
  227. if len(childs) > 0:
  228. scope = ((childs[0]['sentence_index'], childs[0]['wordOffset_begin']),
  229. (childs[-1]['sentence_index'], childs[-1]['wordOffset_end']))
  230. else:
  231. scope = ((_data['sentence_index'], _data['wordOffset_begin']),
  232. (_data['sentence_index'], _data['wordOffset_end']))
  233. out_lines.append((outline_text, _data['sentence_title'], _data['title_index'], _data['next_index'], scope))
  234. if re.search(requirement_pattern,_text[:30]) is not None and re.search('符合采购需求,', _text[:30])==None:
  235. b = (_data['sentence_index'], _data['wordOffset_begin'])
  236. childs = get_childs([_data])
  237. for c in childs:
  238. # requirement_text += c["text"]+"\n"
  239. requirement_text += c["text"]
  240. e = (c['sentence_index'], c["wordOffset_end"]) if len(childs)>0 else (_data['sentence_index'], _data['wordOffset_end'])
  241. requirement_scope.append(b)
  242. requirement_scope.append(e)
  243. _data_i += len(childs)
  244. _data_i -= 1
  245. _data_i = -1
  246. # 中标信息
  247. while _data_i<len(list_data)-1:
  248. _data_i += 1
  249. _data = list_data[_data_i]
  250. _type = _data["type"]
  251. _text = _data["text"].strip()
  252. # print(_data.keys())
  253. if _type=="sentence":
  254. # print('_text',_text)
  255. # print('sentence_title',_data["sentence_title"])
  256. if _data["sentence_title"] is not None:
  257. if re.search(winter_pattern,_text[:30]) is not None:
  258. # print('winter_pattern text', _text)
  259. b = (_data['sentence_index'], _data['wordOffset_begin'])
  260. childs = get_childs([_data])
  261. e = (childs[-1]['sentence_index'], childs[-1]["wordOffset_end"]) if len(childs)>0 else (_data['sentence_index'], _data['wordOffset_end'])
  262. winter_scope.append(b)
  263. winter_scope.append(e)
  264. _data_i += len(childs)
  265. _data_i -= 1
  266. _data_i = -1
  267. # 更正内容
  268. while _data_i < len(list_data) - 1:
  269. _data_i += 1
  270. _data = list_data[_data_i]
  271. _type = _data["type"]
  272. _text = _data["text"].strip()
  273. if _type == "sentence":
  274. if _data["sentence_title"] is not None:
  275. if re.search(correction_pattern, _text[:20]) is not None:
  276. childs = get_childs([_data])
  277. correction_text = ""
  278. for c in childs:
  279. correction_text += c["text"].strip()
  280. # print('correction_text',correction_text)
  281. correction_content += correction_text
  282. _data_i += len(childs)
  283. _data_i -= 1
  284. _data_i = -1
  285. while _data_i<len(list_data)-1:
  286. _data_i += 1
  287. _data = list_data[_data_i]
  288. _type = _data["type"]
  289. _text = _data["text"].strip()
  290. # print(_data.keys())
  291. if _type=="sentence":
  292. # print("aptitude_pattern", _text)
  293. if _data["sentence_title"] is not None:
  294. # print("aptitude_pattern",_text)
  295. # outline = re.sub('(?[一二三四五六七八九十\d.]+)?\s*、?', '',
  296. # re.split('[::,]', _text)[0].replace('(', '(').replace(')', ')'))
  297. if re.search(aptitude_pattern,_text[:15]) is not None:
  298. childs = get_childs([_data])
  299. for c in childs:
  300. aptitude_text += c["text"]
  301. # if c["sentence_title"]:
  302. # aptitude_text += c["text"]+"\n"
  303. # else:
  304. # aptitude_text += c["text"]
  305. _data_i += len(childs)
  306. _data_i -= 1
  307. # elif re.match('[((\s★▲\*]?[一二三四五六七八九十\dⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ]+', _text) and len(_text)<30 and re.search('资质|资格', _text):
  308. # out_lines.append(outline)
  309. if _type=="table":
  310. list_table = _data["list_table"]
  311. parent_title = _data["parent_title"]
  312. if list_table is not None:
  313. for line in list_table[:2]:
  314. for cell_i in range(len(line)):
  315. cell = line[cell_i]
  316. cell_text = cell[0]
  317. if len(cell_text)>120 and re.search(aptitude_pattern,cell_text) is not None:
  318. aptitude_text += cell_text+"\n"
  319. _data_i = -1
  320. while _data_i < len(list_data) - 1:
  321. _data_i += 1
  322. _data = list_data[_data_i]
  323. _type = _data["type"]
  324. _text = _data["text"].strip()
  325. # print(_data.keys())
  326. if _type == "sentence":
  327. if _data["sentence_title"] is not None:
  328. if re.search(addr_bidopen_pattern, _text[:20]) is not None:
  329. childs = get_childs([_data], max_depth=1)
  330. for c in childs:
  331. addr_bidopen_text += c["text"]
  332. _data_i += len(childs)
  333. _data_i -= 1
  334. elif re.search(addr_bidsend_pattern, _text[:20]):
  335. childs = get_childs([_data], max_depth=1)
  336. for c in childs:
  337. addr_bidsend_text += c["text"]
  338. _data_i += len(childs)
  339. _data_i -= 1
  340. elif re.search(pinmu_name_pattern, _text):
  341. childs = get_childs([_data], max_depth=1)
  342. for c in childs:
  343. pinmu_name += c["text"]
  344. _data_i += len(childs)
  345. _data_i -= 1
  346. _data_i = -1
  347. while _data_i<len(list_data)-1:
  348. _data_i += 1
  349. _data = list_data[_data_i]
  350. _type = _data["type"]
  351. _text = _data["text"].strip()
  352. # print(_data.keys())
  353. if _type=="sentence":
  354. for it in re.finditer(policy_pattern, _text):
  355. if it not in list_policy:
  356. list_policy.append(it.group(0))
  357. ser = re.search('地[址点][:为](?P<addr>([\w()()【】]{2,25}([省市县区州旗]|采购网|平台|公司)[\w()()【】-]{,60}))[,。]', addr_bidopen_text)
  358. addr_bidopen_text = ser.group('addr') if ser else ''
  359. ser = re.search('地[址点][:为](?P<addr>([\w()()【】]{2,25}([省市县区州旗]|采购网|平台|公司)[\w()()【】-]{,60}))[,。]', addr_bidsend_text)
  360. addr_bidsend_text = ser.group('addr') if ser else ''
  361. if re.search('开启', addr_bidopen_text) and re.search('时间:\d{2,4}年\d{1,2}月\d{1,2}日', addr_bidopen_text) and len(addr_bidopen_text)<40: # 优化类似 364991684只有时间没地址情况
  362. addr_bidopen_text = ""
  363. ser = re.search(pinmu_name_pattern, pinmu_name)
  364. if ser:
  365. pinmu_name = pinmu_name[ser.end():]
  366. if re.search('[^\w]$', pinmu_name):
  367. pinmu_name = pinmu_name[:-1]
  368. if len(out_lines) < 3: # 小于三个的大纲去掉
  369. out_lines = []
  370. # else:
  371. # text_, title_type, title_index, next_index, scope = out_lines[-1]
  372. # if scope[0][0] < scope[1][0]:# 最后一个大纲范围取当句,避免错误
  373. # out_lines[-1] = (text_, title_type, title_index, next_index,(scope[0], (scope[0][0]+1, 0)))
  374. return requirement_text, aptitude_text, addr_bidopen_text, addr_bidsend_text, out_lines, requirement_scope, pinmu_name, list_policy, winter_scope,correction_content
  375. def extract_addr(content):
  376. '''
  377. 通过正则提取地址
  378. :param content: 公告预处理后文本
  379. :return:
  380. '''
  381. addr_bidopen_text = ''
  382. ser = re.search('([开评]标|开启|评选|比选|磋商|遴选|寻源|采购|招标|竞价|议价|委托|询比?价|比价|谈判|邀标|邀请|洽谈|约谈|选取|抽取|抽选|递交\w{,4}文件))?(会议)?地[点址]([((]网址[))])?[:为][^,;。]{2,100}[,;。]', content)
  383. if ser:
  384. addr_bidopen_text = ser.group(0)
  385. return addr_bidopen_text