table_prem.py 76 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232
  1. # -*- coding: utf-8 -*-
  2. """表格要素 / 候选人 / 表头识别提取器。
  3. 按 ARCHITECTURE.md Phase 5 拆分建议,从 ``interface/predictor.py`` 迁出以下
  4. 表格相关类与函数:
  5. - ``TableTag2List`` — soup table 转列表并补全 span(原 8033-8192 行)
  6. - ``is_head_line`` — 调用表头识别模型判断是否为表头行(原 8195-8211 行)
  7. - ``TablePremExtractor`` — 表格要素(标段/招标人/中标人/金额)提取(原 8213-8754 行)
  8. - ``CandidateExtractor`` — 表格候选人提取(原 8756-9183 行)
  9. - ``get_header_line`` — 判断列表内文本哪些是表头(原 9298-9321 行)
  10. 原 ``from common.Utils import *`` / ``from interface.modelFactory import *``
  11. 已替换为显式 import;``os.path.dirname(__file__)`` 路径引用替换为
  12. ``predictors._common.INTERFACE_DIR``。
  13. ``interface/predictor.py`` 仍保留原定义,老 import 不受影响。
  14. """
  15. from __future__ import absolute_import
  16. import re
  17. import pickle
  18. import numpy as np
  19. import pandas as pd
  20. from bs4 import BeautifulSoup
  21. from BiddingKG.dl.common.logging import log
  22. from BiddingKG.dl.common.context_utils import (
  23. find_package,
  24. uniform_package_name,
  25. money_process,
  26. cut_repeat_name,
  27. )
  28. from BiddingKG.dl.common.Utils import del_tabel_achievement
  29. from BiddingKG.dl.common.nerUtils import getNers
  30. from BiddingKG.dl.foolnltk import selffool
  31. from BiddingKG.dl.time.re_servicetime import extract_servicetime
  32. from BiddingKG.dl.common.attr_utils import extract_serviceTime
  33. from BiddingKG.dl.interface.predictor import getPredictor
  34. from BiddingKG.dl.predictors._common import (
  35. INTERFACE_DIR,
  36. header_set,
  37. get_td_companys,
  38. get_role,
  39. )
  40. __all__ = [
  41. "TableTag2List",
  42. "is_head_line",
  43. "TablePremExtractor",
  44. "CandidateExtractor",
  45. "get_header_line",
  46. ]
  47. class TableTag2List():
  48. '''把soup table 转化为表格补全后的文本列表[[td, td, td], [td, td, td]]'''
  49. def table2list(self, table, text_process=None, return_html_table=False,return_kv=False):
  50. '''
  51. 表格补全及把表格内容列表返回
  52. :param table:
  53. :param text_process: 预处理方法,segment(),不为None 时把td内容做预处理,结果返回加标签,适配表头识别 [[[text, 0], [text, 0]] ], 否则只返回文本[[text, text], [text, text]]
  54. :param return_html_table:
  55. :param return_kv:
  56. :return:
  57. '''
  58. self._output = []
  59. row_ind = 0
  60. col_ind = 0
  61. html_table = []
  62. for row in table.find_all('tr'):
  63. # record the smallest row_span, so that we know how many rows
  64. # we should skip
  65. smallest_row_span = 1
  66. if len(row.find_all(['td', 'th'], recursive=False)) > 20:
  67. log('未补全前表格列数大于20的不做表格处理')
  68. if return_html_table:
  69. return [], []
  70. return []
  71. for cell in row.children:
  72. if cell.name in ('td', 'th'):
  73. # check multiple rows
  74. # pdb.set_trace()
  75. row_span = int(re.sub('[^0-9]', '', cell.get('rowspan'))) if cell.get('rowspan') and cell.get('rowspan').isdigit() else 1
  76. if row_span == 0: # 20250806 修复 659303627 附件OCR重构表格span为0导致缺少问题
  77. row_span = 1
  78. # try updating smallest_row_span
  79. smallest_row_span = min(smallest_row_span, row_span)
  80. # check multiple columns
  81. col_span = int(re.sub('[^0-9]', '', cell.get('colspan'))) if cell.get('colspan') and cell.get('colspan').isdigit() else 1
  82. if col_span > 20: # 修复 335590254 山东港口阳光智采e平台 数据源表格第一行colspan为200超过50列造成无法提取问题
  83. col_span = 20
  84. elif col_span == 0: # 20250806 修复 659303627 附件OCR重构表格span为0导致缺少问题
  85. col_span = 1
  86. # find the right index
  87. while True:
  88. if self._check_cell_validity(row_ind, col_ind):
  89. break
  90. col_ind += 1
  91. # insert into self._output
  92. try:
  93. if 'title' in cell.attrs and cell.get_text().replace(' ', '').endswith(
  94. '...') and cell.attrs['title'].replace(' ', '').startswith(cell.get_text().replace(' ', '')[:-3]):
  95. cell.string = cell.attrs['title'] # 修复 类似 215597851 215597851 省略号隐藏内容
  96. if text_process != None:
  97. # text = [re.sub('\xa0', '', text_process(cell, final=False)), 0]
  98. # td_text = re.sub('\xa0', '', text_process(cell, final=False))
  99. td_text = re.sub('\s|\xa0', '', str(cell.get_text())) # 修复 370835008 td 内公司被p标签拆分为两半情况
  100. if len(td_text)>30:
  101. if return_kv:
  102. td_text = cell.get_text().strip()
  103. else:
  104. td_text = re.sub('\xa0', '', text_process(cell, final=False))
  105. if td_text == "":
  106. td_text = ' '
  107. text = [td_text,0]
  108. else:
  109. if return_kv:
  110. td_text = cell.get_text().strip()
  111. else:
  112. td_text = str(cell.get_text()).strip().replace("\x06", "").replace("\x05", "").replace("\x07", "").replace('\\', '').replace("(", "(").replace(')', ')').replace('?', '').replace('&nbsp', '')
  113. td_text = re.sub('\s+', ' ', td_text)
  114. text = td_text
  115. # text = str(cell.get_text()).strip().replace("\x06", "").replace("\x05", "").replace("\x07", "").replace('\\', '').replace("(", "(").replace(')', ')').replace('?', '')
  116. # # text = re.sub('\s', '', text)[:200] # 只需取前200字即可
  117. # text = ' ' if text == "" else text
  118. self._insert(row_ind, col_ind, row_span, col_span, text)
  119. if return_html_table:
  120. html_table = self._insert_new(row_ind, col_ind, row_span, col_span, str(cell), html_table)
  121. except UnicodeEncodeError:
  122. raise Exception( 'Failed to decode text; you might want to specify kwargs transformer=unicode' )
  123. # update col_ind
  124. col_ind += col_span
  125. if col_ind > 50 and text_process == None: # 表格要素提取及候选人提取的 表格列数大于50的去掉
  126. if return_html_table:
  127. return [], []
  128. return []
  129. # update row_ind
  130. row_ind += smallest_row_span
  131. col_ind = 0
  132. if return_html_table:
  133. temp_list = []
  134. for row in self._output:
  135. if len(row) > 0:
  136. temp_list.append(row)
  137. self._output = temp_list
  138. temp_list = []
  139. for row in html_table:
  140. if len(row) > 0:
  141. temp_list.append(row)
  142. html_table = temp_list
  143. return self._output, html_table
  144. else:
  145. return self._output
  146. def _check_validity(self, i, j, height, width):
  147. """
  148. check if a rectangle (i, j, height, width) can be put into self.output
  149. """
  150. return all(self._check_cell_validity(ii, jj) for ii in range(i, i+height) for jj in range(j, j+width))
  151. def _check_cell_validity(self, i, j):
  152. """
  153. check if a cell (i, j) can be put into self._output
  154. """
  155. if i >= len(self._output):
  156. return True
  157. if j >= len(self._output[i]):
  158. return True
  159. if self._output[i][j] == "":
  160. return True
  161. return False
  162. def _insert(self, i, j, height, width, val):
  163. # pdb.set_trace()
  164. for ii in range(i, i+height):
  165. for jj in range(j, j+width):
  166. self._insert_cell(ii, jj, val)
  167. def _insert_cell(self, i, j, val):
  168. while i >= len(self._output):
  169. self._output.append([])
  170. while j >= len(self._output[i]):
  171. self._output[i].append("")
  172. if self._output[i][j] == "":
  173. self._output[i][j] = val
  174. def _insert_new(self, i, j, height, width, val, cell_list):
  175. # pdb.set_trace()
  176. for ii in range(i, i+height):
  177. for jj in range(j, j+width):
  178. cell_list = self._insert_cell_new(ii, jj, val, cell_list)
  179. return cell_list
  180. def _insert_cell_new(self, i, j, val, cell_list):
  181. while i >= len(cell_list):
  182. cell_list.append([])
  183. while j >= len(cell_list[i]):
  184. cell_list[i].append("")
  185. if cell_list[i][j] == "":
  186. cell_list[i][j] = val
  187. return cell_list
  188. def is_head_line(list_item):
  189. '''
  190. 调用表头识别模型判断是否为表头行
  191. :param list_item: 行内容 例: ['技术参数、要求', '变更项']
  192. :return:
  193. '''
  194. x = []
  195. for item in list_item:
  196. x.append(getPredictor("form").encode(item))
  197. predict_y = getPredictor("form").predict(np.array(x), type="item")
  198. count = 0
  199. for item, values in zip(list_item, list(predict_y)):
  200. if values[1] > 0.6:
  201. count += 1
  202. if count/len(list_item)>0.6:
  203. return True
  204. return False
  205. class TablePremExtractor(object):
  206. def __init__(self):
  207. '''各要素表头规则'''
  208. self.head_rule_dic = {
  209. 'project_code': "(项目|招标|采购|计划|公告|包[段组件]|标[段包的]|标段(包)|分[包标])(编号|编码|代码)",
  210. 'package_code': "(包[段组件]|标[段包]|分[包标])(序?号|$)|包号|^标段$|^品目$",
  211. "project_name": "(包[段组件]|标[段包的项]|标段(包)|分[包标]|采购|项目|工程|^包)(名称?|内容)", # |货物|商品|产品|设备|通用|主要标的 20250812 货物的不作为项目名称
  212. "win_sort": "排名|排序|名次|推荐顺序",
  213. 'win_or_not': '是否(建议|推荐)?(中标|成交|中选)|是否入围|是否入库|入围结论|未(中标|成交)原因|中标情况|^中标结果$',
  214. "tenderer": "(中标|中选|中价|中拍|成交|供货|承包|承建|承租|竞得|受让))?(候选)?(人(单位)?|单位|供应商|公司|企业|厂家|商家?|客户|供?方|银行|回收商)(名称|$)|^(拟定|单一来源|邀请|拟?推荐(入选|入围)?)?供应商(名称)?$",
  215. "tenderee": "(项目|采购|招标|遴选|寻源|竞价|议价|比选|委托|询比?价|比价|评选|谈判|邀标|邀请|洽谈|约谈|选取|抽取|抽选)(人|公司|单位|组织|用户|业主|主体|方|部门)(名称|$)",
  216. "budget": "最高(投标)?限价|总价限价|控制(价格?|金额|总价)|(总价|采购)限价|上限价|拦标价|(采购|招标|项目)?预算|(预算|招标|采购|计划)金额|挂牌价",
  217. "bid_amount": "投标[报总]?价|报价(总?金额|总价|总额)|总报价|^\w{,5}报价(([\w、/]{1,15}))?$|(中标|中选|中价|中拍|成交|合同))?总?(金?额|[报均总]价|价[格款]?)|承包价|含税价|经评审的价格|中标存款金?额|中标资金|中标存款|存放金额|分配额度",
  218. "serviceTime": '合同期限|工期/交货期/服务期|工期\(交货期\)|合格工期|服务期限|工期' \
  219. '|工期要求|项目周期|工期\(交货期\)|计划工期\(服务期限\)|服务时限|履行期限|服务周期|供货期限' \
  220. '|合格工期|计划工期\(服务期\)|服务期|服务,期|交货\(完工\)时间|交付\(服务、完工\)时间' \
  221. '|交货时间|保洁期限|维保期|管理年限|工期承诺|(服务|合同|施工|实施|工程|设计)的?(年限|期限|周期|期:)' \
  222. '|计划工期|工期要求|服务期限?' \
  223. '|投标工期|设计工期|合格服务周期|总工期|服务时间(范围)?|流转期限|维护期限|服务时限|交货期' \
  224. '|完成时间|中标工期|项目周期|期限要求|周期|供货期|合同的?履行日期|计划周期' \
  225. '|履约期限|合同的?约定完成时限|合同的?完成日期|承诺完成日期' \
  226. '|合同起始日起|合同的?履约期|履约截止日期|承包期限|合同的?完成日期|特许经营期限' \
  227. '|服务期间|服务履行期|委托(管理)?期限|经营期限|数量' \
  228. '|(工期|服务期限?|交货期限?|服务履行期|合同期限?|履[行约]期限?)说明|存款期限?|(存款|存放|定存)(期|年)限' \
  229. '|服务(有效期|年限)|本?合同有效期|协议有效期|项目期限'
  230. }
  231. with open(INTERFACE_DIR+'/header_set.pkl', 'rb') as f:
  232. self.headerset = pickle.load(f)
  233. self.tb = TableTag2List()
  234. def find_header(self, td_list, all_winner=False, first_line=False):
  235. fix_td_list = [re.sub('[::]$|^[一二三四五六七八九十0-9]{1,3}、|(([\w、×*/]{1,20}))$|(不?含税)|/万?元|拟|\s', '', it) for it in td_list] # 去除表头无关信息,方便匹配判断是否为表头
  236. header_dic = dict()
  237. flag = False
  238. contain_header = False
  239. not_sure_winner = False # 是否 不确定中标的中标人表达方式
  240. for text in set(fix_td_list) - self.headerset:
  241. if len(text)<10 and re.search(self.head_rule_dic['bid_amount'], text):
  242. self.headerset.add(text)
  243. if len(set(fix_td_list))>0 and (first_line or len(set(fix_td_list) & self.headerset)>=2) and (len(set(fix_td_list) & self.headerset)/len(set(fix_td_list))>=0.6 or is_head_line(fix_td_list)):
  244. other_tenderer = ""
  245. other_tenderer2 = ""
  246. flag = True
  247. for i in range(len(td_list)) :
  248. text = td_list[i]
  249. text = re.sub('\s|[((]排名不分先后[))]', '', text)
  250. text = re.sub('排名价', '', text) # 20241225 修复 252208201 排名价(元)错误为排名
  251. text = re.sub('^人选', '入选', text)
  252. if text == '备选中标人':
  253. text = '第二候选人'
  254. if len(re.sub('(([\w、×*/]{1,20}))$', '', text)) > 15: # 长度大于15 不进行表头匹配
  255. continue
  256. if re.search('未(中标|成交|中选|入围|通过)原因', text): # 不提取此种表格 673143737
  257. return flag, contain_header, dict(), not_sure_winner
  258. num = 0
  259. for k, v in self.head_rule_dic.items():
  260. if re.search('评分|得分|分数|分值', text):
  261. continue
  262. if re.search(v, text):
  263. if k in ['tenderer'] and re.search('是否|未', text): # 修复103367444 未中标单位名称 作为中标
  264. continue
  265. if k == 'budget' and re.search('量', text): # 预算工作量 预算采购量 等不作为预算
  266. continue
  267. elif k == 'bid_amount' and re.search('分配方案|基准利率|BP值', text): # 517987084 中标资金分配方案
  268. continue
  269. elif k in header_dic:
  270. if k in ['budget', 'bid_amount'] and re.search('总(价|金?额)', text): # 总价替换单价
  271. header_dic[k] = (i, text)
  272. num += 1
  273. elif k == 'project_code' and text != header_dic[k][1] and 'package_code' not in header_dic\
  274. and re.search(self.head_rule_dic['package_code'], re.sub('\s', '', ','.join(td_list)))==None: # 如果出现两次项目编号且没有包号,把第二次出现的作为包号 例:472537470
  275. header_dic['package_code'] = (i, text)
  276. continue
  277. header_dic[k] = (i, text)
  278. num += 1
  279. elif re.search('^((中标|成交|中选|入围|入选)(候选)?)(人|单位|机构|中介(服务)?机构|供应商|客户|方|公司|厂商|商家?|社会资本方?|银行)(名称)?$', text) and re.search('未', text)==None:
  280. other_tenderer = (i, text)
  281. elif re.search('^((投标|应答|响应|候选)(人|单位|机构|中介(服务)?机构|供应商|客户|方|公司|厂商|商家?|社会资本方?|银行)|(存款|投标)?银行|供应商)(名称)?$|^机构名称$|^单位(名称)?$', text) and re.search('未', text)==None:
  282. other_tenderer2 = (i, text)
  283. if num>1:
  284. if re.search(self.head_rule_dic['project_code'], text) and re.search(self.head_rule_dic['package_code'], text): # 修复 528486798 分标编号-包号
  285. continue
  286. # print('表头错误,一个td匹配到两个表头:', header_dic)
  287. return flag, contain_header, dict(), not_sure_winner
  288. if text == '单位': # 20241128 补充金额单位
  289. header_dic['amount_unit'] = (i, text)
  290. if re.search(';金额((万?元))?;', ';'.join(td_list)): # 召回某些表格只写 金额 作为表头,不能识别为招标或中标金额
  291. if 'tenderer' in header_dic and 'bid_amount' not in header_dic:
  292. for i in range(len(td_list)):
  293. text = td_list[i]
  294. if re.search('^金额((万?元))?$',text):
  295. header_dic['bid_amount'] = (i, text)
  296. break
  297. elif 'tenderee' in header_dic and 'budget' not in header_dic:
  298. for i in range(len(td_list)):
  299. text = td_list[i]
  300. if re.search('^金额((万?元))?$', text):
  301. header_dic['budget'] = (i, text)
  302. break
  303. if all_winner == 1 and 'tenderer' not in header_dic: # 标题有存款、入库、入围等公告补充其他表达做中标人
  304. if other_tenderer!="":
  305. header_dic['tenderer'] = other_tenderer
  306. elif other_tenderer2!="":
  307. header_dic['tenderer'] = other_tenderer2
  308. if 'win_sort' not in header_dic:
  309. not_sure_winner = True
  310. elif 'tenderer' not in header_dic and 'win_or_not' in header_dic:
  311. if other_tenderer!="":
  312. header_dic['tenderer'] = other_tenderer
  313. # elif other_tenderer2!="": # 20260115 注释 723429011 单位 是否中标 不是中标人
  314. # header_dic['tenderer'] = other_tenderer2
  315. if all_winner == 1 and 'win_sort' in header_dic: # 标题有存管类公告不分排名
  316. header_dic.pop('win_sort')
  317. if 'tenderer' in header_dic and re.search('候选|入围|入选', header_dic['tenderer'][1]) and re.search('推荐的?((中标|成交|中选)候选人|(候选|入围|入选)供应商)', header_dic['tenderer'][1])==None and all_winner == False:
  318. header_dic.pop('tenderer')
  319. if ('project_code' in header_dic or 'package_code' in header_dic or 'project_name' in header_dic) and (
  320. 'tenderer' in header_dic): # 包含标段及招标金额或中标人的进行提取 # or'budget' in header_dic 20250919 改为有中标人的才提取,只有预算的容易与非表格提取重复 例子:647543768
  321. return flag, contain_header, header_dic, not_sure_winner
  322. elif ('tenderer' in header_dic) and ('bid_amount' in header_dic): # 包含中标人及中标金额的进行提取
  323. if 'win_sort' in header_dic: # 有排名的 用候选人提取类
  324. return flag, contain_header, dict(), not_sure_winner
  325. elif re.search('^(候选)?供应商(名称)?', header_dic['tenderer'][1]) and 'win_or_not' not in header_dic and re.search('(中标|成交|合同))?总?(金?额|[报均总]价|价[格款]?)', header_dic['bid_amount'][1])==None: # 只有供应商名称 没排名和包号的去掉,预防错误包提取 334205629
  326. # print('只有供应商名称 没排名和包号的去掉')
  327. return flag, contain_header, dict(), not_sure_winner
  328. return flag,contain_header, header_dic, not_sure_winner
  329. elif 'tenderer' in header_dic and (re.search('(中标|中选|中价|成交|竞得)(人|单位|供应商|公司|企业|厂家|商家?|客户|供?方|银行)',header_dic['tenderer'][1]) or all_winner): # 有中标人,且有明确中标关键词的进行提取
  330. return flag, contain_header, header_dic, not_sure_winner
  331. # elif 'tenderer' in header_dic and 'serviceTime' in header_dic:
  332. # return flag, contain_header, header_dic, not_sure_winner
  333. elif len(set(fix_td_list) & self.headerset) >= 2 or (len(set(fix_td_list)) == 2 and len(set(td_list) & self.headerset) >= 1): # 如果包含两个表头以上或 只有两列且包含一个表头
  334. contain_header = True
  335. return flag, contain_header, dict(), not_sure_winner
  336. def extract_from_df(self, df, headers, web_source_name, all_winner=False):
  337. prem_dic = {}
  338. previous_package = "" # 上一行包号
  339. previous_project_name = "" # 上一行项目名称
  340. multi_same_package = False # 非连续的重复包号
  341. package_fix2raw = dict() # 处理后包号:处理前包号 字典
  342. link_set = set()
  343. tenderer_list = [] # 保存所有中标人
  344. serviceTime_list = []
  345. not_package = True if 'project_name' in headers and re.search('(货物|商品|产品|通用|主要标的)(名称?|内容)', headers['project_name'][1]) and \
  346. 'package_code' not in headers and 'budget' not in headers and "bid_amount" not in headers else False
  347. if set(['project_code', 'package_code', 'tenderee', 'tenderer']) & set(headers) == set() and ('project_name' not in headers # 补充没有项目名称或有项目名称且是货物的才过滤掉
  348. or re.search('(货物|商品|产品|设备|通用|主要标的)(名称?|内容)', headers['project_name'][1])): # 20240131修复只有货物名称及最高限价的错误作为多包 396636683; 补充避免423647863采购意向被过滤
  349. # print('没有包号及角色的不要')
  350. return {}
  351. have_bid_amount = False # 是否包含中标金额
  352. if "bid_amount" in headers and re.search('[1-9]+', '#'.join([it.strip() for it in df[headers['bid_amount'][0]]])):
  353. have_bid_amount = True
  354. for i in df.index:
  355. same_package = False # 连续重复包号,一般是 rowspan 造成;一包 多个采购
  356. project_code = df.loc[i, headers['project_code'][0]].strip().replace(' ', '') if "project_code" in headers else ""
  357. package_code_raw = df.loc[i, headers['package_code'][0]].strip().replace(' ', '') if "package_code" in headers else ""
  358. project_name = df.loc[i, headers['project_name'][0]].strip().replace(' ', '') if "project_name" in headers else ""
  359. tenderee = df.loc[i, headers['tenderee'][0]].strip().replace(' ', '') if "tenderee" in headers else ""
  360. tenderer = df.loc[i, headers['tenderer'][0]].strip().replace(' ', '') if "tenderer" in headers else ""
  361. budget_ = df.loc[i, headers['budget'][0]].strip().replace(' ', '') if "budget" in headers else ""
  362. bid_amount_ = df.loc[i, headers['bid_amount'][0]].strip().replace(' ', '') if "bid_amount" in headers else ""
  363. win_sort = df.loc[i, headers['win_sort'][0]].strip().replace(' ', '') if "win_sort" in headers else ""
  364. win_or_not = df.loc[i, headers['win_or_not'][0]].strip().replace(' ', '') if "win_or_not" in headers else ""
  365. serviceTime = df.loc[i, headers['serviceTime'][0]].strip().replace(' ', '') if "serviceTime" in headers else ""
  366. amount_unit = df.loc[i, headers['amount_unit'][0]].strip().replace(' ', '') if "amount_unit" in headers else ""
  367. joint = ''
  368. if set([project_code, package_code_raw, project_name,tenderee,tenderer,budget_,bid_amount_]) & self.headerset != set(): # 只要有一项为表头 停止匹配
  369. # print('只要有一项为表头 停止匹配', set([project_code, package_code_raw, project_name,tenderee,tenderer,budget_,bid_amount_,win_sort]) & self.headerset)
  370. break
  371. if len(set([project_code, package_code_raw, project_name,tenderee,tenderer,budget_,bid_amount_,win_sort])- set(['', ' '])) < 2 and tenderer=='': # 内容为空或全部一样 停止匹配
  372. # print('内容为空或全部一样 停止匹配')
  373. break
  374. if re.search('详见', project_name): # 去除某些表达: 详见招标文件
  375. project_name = ""
  376. if package_code_raw == "" and re.search('第?[0-9一二三四五六七八九十a-zA-Z]{1,4}(标[段号的包项]|([分子]?包|包[组件号]))$|^(标[段号的包项]|([分子]?包|包[组件号]))号?:?[0-9一二三四五六七八九十a-zA-Z]{1,4}$', project_name):
  377. package_code_raw = project_name
  378. project_name = ""
  379. package_code = package_code_raw
  380. if re.search('合计|总计', package_code+project_code+project_name):
  381. continue
  382. if package_code + project_code == previous_package: # 处理 208162730 一个包采购多种东西情况
  383. if project_name == previous_project_name:
  384. same_package = True
  385. if previous_package!="": # 有包号或项目编号且跟上一行相同时,去除项目名称
  386. project_name = ''
  387. if win_sort != "" and re.search('排名|排序|名次|推荐顺序', headers['win_sort'][1]): # 此类型表由 CandidateExtractor类提取 防止类似 328485591 作为多包
  388. break
  389. if win_or_not != "" and (re.search('(建议|推荐)(中标|成交|中选)|是|^(中标|成交|中选)', win_or_not)==None or re.search('\w', win_or_not)==None): # 2024/04/2 修复 252208201 为空的不中标
  390. continue
  391. elif 'win_or_not' in headers and win_or_not == '': # 2024/12/25 修复 334753545 中标情况为空的不中标
  392. continue
  393. if "win_sort" in headers and win_sort == "": # '表头有是否中标,内容却空白的,过滤掉'
  394. continue
  395. if win_sort == "" and "tenderer" in headers and re.search('候选|入围|入选', headers['tenderer'][1]) and re.search('推荐的?((中标|成交|中选)候选人|(候选|入围|入选)供应商)', headers['tenderer'][1])==None and all_winner == False:
  396. tenderer = ""
  397. if tenderer in ['采购失败', '废标']: # 避免类似 353867205 这篇只提取到一个
  398. continue
  399. # tenderee = tenderee if self.is_role(tenderee) else ""
  400. # tenderer = tenderer if self.is_role(tenderer) else ""
  401. package = uniform_package_name(package_code) if package_code else '自增1' # 没有包号的自动编号的修改为提取到多少个包,某些行未必中标
  402. if package == '自增1' and project_name != '' and 'project_name' in headers and re.search('包[段组件]|标[段包的项]|标段(包)|分[包标]', headers['project_name'][1]): # 避免 266252226 采购项目名称 做多包多招标人 修复 661973750 标段名称为多包
  403. package = "自增%s"%str(len(prem_dic) + 1) if previous_project_name != project_name else "自增%s"%str(len(prem_dic))
  404. if project_name != "" and package.startswith('自增'):
  405. pk_l = find_package(project_name)
  406. if len(pk_l)==1:
  407. package = uniform_package_name(pk_l[0].group(0))
  408. elif re.search('[一二三四五六七八九十]+标段:|标段[一二三四五六七八九十]+:', tenderer) and package.startswith('自增'):
  409. pk_l = find_package(tenderer)
  410. if len(pk_l) == 1:
  411. package = uniform_package_name(pk_l[0].group(0))
  412. tenderee = get_role(tenderee, self.nlp_enterprise) if tenderee!="" else tenderee
  413. # tenderer = get_role(tenderer, self.nlp_enterprise) if tenderer!='' else tenderer
  414. if tenderer!='':
  415. leader, joint = get_td_companys(tenderer, self.nlp_enterprise)
  416. if leader == '':
  417. log('表格角色内容不在公司列表:%s,docid:%s' % (tenderer, self.docid))
  418. tenderer = leader
  419. tenderee = cut_repeat_name(tenderee)
  420. tenderer = cut_repeat_name(tenderer)
  421. if len(set([project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_])) < 2:
  422. break
  423. if not_package:
  424. if (project_code, package_code, tenderee, tenderer, budget_, bid_amount_) in link_set:
  425. continue
  426. link_set.add((project_code, package_code, tenderee, tenderer, budget_, bid_amount_))
  427. else:
  428. if (project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_) in link_set:
  429. continue
  430. link_set.add((project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_))
  431. if project_code != "":
  432. uni_project_code= uniform_package_name(project_code)
  433. if uni_project_code != "" and uni_project_code!=package:
  434. if package.startswith('自增'): # 没有包号有项目编号的,直接用项目编号
  435. package = uni_project_code
  436. else:
  437. # print('重组包号:', '%s_%s'%(uni_project_code, package))
  438. package = '%s_%s'%(uni_project_code, package) # 同时包号项目编号及包号的,组合起来做包号
  439. if package_code_raw!='':
  440. if multi_same_package == False and package not in package_fix2raw: # 如果处理后的标段号 已经在列表里面,采用原始标段号文本
  441. package_fix2raw[package] = package_code_raw
  442. elif same_package == False:
  443. multi_same_package = True
  444. if multi_same_package:
  445. package = package_code_raw
  446. if package not in prem_dic or not same_package:
  447. prem_dic[package] = {
  448. 'code': '',
  449. 'name': '',
  450. 'roleList': [],
  451. 'tendereeMoney': 0,
  452. 'tendereeMoneyUnit': ""
  453. }
  454. prem_dic[package]['code'] = project_code
  455. prem_dic[package]['name'] = project_name
  456. if budget_ != "":
  457. if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税|(六个月|一年|\w{2,3})期加点\d+BP', '', budget_)) > 5: # 金额字段出现超过5个非金额字符,中断匹配
  458. prem_dic.pop(package)
  459. break
  460. budget_header = headers['budget'][1] if 'budget' in headers else ''
  461. if amount_unit!='' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and re.search('元', budget_+budget_header)==None : # 20241128 补充某些表格价格单位分开两列, 例:557953660
  462. budget_ += amount_unit
  463. budget, money_unit = money_process(budget_, budget_header)
  464. if re.search('元[/每]', amount_unit) or re.search('单价', budget_header):
  465. unit_tendereeMoney = budget
  466. budget = 0
  467. else:
  468. unit_tendereeMoney = 0
  469. if (re.search('费率|[上下]浮率|[%%‰折]|优惠率|期加点\d+BP',
  470. budget_header + budget_) and budget < 100) or budget > 50000000000: # 如果是费率或大于500亿的金额改为0
  471. budget = 0
  472. if budget > 0:
  473. if same_package and prem_dic[package]['tendereeMoney'] != budget: # 处理 类似 136839070 一包多物品多预算
  474. prem_dic[package]['tendereeMoney'] += budget
  475. else:
  476. prem_dic[package]['tendereeMoney'] = budget
  477. prem_dic[package]['tendereeMoneyUnit'] = money_unit
  478. if unit_tendereeMoney > 0:
  479. if 'unit_tendereeMoney' not in prem_dic[package]:
  480. prem_dic[package]['unit_tendereeMoney'] = 0
  481. if same_package and prem_dic[package]['unit_tendereeMoney'] != unit_tendereeMoney: # 处理 类似 136839070 一包多物品多预算
  482. prem_dic[package]['unit_tendereeMoney'] += unit_tendereeMoney
  483. else:
  484. prem_dic[package]['unit_tendereeMoney'] = unit_tendereeMoney
  485. if tenderee and not same_package:
  486. prem_dic[package]['roleList'].append({
  487. "address": "",
  488. "linklist": [],
  489. "role_money": {
  490. "discount_ratio": "",
  491. "downward_floating_ratio": "",
  492. "floating_ratio": "",
  493. "money": 0,
  494. "money_unit": ""
  495. },
  496. "role_name": "tenderee",
  497. "role_text": tenderee,
  498. "serviceTime": ""
  499. })
  500. if tenderer:
  501. if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税|(六个月|一年|\w{2,3})期加点\d+BP', '',
  502. bid_amount_)) > 5: # 金额字段出现超过5个非金额字符,中断匹配
  503. prem_dic.pop(package)
  504. break
  505. bid_amount_header = headers['bid_amount'][1] if bid_amount_ != "" else ''
  506. if amount_unit != '' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and bid_amount_!='' and re.search('元',
  507. bid_amount_ + bid_amount_header) == None:
  508. bid_amount_ += amount_unit
  509. bid_amount, money_unit = money_process(bid_amount_, headers['bid_amount'][1]) if bid_amount_ != "" and 'bid_amount' in headers else (0, '')
  510. if re.search('[%%‰折]|浮率|期加点\d+BP', bid_amount_) and 0 < bid_amount < 100:
  511. bid_amount = 0
  512. if re.search('元[/每]', amount_unit) or re.search('单价', bid_amount_header):
  513. unit_price = bid_amount
  514. bid_amount = 0
  515. else:
  516. unit_price = 0
  517. if web_source_name == '河钢供应链管理平台' and 'bid_amount' in headers and re.search('[%%‰折]|浮率', bid_amount_) == None and bid_amount == 0: # 有中标金额字段却金额为0的过滤掉,防止类似 河钢供应链管理平台 站源错误,金额不为0的才算中标
  518. if len(prem_dic[package]['roleList']) == 0 and prem_dic[package]['tendereeMoney'] == 0: # 只有项目编号和名称的包 丢弃
  519. prem_dic.pop(package)
  520. continue
  521. elif 'bid_amount' in headers and re.search('[%%‰折]|浮率', bid_amount_) == None and have_bid_amount and bid_amount_ in ['/','','0','0.0']: # 如果不是所有行中标金额都为0,则把为0的做非中标
  522. if len(prem_dic[package]['roleList']) == 0 and prem_dic[package]['tendereeMoney'] == 0: # 只有项目编号和名称的包 丢弃
  523. prem_dic.pop(package)
  524. continue
  525. if (re.search('费率|下浮率|[%%‰折]|优惠率',
  526. bid_amount_header + bid_amount_) and bid_amount < 100) or bid_amount > 50000000000: # 如果是费率或大于500亿的金额改为0
  527. bid_amount = 0
  528. if serviceTime:
  529. serviceTime_text = headers['serviceTime'][1] + serviceTime if headers['serviceTime'][1][-1] in [':',':'] else headers['serviceTime'][1] + ':' + serviceTime
  530. # print('serviceTime_text',serviceTime_text)
  531. serviceTime = extract_servicetime(serviceTime_text)
  532. serviceTime.sort(key=lambda x:x.get('begin_index',0))
  533. serviceTime = extract_serviceTime(serviceTime[0]['body'],"") if serviceTime else ""
  534. # print(serviceTime)
  535. if not same_package or len(prem_dic[package]['roleList'])==0:
  536. role_dic = {
  537. "address": "",
  538. "linklist": [],
  539. "role_money": {
  540. "discount_ratio": "",
  541. "downward_floating_ratio": "",
  542. "floating_ratio": "",
  543. "money": bid_amount,
  544. "money_unit": money_unit
  545. },
  546. "role_name": "win_tenderer",
  547. "role_text": tenderer,
  548. "serviceTime": serviceTime
  549. }
  550. if joint != '':
  551. role_dic['win_tenderer_joint'] = joint
  552. if unit_price > 0:
  553. role_dic['role_money']['unit_price'] = unit_price
  554. prem_dic[package]['roleList'].append(role_dic)
  555. elif prem_dic[package]['roleList'] and prem_dic[package]['roleList'][-1].get('role_name', '')=='win_tenderer':
  556. if 'multi_winner' not in prem_dic[package]['roleList'][-1]:
  557. prem_dic[package]['roleList'][-1]['multi_winner'] = prem_dic[package]['roleList'][-1]['role_text']
  558. prem_dic[package]['roleList'][-1]['multi_winner'] += ','+ tenderer
  559. elif tenderer not in prem_dic[package]['roleList'][-1]['multi_winner']:
  560. prem_dic[package]['roleList'][-1]['multi_winner'] += ','+ tenderer
  561. if bid_amount != 0 or unit_price > 0: # 有中标金额的才放进去
  562. if 'other_winner_dic' not in prem_dic[package]['roleList'][-1]:
  563. prem_dic[package]['roleList'][-1]['other_winner_dic'] = []
  564. prem_dic[package]['roleList'][-1]['other_winner_dic'].append({'role_text': tenderer, "money": bid_amount, "money_unit": money_unit, "serviceTime": serviceTime})
  565. tenderer_list.append(tenderer)
  566. serviceTime_list.append(serviceTime)
  567. if len(prem_dic[package]['roleList']) == 0 and prem_dic[package]['tendereeMoney'] == 0: # 只有项目编号和名称的 丢弃 并不再继续往下匹配
  568. prem_dic.pop(package)
  569. # break # 注释掉避免 400084571 某些包废标 中断匹配
  570. if multi_same_package: # 预处理后包号重复的,使用原始包号
  571. for k, v in package_fix2raw.items():
  572. if k in prem_dic:
  573. prem_dic[v] = prem_dic.pop(k)
  574. previous_package = package_code + project_code
  575. previous_project_name = project_name
  576. if len(tenderer_list)>2 and len(set(tenderer_list))==1 and "package_code" not in headers: # 没提取到包号且中标人一样应该是错误多包,需去掉多包 例 244355092 281854766
  577. total_money = 0
  578. for v in prem_dic.values():
  579. for d in v['roleList']:
  580. if d['role_name'] == "win_tenderer":
  581. total_money += d['role_money']['money']
  582. if 'other_winner_dic' in d:
  583. for other in d['other_winner_dic']:
  584. total_money += other.get('money', 0)
  585. return {'自增1': {
  586. 'code': '',
  587. 'name': '',
  588. 'roleList': [{
  589. "address": "",
  590. "linklist": [],
  591. "role_money": {
  592. "discount_ratio": "",
  593. "downward_floating_ratio": "",
  594. "floating_ratio": "",
  595. "money": total_money,
  596. "money_unit": ''
  597. },
  598. "role_name": "win_tenderer",
  599. "role_text": tenderer_list[0],
  600. "serviceTime": serviceTime_list[0]
  601. }],
  602. 'tendereeMoney': 0,
  603. 'tendereeMoneyUnit': ""
  604. }}
  605. return prem_dic
  606. def update_prem(self, rs_dic, tmp_dic):
  607. '''
  608. 合并更新 prem
  609. :param rs_dic: 返回结果
  610. :param tmp_dic: 待合并结果
  611. :return:
  612. '''
  613. if '自增1' in tmp_dic and '自增1' not in rs_dic and len(tmp_dic)==len(rs_dic):
  614. pass
  615. else:
  616. for pack in tmp_dic:
  617. if pack in rs_dic:
  618. for k in tmp_dic[pack]:
  619. if rs_dic[pack][k] in ['', 0]:
  620. rs_dic[pack][k] = tmp_dic[pack][k]
  621. elif rs_dic[pack][k] == []:
  622. rs_dic[pack][k] = tmp_dic[pack][k]
  623. elif k == 'roleList' and len(rs_dic[pack][k])>0 and rs_dic[pack][k][0].get('role_money', {}).get('money', 0) == 0:
  624. rs_dic[pack][k] = tmp_dic[pack][k]
  625. else:
  626. rs_dic[pack] = tmp_dic[pack]
  627. def get_prem(self, soup, web_source_name='', all_winner=False):
  628. tables = soup.find_all('table')
  629. tables.reverse()
  630. rs_dic = {}
  631. for table in tables:
  632. text = table.text.strip()
  633. pre_text = ""
  634. previous = None
  635. if table.findPreviousSibling() != None:
  636. previous = table.findPreviousSibling()
  637. pre_text = previous.text.strip()
  638. if pre_text == "" and table.findPreviousSibling().findPreviousSibling() != None: # 修复表格前一标签没内容,再前一个才有内容情况
  639. previous = table.findPreviousSibling().findPreviousSibling()
  640. pre_text = previous.text.strip()
  641. if re.search('项目业主|业\s*主', text) and re.search('业\s*绩', text+pre_text): # 包含业绩的表格过滤掉,不进行处理
  642. tb_ex = table.extract()
  643. if previous:
  644. sib = previous.extract()
  645. continue
  646. trs = self.tb.table2list(table)
  647. # table.extract()
  648. i = 0
  649. headers = ""
  650. table_prem = {}
  651. while i < len(trs) - 1:
  652. flag_, contain_header_, headers_, not_sure_winner = self.find_header(trs[i], all_winner, first_line=i==0)
  653. if flag_ and 'tenderer' in headers_ and not_sure_winner and re.search('中标|成交|中选|入围|入选', pre_text)==None:
  654. # print('过滤:',headers_)
  655. flag_ = False
  656. headers_ = {}
  657. if flag_ and headers_ != dict():
  658. table_items = []
  659. headers = headers_
  660. for j in range(i + 1, len(trs)):
  661. if len(trs[j]) == len(trs[i]):
  662. flag_2, contain_header_2, headers_2, not_sure_winner = self.find_header(trs[j], all_winner)
  663. if flag_2 or contain_header_2:
  664. if j == i+1 and flag_2:
  665. if len(headers_)<=len(headers_2):
  666. headers = headers_2
  667. continue
  668. elif trs[i] == trs[j]: # 修复表格重复表头多次出现情况 例:514890585
  669. continue
  670. break
  671. elif ''.join(trs[j]).strip() == '': # 修复整行为空的 例:514890585
  672. continue
  673. else:
  674. table_items.append(trs[j])
  675. else:
  676. # print('表头,内容 列数不一致', len(trs[i]), len(trs[j]))
  677. break
  678. if len(table_items) > 0:
  679. df = pd.DataFrame(table_items)
  680. prem_ = self.extract_from_df(df, headers, web_source_name, all_winner)
  681. # rs_dic.update(prem_)
  682. # table_prem.update(prem_)
  683. self.update_prem(table_prem, prem_)
  684. i = j - 1
  685. i += 1
  686. if table_prem and 'project_code' not in headers and 'package_code' not in headers and '自增1' in table_prem and table.find_previous_sibling(): # 表格内没有标段的,从上一个兄弟标签找标段
  687. sib = table.find_previous_sibling()
  688. sib_text = sib.get_text()
  689. ser_sib = re.search('第?[0-9一二三四五六七八九十a-zA-Z]{1,4}(标[段号的包项]|([分子]?包|包[组件号]))|(标[段号的包项]|([分子]?包|包[组件号]))号?:?[0-9一二三四五六七八九十a-zA-Z]{1,4}|包名:[0-9一二三四五六七八九十]{1,4}', sib_text)
  690. if sib.name in ['p','div','dl','ol','ul','h1','h2','h3','h4','h5','h6'] and len(sib_text)<100 and ser_sib:
  691. package_sib = ser_sib.group(0)
  692. package_sib = uniform_package_name(package_sib)
  693. table_prem[package_sib] = table_prem.pop('自增1')
  694. if table_prem:
  695. # rs_dic.update(table_prem)
  696. self.update_prem(rs_dic, table_prem)
  697. table.extract()
  698. return rs_dic
  699. def predict(self, html, nlp_enterprise, web_source_name="", all_winner=False, docid=''):
  700. html = re.sub("<html>|</html>|<body>|</body>","",html)
  701. html = re.sub("##attachment##","",html)
  702. soup = BeautifulSoup(html, 'lxml')
  703. richText = soup.find(name='div', attrs={'class': 'richTextFetch'})
  704. self.nlp_enterprise = sorted(nlp_enterprise, key=lambda x:len(x), reverse=True)
  705. self.docid = docid
  706. in_attachment = False
  707. if richText:
  708. richText = richText.extract() # 过滤掉附件
  709. del_tabel_achievement(soup) # 20240819 过滤掉业绩表格
  710. prem = self.get_prem(soup, web_source_name, all_winner)
  711. if prem == {} and richText:
  712. del_tabel_achievement(richText) # 20240819 过滤掉业绩表格
  713. prem = self.get_prem(richText, web_source_name, all_winner)
  714. in_attachment = True
  715. if len(prem) == 1: # 只有一个包且包号为1 或 长度大于2 的大概率为自动增加编号包,改为Project
  716. k = list(prem)[0]
  717. if k.startswith('自增'):
  718. prem['Project'] = prem.pop(k)
  719. return prem, in_attachment
  720. class CandidateExtractor(object):
  721. def __init__(self):
  722. '''各要素表头规则'''
  723. self.head_rule_dic = {
  724. 'package_code': "(包[段组件]|标[段包]|分[包标])(序?号|$)|包号|^标段$",
  725. 'project_code': "(项目|招标|采购|计划|公告|包[段组件]|标[段包的]|标段(包)|分[包标])(编号|编码)",
  726. "project_name": "(包[段组件]|标[段包的项]|标段(包)|分[包标]|采购|项目|工程|货物|商品|产品|设备|通用|主要标的|^包)(名称?|内容)|^标的$",
  727. "win_sort": "排名|排序|名次|中标及备选结果|(中标|成交|中选|推荐)(次序|顺序)",
  728. 'win_or_not': '是否(建议|推荐)?(中标|成交)|是否入围|是否入库|入围结论|^选择设备$', # 补充站源特别表达:例:577351909 选择设备 1 为中标 0 非中标
  729. "candidate": "((候选|入围|入选|投标|应答|响应|参选|比选|参与)(供应商库)?的?(人|人?单位|机构|供应商|供货商|服务商|投标人|申请人|(中标)?公司|(中标)?企业|银行)|(通过)?名单|中标候选人)(名称|名单|全称|\d)?$|^(推荐的?)?供应商(名称|信息)?$|投标个人/单位|^(公司|单位)名称$|供应商单位名称$", #补充 368295593 投标个人/单位 提取
  730. "bid_amount": "投标[报总]?价|报价(总?金额|总价|总额)|总报价|^\w{,5}报价(([\w、/]{1,15}))?$|(中标|成交|合同)(不?含税|\w{,3}?))?([金总]额|[报均总]价|价[格款]?)|承包价|含税价|经评审的价格",
  731. "win_tenderer": "第[一1]名|第[一1]预?(中标|成交)?候选人",
  732. "second_tenderer": "第[二2]名|第[二2]预?(中标|成交)?候选人",
  733. "third_tenderer": "第[三3]名|第[三3]预?(中标|成交)?候选人",
  734. }
  735. '''非表格候选人正则'''
  736. # self.p = '((候选|入围|入选|投标)(供应商库)?的?(人|人?单位|机构|供应商|供货商|服务商|投标人|(中标)?公司|(中标)?企业|应答人)|(通过)?名单)(名称|名单|全称|\d)?:$'
  737. self.p = '((候选|入围|入选|投标|报价|成交|中标|中选|供[货应]|应答|响应)(人|方|人?单位|机构|厂?商|商家|服务商|公司|企业)|(通过|入围)名单)(名称|名单|全称|\d)?[是为:]?$'
  738. self.tb = TableTag2List()
  739. with open(INTERFACE_DIR+'/header_set.pkl', 'rb') as f:
  740. self.headerset = pickle.load(f)
  741. # self.headerset.update(set(['异议受理部门', '异议受理部门联系人', '招标投标监督部门']))
  742. # with open(os.path.dirname(__file__)+'/header_set.pkl', 'wb') as f:
  743. # pickle.dump(self.headerset, f)
  744. def find_header(self, td_list):
  745. fix_td_list = [re.sub('[::]$|^[一二三四五六七八九十0-9]{1,3}、|(([\w、×*/]{1,20}))$|(不?含税)|/万?元|拟|\s', '', it) for it in td_list] # 去除表头无关信息,方便匹配判断是否为表头
  746. header_dic = dict()
  747. flag = False
  748. contain_header = False
  749. if len(set(fix_td_list) & self.headerset)>=2 and (len(set(fix_td_list) & self.headerset)/len(set(fix_td_list))>=0.6 or is_head_line(fix_td_list)):
  750. flag = True
  751. for i in range(len(td_list)) :
  752. text = td_list[i]
  753. text = re.sub('\s|[((]排名不分先后[))]', '', text)
  754. if len(text) > 15: # 长度大于15 不进行表头匹配
  755. continue
  756. if re.search('未(中标|成交)原因', text): # 不提取此种表格
  757. return flag, contain_header, dict()
  758. num = 0
  759. for k, v in self.head_rule_dic.items():
  760. if k == 'candidate' and re.search('第[一二三]名|第[一二三]预?(中标|成交)?候选人', text):
  761. continue
  762. if re.search('评分|得分|分数|分值', text) and re.search('排名|排序', text)==None: # 692295071 综合得分排序
  763. continue
  764. if re.search(v, text):
  765. if k in ['candidate', 'win_tenderer', 'second_tenderer', 'third_tenderer'] and re.search('是否', text):
  766. continue
  767. elif k == 'win_or_not' and re.search('是否(中标|成交)候选人', text): # 修复 584112560 把第二作第一错误
  768. continue
  769. elif k in header_dic:
  770. if k in ['bid_amount'] and re.search('总(价|金?额)', text): # 总价替换单价
  771. header_dic[k] = (i, text)
  772. num += 1
  773. elif k == 'project_code' and text != header_dic[k][1] and 'package_code' not in header_dic\
  774. and re.search(self.head_rule_dic['package_code'], re.sub('\s', '', ','.join(td_list)))==None: # 如果出现两次项目编号且没有包号,把第二次出现的作为包号 例:472537470
  775. header_dic['package_code'] = (i, text)
  776. elif text == header_dic[k][1]: # 表头相同取后一个
  777. header_dic[k] = (i, text)
  778. continue
  779. header_dic[k] = (i, text)
  780. # if k != 'candidate': # candidate 可与前三候选重复
  781. num += 1
  782. if 'win_tenderer'in header_dic and 'second_tenderer' in header_dic and 'candidate' in header_dic:
  783. header_dic.pop('candidate')
  784. if num>1:
  785. # print('表头错误,一个td匹配到两个表头:', header_dic)
  786. return flag, contain_header, dict()
  787. if text == '单位': # 20241128 补充金额单位
  788. header_dic['amount_unit'] = (i, text)
  789. if ('candidate' in header_dic and 'win_sort' in header_dic) or ('win_tenderer' in header_dic and 'second_tenderer' in header_dic): # 有排名才返回表头进行提取
  790. return flag, contain_header, header_dic
  791. elif len(set(fix_td_list) & self.headerset) >= 2 or (len(set(fix_td_list)) == 2 and len(set(fix_td_list[:-1]) & self.headerset) >= 1): # 如果包含两个表头以上或 只有两列且包含一个表头 # 最后一个不算 563507195 单位名称 中标及备选结果
  792. contain_header = True
  793. if len(set(fix_td_list) & self.headerset) == 1:
  794. td_text = (set(fix_td_list) & self.headerset).pop()
  795. if re.match('第[一二三\d](名|(中标|成交)?候选人)$', td_text):
  796. contain_header = False
  797. return flag, contain_header, dict()
  798. def is_role(self, text):
  799. if len(text) > 25 or len(text) < 4:
  800. return False
  801. elif len(re.findall('有限责?任?公司', text)) > 1:
  802. return False
  803. elif re.search('[\w()]{4,}(有限责?任?公司|学校|学院|大学|中学|小学|医院|管理处|办公室|委员会|村委会|纪念馆|监狱|管教所|修养所|社区|农场|林场|羊场|猪场|石场|村|幼儿园|厂|中心|超市|门市|商场|工作室|文印室|城|部|店|站|馆|行|社|处)$', text):
  804. return True
  805. else:
  806. ners = selffool.ner(text)
  807. if len(ners[0]) == 1 and ('company' in ners[0][0] or 'org' in ners[0][0]):
  808. return True
  809. return False
  810. def extract_from_df(self, df, headers):
  811. prem_dic = {}
  812. link_set = set()
  813. candidate_set = set()
  814. role_dic = dict() # 保存一二三候选人并排的情况
  815. findtop3 = False
  816. findmoney = False
  817. line_num = 0
  818. line_package = None
  819. package_flag = 0
  820. if "package_code" in headers:
  821. package_flag = 1
  822. if len(df)!=len(set(df[headers["package_code"][0]])): # 如果有包号但重复,进行下列判断是否和跟其他字段组合包号
  823. if "project_code" in headers and df[headers["project_code"][0]][0] != df[headers["package_code"][0]][0]:
  824. package_flag = 2
  825. elif "project_name" in headers and find_package(df[headers["package_code"][0]][0]):
  826. package_flag = 3
  827. for i in df.index:
  828. package_code_raw = df.loc[i, headers['package_code'][0]].strip().replace(' ', '') if "package_code" in headers else ""
  829. project_code = df.loc[i, headers['project_code'][0]].strip().replace(' ', '') if "project_code" in headers else ""
  830. project_name = df.loc[i, headers['project_name'][0]].strip().replace(' ', '') if "project_name" in headers else ""
  831. candidate_ = df.loc[i, headers['candidate'][0]].strip().replace(' ', '') if "candidate" in headers else ""
  832. win_or_not = df.loc[i, headers['win_or_not'][0]].strip().replace(' ', '') if "win_or_not" in headers else ""
  833. # budget_ = df.loc[i, headers['budget'][0]] if "budget" in headers else ""
  834. bid_amount_ = df.loc[i, headers['bid_amount'][0]].strip().replace(' ', '') if "bid_amount" in headers else ""
  835. win_sort = df.loc[i, headers['win_sort'][0]].strip().replace(' ', '') if "win_sort" in headers else ""
  836. win_tenderer = df.loc[i, headers['win_tenderer'][0]].strip().replace(' ', '') if "win_tenderer" in headers else ""
  837. second_tenderer = df.loc[i, headers['second_tenderer'][0]].strip().replace(' ', '') if "second_tenderer" in headers else ""
  838. third_tenderer = df.loc[i, headers['third_tenderer'][0]].strip().replace(' ', '') if "third_tenderer" in headers else ""
  839. amount_unit = df.loc[i, headers['amount_unit'][0]].strip().replace(' ', '') if "amount_unit" in headers else ""
  840. joint = ''
  841. if set([package_code_raw, candidate_, win_or_not, bid_amount_, win_tenderer, second_tenderer, third_tenderer]) & self.headerset != set(): # 包含表头, 停止匹配 # 排除 ,win_sort 避免367940050漏提取
  842. # print('包含表头, 停止匹配')
  843. break
  844. if len(set([package_code_raw, candidate_,win_sort, win_or_not, bid_amount_, win_tenderer, second_tenderer, third_tenderer]) - set(['', ' '])) < 2: # 全部为空或内容一样 停止匹配
  845. # print('全部为空或内容一样 停止匹配')
  846. if len(set(df.loc[i,:]))==1 and re.search('^第?([一二三四五六七八九十]{1,3}|[a-zA-Z0-9-]{,9})?[分子]?(标[段包项]?|包[组件标]?|合同[包段])([一二三四五六七八九十]{1,3}|[a-zA-Z0-9-]{,9})?$', win_sort):
  847. line_package = win_sort
  848. continue
  849. elif win_sort == '' and candidate_ != '': # 修复 696375822 前几行为空
  850. continue
  851. else:
  852. break
  853. if candidate_ != "" and win_sort == "" and headers['candidate'][0] > 0: # 修复某些表头不说 排名,直接用候选人代替
  854. col_indx = headers['candidate'][0] -1
  855. pre_col = df.loc[i, col_indx]
  856. if col_indx > 0 and pre_col == candidate_:
  857. pre_col = df.loc[i, col_indx - 1]
  858. if re.search('第[一二三]名|第[一二三](中标)?候选人', pre_col):
  859. win_sort = pre_col
  860. package_code = package_code_raw
  861. if package_code == '' and line_package:
  862. package_code = line_package
  863. # candidate = candidate_ if self.is_role(candidate_) else ""
  864. # tenderer = tenderer if self.is_role(tenderer) else ""
  865. # candidate = get_role(candidate_, self.nlp_enterprise)
  866. if candidate_!='':
  867. leader, joint = get_td_companys(candidate_, self.nlp_enterprise)
  868. if leader == '':
  869. log('表格角色内容不在公司列表:%s,docid:%s' % (candidate_, self.docid))
  870. candidate = leader
  871. else:
  872. candidate = candidate_
  873. # if len(set([project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_])) < 2:
  874. # break
  875. if(candidate_,win_tenderer, second_tenderer,third_tenderer, bid_amount_,package_code,project_code,win_sort) in link_set:
  876. continue
  877. link_set.add((candidate_, win_tenderer, second_tenderer, third_tenderer, bid_amount_,package_code,project_code,win_sort))
  878. package = package_code
  879. if package == "" and project_code != "": # 修复 395747178 多项目 只提取到一个
  880. package = project_code
  881. package = uniform_package_name(package) if package !="" else "Project"
  882. if package_flag == 2 and project_code != "":
  883. project_code_pk = uniform_package_name(project_code)
  884. package = "%s_%s"%(project_code_pk, package)
  885. elif package_flag == 3 and project_name != "":
  886. for iter in find_package(project_name):
  887. project_name_pk = uniform_package_name(iter.group(0))
  888. package = "%s_%s"%(project_name_pk, package)
  889. break
  890. if candidate:
  891. if win_or_not and re.search('否|未入围', win_or_not):
  892. candidate_set.add(candidate)
  893. # elif re.search('^((建议|推荐)(中标|成交)|是)$', win_or_not) and win_sort in ['', '参与投标单位及排名'] and win_tenderer=='':
  894. # win_sort = '第一名'
  895. # candidate_set.add(candidate)
  896. else:
  897. candidate_set.add(candidate)
  898. if win_tenderer and second_tenderer: # and third_tenderer 128778062 这篇只有 第一二候选人
  899. if re.search("(候选人|投标人|单位|公司)名?称?$", df.loc[i, 0]) or re.search("(候选人|投标人|单位|公司)名?称?", df.loc[i, 1]):
  900. findtop3 = True
  901. for type, text in zip(['win_tenderer', 'second_tenderer', 'third_tenderer'],
  902. [win_tenderer, second_tenderer, third_tenderer]):
  903. # text = get_role(text, self.nlp_enterprise)
  904. leader, joint = get_td_companys(text, self.nlp_enterprise)
  905. if leader == '':
  906. log('表格角色内容不在公司列表:%s,docid:%s' % (text, self.docid))
  907. text = leader
  908. if text:
  909. # if self.is_role(text):
  910. if type not in role_dic:
  911. role_dic[type] = dict()
  912. role_dic[type]['role_text'] = text
  913. candidate_set.add(text)
  914. if joint:
  915. role_dic[type]['win_tenderer_joint'] = joint
  916. elif re.search('投标报价|报价$', df.loc[i, 0]) or re.search('投标报价|报价$', df.loc[i, 1]):
  917. findmoney = True
  918. header = df.loc[i, 0] if re.search('投标报价|报价$', df.loc[i, 0]) else df.loc[i, 1]
  919. for type, text in zip(['win_tenderer', 'second_tenderer', 'third_tenderer'],
  920. [win_tenderer, second_tenderer, third_tenderer]):
  921. if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税', '',
  922. text)) > 5: # 金额字段出现超过5个非金额字符,中断匹配
  923. break
  924. if amount_unit != '' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and re.search('元', text+header)==None: # 补充另外在一列的金额单位
  925. text += amount_unit
  926. money, money_unit = money_process(text, header)
  927. if re.search('元[/每]', amount_unit) or re.search('单价', header):
  928. unit_price = money
  929. money = 0
  930. else:
  931. unit_price = 0
  932. if (re.search('费率|下浮率|[%%‰折]|优惠率', header+text) and money < 100) or money > 50000000000: # 如果是费率或大于500亿的金额改为0
  933. money = 0
  934. if money > 0:
  935. if type not in role_dic:
  936. role_dic[type] = dict()
  937. role_dic[type]['money'] = money
  938. role_dic[type]['money_unit'] = money_unit
  939. if unit_price > 0:
  940. if type not in role_dic:
  941. role_dic[type] = dict()
  942. role_dic[type]['unit_price'] = unit_price
  943. role_dic[type]['money_unit'] = money_unit
  944. else:
  945. line_num += 1
  946. if findtop3 and findmoney:
  947. break
  948. if line_num > 3:
  949. break
  950. elif candidate and win_sort:
  951. role_type = ""
  952. if re.search('第[一1]|^([一1]|01|中标人)$', win_sort) and re.search('第一备选人', win_sort)==None:
  953. role_type = "win_tenderer"
  954. if win_or_not in ['否', '未中标', '0']: # 修复特别站源表达 577351909 选择设备:0 不是中标
  955. role_type = ''
  956. elif re.search('第[二2]|^([二2]|02|第一备选人)$', win_sort) and re.search('第二备选人', win_sort)==None:
  957. role_type = "second_tenderer"
  958. if win_or_not in ['是', '1']:
  959. role_type = "win_tenderer"
  960. elif re.search('第[三3]|^([三3]|03|第二备选人)$', win_sort):
  961. role_type = "third_tenderer"
  962. if role_type != "":
  963. if package not in prem_dic:
  964. prem_dic[package] = {
  965. 'code': '',
  966. 'name': '',
  967. 'roleList': [],
  968. 'tendereeMoney': 0,
  969. 'tendereeMoneyUnit': ""
  970. }
  971. prem_dic[package]['code'] = project_code
  972. prem_dic[package]['name'] = project_name
  973. if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税', '', bid_amount_))> 5: # 金额字段出现超过5个非金额字符,中断匹配
  974. break
  975. header = headers['bid_amount'][1] if "bid_amount" in headers else ''
  976. if amount_unit != '' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and re.search('元',
  977. bid_amount_ + header) == None: # 补充另外在一列的金额单位
  978. bid_amount_ += amount_unit
  979. bid_amount, money_unit = money_process(bid_amount_, headers['bid_amount'][1]) if "bid_amount" in headers else (0, "")
  980. if re.search('元[/每]', amount_unit) or re.search('单价', header):
  981. unit_price = bid_amount
  982. bid_amount = 0
  983. else:
  984. unit_price = 0
  985. if (re.search('费率|下浮率|[%%‰折]|优惠率',
  986. header + bid_amount_) and bid_amount < 100) or bid_amount > 50000000000: # 如果是费率或大于500亿的金额改为0
  987. bid_amount = 0
  988. tmp_role_dic = {
  989. "address": "",
  990. "linklist": [],
  991. "role_money": {
  992. "discount_ratio": "",
  993. "downward_floating_ratio": "",
  994. "floating_ratio": "",
  995. "money": bid_amount,
  996. "money_unit": money_unit
  997. },
  998. "role_name": role_type,
  999. "role_text": candidate,
  1000. "serviceTime": ""
  1001. }
  1002. if joint != '':
  1003. tmp_role_dic['win_tenderer_joint'] = joint
  1004. if unit_price > 0:
  1005. tmp_role_dic['role_money']['unit_price'] = unit_price
  1006. if role_type in str(prem_dic[package]['roleList']): # 一种角色只保留一个,解决 682633825 有多个第二名情况
  1007. continue
  1008. prem_dic[package]['roleList'].append(tmp_role_dic)
  1009. if len(prem_dic[package]['roleList']) == 0: # 只有项目编号和名称的 丢弃
  1010. prem_dic.pop(package)
  1011. if role_dic and prem_dic == dict():
  1012. if package not in prem_dic:
  1013. prem_dic[package] = {
  1014. 'code': '',
  1015. 'name': '',
  1016. 'roleList': [],
  1017. 'tendereeMoney': 0,
  1018. 'tendereeMoneyUnit': ""
  1019. }
  1020. for role_type, v in role_dic.items():
  1021. role_text = v.get('role_text', '')
  1022. if role_text == "":
  1023. continue
  1024. money = v.get('money', 0)
  1025. money_unit = v.get('money_unit', '')
  1026. tmp_d = {
  1027. "address": "",
  1028. "linklist": [],
  1029. "role_money": {
  1030. "discount_ratio": "",
  1031. "downward_floating_ratio": "",
  1032. "floating_ratio": "",
  1033. "money": money,
  1034. "money_unit": money_unit
  1035. },
  1036. "role_name": role_type,
  1037. "role_text": role_text,
  1038. "serviceTime": ""
  1039. }
  1040. if 'win_tenderer_joint' in v:
  1041. tmp_d['win_tenderer_joint'] = v['win_tenderer_joint']
  1042. prem_dic[package]['roleList'].append(tmp_d)
  1043. if len(prem_dic[package]['roleList']) == 0: # 只有项目编号和名称的 丢弃
  1044. prem_dic.pop(package)
  1045. return prem_dic, candidate_set
  1046. def get_prem(self, soup):
  1047. tables = soup.find_all('table')
  1048. tables.reverse()
  1049. rs_dic = {}
  1050. candidate_set = set()
  1051. for table in tables:
  1052. trs = self.tb.table2list(table)
  1053. i = 0
  1054. headers = ""
  1055. while i < len(trs) - 1:
  1056. flag_, contain_header_, headers_ = self.find_header(trs[i])
  1057. if flag_ and headers_ != dict():
  1058. table_items = []
  1059. headers = headers_
  1060. for j in range(i + 1, len(trs)):
  1061. if len(trs[j]) == len(trs[i]):
  1062. flag_, contain_header_, headers_ = self.find_header(trs[j])
  1063. if flag_ or contain_header_:
  1064. break
  1065. else:
  1066. table_items.append(trs[j])
  1067. else:
  1068. # print('表头,内容 列数不一致', len(trs[i]), len(trs[j]))
  1069. break
  1070. # print('内容:', len(table_items), table_items)
  1071. if len(table_items) >= 1:
  1072. df = pd.DataFrame(table_items)
  1073. # print('候选人表格行数:', len(df))
  1074. prem_, candidate_set_ = self.extract_from_df(df, headers)
  1075. # print('prem_: ', prem_)
  1076. rs_dic.update(prem_)
  1077. candidate_set.update(candidate_set_)
  1078. i = j - 1
  1079. i += 1
  1080. if rs_dic and 'package_code' not in headers and 'Project' in rs_dic and (table.find('caption') or table.find_previous_sibling()): # 一个表格只有两行且没有标段的,从上一个兄弟标签找标段
  1081. sib = table.find('caption') or table.find_previous_sibling() # 693211745 table下是caption标签写标段信息
  1082. sib_text = sib.get_text().strip()
  1083. ser_sib = re.search('第?[0-9一二三四五六七八九十a-zA-Z]{1,4}(标[段号的包项]|([分子]?包|包[组件号]))|(标[段号的包项]|([分子]?包|包[组件号]))号?:?[0-9一二三四五六七八九十a-zA-Z]{1,4}|包名:[0-9一二三四五六七八九十]{1,4}', sib_text)
  1084. if sib.name in ['p', 'div', 'caption'] and len(sib_text)<100 and ser_sib:
  1085. package_sib = ser_sib.group(0)
  1086. package_sib = uniform_package_name(package_sib)
  1087. rs_dic[package_sib] = rs_dic.pop('Project')
  1088. table.extract()
  1089. return rs_dic, candidate_set
  1090. def get_candidates_from_text(self, list_sentences, list_entitys):
  1091. candidates = set()
  1092. tenderee_or_agency = set()
  1093. sentences = sorted(list_sentences[0], key=lambda x: x.sentence_index)
  1094. for ent in list_entitys[0]:
  1095. if ent.entity_type in ['org', 'company']:
  1096. sen_index = ent.sentence_index
  1097. text = sentences[sen_index].sentence_text
  1098. b = ent.wordOffset_begin
  1099. e = ent.wordOffset_end
  1100. if ent.label in [2,3,4]: # 直接加实体预测的候选人, 否则规则检查是否为候选人
  1101. candidates.add(ent.entity_text)
  1102. elif isinstance(b, int) and isinstance(e, int) and ent.label in [5]:
  1103. foreword = text[max(0, b - 10):b]
  1104. if re.search(self.p, foreword):
  1105. candidates.add(ent.entity_text)
  1106. if ent.label in [0, 1] and ent.values[ent.label]>0.5:
  1107. tenderee_or_agency.add(ent.entity_text)
  1108. candidates -= tenderee_or_agency # 2024/05/10 463166661 把 四川省第二中医医院作为候选人 过滤掉为招标或代理角色 的候选人
  1109. return candidates
  1110. def predict(self, html, list_sentences, list_entitys, nlp_enterprise, docid=''):
  1111. self.nlp_enterprise = sorted(nlp_enterprise, key=lambda x:len(x), reverse=True)
  1112. self.docid = docid
  1113. html = html.replace('比选申请单位', '中标候选人') # 82347769
  1114. html = re.sub("<html>|</html>|<body>|</body>","",html)
  1115. html = re.sub("##attachment##","",html)
  1116. soup = BeautifulSoup(html, 'lxml')
  1117. richText = soup.find(name='div', attrs={'class': 'richTextFetch'})
  1118. in_attachment = False
  1119. if richText:
  1120. richText = richText.extract() # 过滤掉附件
  1121. del_tabel_achievement(soup) # 20240819 过滤掉业绩表格 例:500817166
  1122. prem, candidate_set = self.get_prem(soup)
  1123. if prem == {} and richText:
  1124. del_tabel_achievement(richText) # 20240819 过滤掉业绩表格
  1125. prem, candidate_set = self.get_prem(richText)
  1126. in_attachment = True
  1127. candidate_set2 = self.get_candidates_from_text(list_sentences, list_entitys)
  1128. candidate_set.update(candidate_set2)
  1129. return prem, {'candidate': ','.join(candidate_set)}, in_attachment
  1130. def get_header_line(list_item):
  1131. '''
  1132. 判断列表内文本哪些是表头,哪些不是
  1133. :param list_item: [ '批复结果', '许可/同意', '批复文号',]
  1134. :return:
  1135. '''
  1136. rs = []
  1137. x = []
  1138. for item in list_item:
  1139. if len(item.strip()) > 30: # 大于30字不可能是表头,直接替换为无意义字符
  1140. item = 'xxx'
  1141. x.append(getPredictor("form").encode(item))
  1142. predict_y = getPredictor("form").predict(np.array(x), type="item")
  1143. for item, values in zip(list_item, list(predict_y)):
  1144. item = str(item).replace(' ', '')
  1145. lb = 1 if values[1] > 0.5 else 0
  1146. if item in ['许可/同意', '办结(通过)', '办结(准予许可)','批准', '合格', '民间投资', '备案','综合排序第一','满足采购文件要求','未否决']:
  1147. lb = 0
  1148. elif item in ['环境影响评价机构', '建设单位或地方政府作出的相关环保承诺', '环境影响评价技术服务机构', '报告全本'] or re.search('^比例\d{1,2}%$', item):
  1149. lb = 1
  1150. elif lb == 0 and item in header_set:
  1151. lb = 1
  1152. rs.append(lb)
  1153. return rs