| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232 |
- # -*- coding: utf-8 -*-
- """表格要素 / 候选人 / 表头识别提取器。
- 按 ARCHITECTURE.md Phase 5 拆分建议,从 ``interface/predictor.py`` 迁出以下
- 表格相关类与函数:
- - ``TableTag2List`` — soup table 转列表并补全 span(原 8033-8192 行)
- - ``is_head_line`` — 调用表头识别模型判断是否为表头行(原 8195-8211 行)
- - ``TablePremExtractor`` — 表格要素(标段/招标人/中标人/金额)提取(原 8213-8754 行)
- - ``CandidateExtractor`` — 表格候选人提取(原 8756-9183 行)
- - ``get_header_line`` — 判断列表内文本哪些是表头(原 9298-9321 行)
- 原 ``from common.Utils import *`` / ``from interface.modelFactory import *``
- 已替换为显式 import;``os.path.dirname(__file__)`` 路径引用替换为
- ``predictors._common.INTERFACE_DIR``。
- ``interface/predictor.py`` 仍保留原定义,老 import 不受影响。
- """
- from __future__ import absolute_import
- import re
- import pickle
- import numpy as np
- import pandas as pd
- from bs4 import BeautifulSoup
- from BiddingKG.dl.common.logging import log
- from BiddingKG.dl.common.context_utils import (
- find_package,
- uniform_package_name,
- money_process,
- cut_repeat_name,
- )
- from BiddingKG.dl.common.Utils import del_tabel_achievement
- from BiddingKG.dl.common.nerUtils import getNers
- from BiddingKG.dl.foolnltk import selffool
- from BiddingKG.dl.time.re_servicetime import extract_servicetime
- from BiddingKG.dl.common.attr_utils import extract_serviceTime
- from BiddingKG.dl.interface.predictor import getPredictor
- from BiddingKG.dl.predictors._common import (
- INTERFACE_DIR,
- header_set,
- get_td_companys,
- get_role,
- )
- __all__ = [
- "TableTag2List",
- "is_head_line",
- "TablePremExtractor",
- "CandidateExtractor",
- "get_header_line",
- ]
- class TableTag2List():
- '''把soup table 转化为表格补全后的文本列表[[td, td, td], [td, td, td]]'''
- def table2list(self, table, text_process=None, return_html_table=False,return_kv=False):
- '''
- 表格补全及把表格内容列表返回
- :param table:
- :param text_process: 预处理方法,segment(),不为None 时把td内容做预处理,结果返回加标签,适配表头识别 [[[text, 0], [text, 0]] ], 否则只返回文本[[text, text], [text, text]]
- :param return_html_table:
- :param return_kv:
- :return:
- '''
- self._output = []
- row_ind = 0
- col_ind = 0
- html_table = []
- for row in table.find_all('tr'):
- # record the smallest row_span, so that we know how many rows
- # we should skip
- smallest_row_span = 1
- if len(row.find_all(['td', 'th'], recursive=False)) > 20:
- log('未补全前表格列数大于20的不做表格处理')
- if return_html_table:
- return [], []
- return []
- for cell in row.children:
- if cell.name in ('td', 'th'):
- # check multiple rows
- # pdb.set_trace()
- row_span = int(re.sub('[^0-9]', '', cell.get('rowspan'))) if cell.get('rowspan') and cell.get('rowspan').isdigit() else 1
- if row_span == 0: # 20250806 修复 659303627 附件OCR重构表格span为0导致缺少问题
- row_span = 1
- # try updating smallest_row_span
- smallest_row_span = min(smallest_row_span, row_span)
- # check multiple columns
- col_span = int(re.sub('[^0-9]', '', cell.get('colspan'))) if cell.get('colspan') and cell.get('colspan').isdigit() else 1
- if col_span > 20: # 修复 335590254 山东港口阳光智采e平台 数据源表格第一行colspan为200超过50列造成无法提取问题
- col_span = 20
- elif col_span == 0: # 20250806 修复 659303627 附件OCR重构表格span为0导致缺少问题
- col_span = 1
- # find the right index
- while True:
- if self._check_cell_validity(row_ind, col_ind):
- break
- col_ind += 1
- # insert into self._output
- try:
- if 'title' in cell.attrs and cell.get_text().replace(' ', '').endswith(
- '...') and cell.attrs['title'].replace(' ', '').startswith(cell.get_text().replace(' ', '')[:-3]):
- cell.string = cell.attrs['title'] # 修复 类似 215597851 215597851 省略号隐藏内容
- if text_process != None:
- # text = [re.sub('\xa0', '', text_process(cell, final=False)), 0]
- # td_text = re.sub('\xa0', '', text_process(cell, final=False))
- td_text = re.sub('\s|\xa0', '', str(cell.get_text())) # 修复 370835008 td 内公司被p标签拆分为两半情况
- if len(td_text)>30:
- if return_kv:
- td_text = cell.get_text().strip()
- else:
- td_text = re.sub('\xa0', '', text_process(cell, final=False))
- if td_text == "":
- td_text = ' '
- text = [td_text,0]
- else:
- if return_kv:
- td_text = cell.get_text().strip()
- else:
- td_text = str(cell.get_text()).strip().replace("\x06", "").replace("\x05", "").replace("\x07", "").replace('\\', '').replace("(", "(").replace(')', ')').replace('?', '').replace(' ', '')
- td_text = re.sub('\s+', ' ', td_text)
- text = td_text
- # text = str(cell.get_text()).strip().replace("\x06", "").replace("\x05", "").replace("\x07", "").replace('\\', '').replace("(", "(").replace(')', ')').replace('?', '')
- # # text = re.sub('\s', '', text)[:200] # 只需取前200字即可
- # text = ' ' if text == "" else text
- self._insert(row_ind, col_ind, row_span, col_span, text)
- if return_html_table:
- html_table = self._insert_new(row_ind, col_ind, row_span, col_span, str(cell), html_table)
- except UnicodeEncodeError:
- raise Exception( 'Failed to decode text; you might want to specify kwargs transformer=unicode' )
- # update col_ind
- col_ind += col_span
- if col_ind > 50 and text_process == None: # 表格要素提取及候选人提取的 表格列数大于50的去掉
- if return_html_table:
- return [], []
- return []
- # update row_ind
- row_ind += smallest_row_span
- col_ind = 0
- if return_html_table:
- temp_list = []
- for row in self._output:
- if len(row) > 0:
- temp_list.append(row)
- self._output = temp_list
- temp_list = []
- for row in html_table:
- if len(row) > 0:
- temp_list.append(row)
- html_table = temp_list
- return self._output, html_table
- else:
- return self._output
- def _check_validity(self, i, j, height, width):
- """
- check if a rectangle (i, j, height, width) can be put into self.output
- """
- return all(self._check_cell_validity(ii, jj) for ii in range(i, i+height) for jj in range(j, j+width))
- def _check_cell_validity(self, i, j):
- """
- check if a cell (i, j) can be put into self._output
- """
- if i >= len(self._output):
- return True
- if j >= len(self._output[i]):
- return True
- if self._output[i][j] == "":
- return True
- return False
- def _insert(self, i, j, height, width, val):
- # pdb.set_trace()
- for ii in range(i, i+height):
- for jj in range(j, j+width):
- self._insert_cell(ii, jj, val)
- def _insert_cell(self, i, j, val):
- while i >= len(self._output):
- self._output.append([])
- while j >= len(self._output[i]):
- self._output[i].append("")
- if self._output[i][j] == "":
- self._output[i][j] = val
- def _insert_new(self, i, j, height, width, val, cell_list):
- # pdb.set_trace()
- for ii in range(i, i+height):
- for jj in range(j, j+width):
- cell_list = self._insert_cell_new(ii, jj, val, cell_list)
- return cell_list
- def _insert_cell_new(self, i, j, val, cell_list):
- while i >= len(cell_list):
- cell_list.append([])
- while j >= len(cell_list[i]):
- cell_list[i].append("")
- if cell_list[i][j] == "":
- cell_list[i][j] = val
- return cell_list
- def is_head_line(list_item):
- '''
- 调用表头识别模型判断是否为表头行
- :param list_item: 行内容 例: ['技术参数、要求', '变更项']
- :return:
- '''
- x = []
- for item in list_item:
- x.append(getPredictor("form").encode(item))
- predict_y = getPredictor("form").predict(np.array(x), type="item")
- count = 0
- for item, values in zip(list_item, list(predict_y)):
- if values[1] > 0.6:
- count += 1
- if count/len(list_item)>0.6:
- return True
- return False
- class TablePremExtractor(object):
- def __init__(self):
- '''各要素表头规则'''
- self.head_rule_dic = {
- 'project_code': "(项目|招标|采购|计划|公告|包[段组件]|标[段包的]|标段(包)|分[包标])(编号|编码|代码)",
- 'package_code': "(包[段组件]|标[段包]|分[包标])(序?号|$)|包号|^标段$|^品目$",
- "project_name": "(包[段组件]|标[段包的项]|标段(包)|分[包标]|采购|项目|工程|^包)(名称?|内容)", # |货物|商品|产品|设备|通用|主要标的 20250812 货物的不作为项目名称
- "win_sort": "排名|排序|名次|推荐顺序",
- 'win_or_not': '是否(建议|推荐)?(中标|成交|中选)|是否入围|是否入库|入围结论|未(中标|成交)原因|中标情况|^中标结果$',
- "tenderer": "(中标|中选|中价|中拍|成交|供货|承包|承建|承租|竞得|受让))?(候选)?(人(单位)?|单位|供应商|公司|企业|厂家|商家?|客户|供?方|银行|回收商)(名称|$)|^(拟定|单一来源|邀请|拟?推荐(入选|入围)?)?供应商(名称)?$",
- "tenderee": "(项目|采购|招标|遴选|寻源|竞价|议价|比选|委托|询比?价|比价|评选|谈判|邀标|邀请|洽谈|约谈|选取|抽取|抽选)(人|公司|单位|组织|用户|业主|主体|方|部门)(名称|$)",
- "budget": "最高(投标)?限价|总价限价|控制(价格?|金额|总价)|(总价|采购)限价|上限价|拦标价|(采购|招标|项目)?预算|(预算|招标|采购|计划)金额|挂牌价",
- "bid_amount": "投标[报总]?价|报价(总?金额|总价|总额)|总报价|^\w{,5}报价(([\w、/]{1,15}))?$|(中标|中选|中价|中拍|成交|合同))?总?(金?额|[报均总]价|价[格款]?)|承包价|含税价|经评审的价格|中标存款金?额|中标资金|中标存款|存放金额|分配额度",
- "serviceTime": '合同期限|工期/交货期/服务期|工期\(交货期\)|合格工期|服务期限|工期' \
- '|工期要求|项目周期|工期\(交货期\)|计划工期\(服务期限\)|服务时限|履行期限|服务周期|供货期限' \
- '|合格工期|计划工期\(服务期\)|服务期|服务,期|交货\(完工\)时间|交付\(服务、完工\)时间' \
- '|交货时间|保洁期限|维保期|管理年限|工期承诺|(服务|合同|施工|实施|工程|设计)的?(年限|期限|周期|期:)' \
- '|计划工期|工期要求|服务期限?' \
- '|投标工期|设计工期|合格服务周期|总工期|服务时间(范围)?|流转期限|维护期限|服务时限|交货期' \
- '|完成时间|中标工期|项目周期|期限要求|周期|供货期|合同的?履行日期|计划周期' \
- '|履约期限|合同的?约定完成时限|合同的?完成日期|承诺完成日期' \
- '|合同起始日起|合同的?履约期|履约截止日期|承包期限|合同的?完成日期|特许经营期限' \
- '|服务期间|服务履行期|委托(管理)?期限|经营期限|数量' \
- '|(工期|服务期限?|交货期限?|服务履行期|合同期限?|履[行约]期限?)说明|存款期限?|(存款|存放|定存)(期|年)限' \
- '|服务(有效期|年限)|本?合同有效期|协议有效期|项目期限'
- }
- with open(INTERFACE_DIR+'/header_set.pkl', 'rb') as f:
- self.headerset = pickle.load(f)
- self.tb = TableTag2List()
- def find_header(self, td_list, all_winner=False, first_line=False):
- fix_td_list = [re.sub('[::]$|^[一二三四五六七八九十0-9]{1,3}、|(([\w、×*/]{1,20}))$|(不?含税)|/万?元|拟|\s', '', it) for it in td_list] # 去除表头无关信息,方便匹配判断是否为表头
- header_dic = dict()
- flag = False
- contain_header = False
- not_sure_winner = False # 是否 不确定中标的中标人表达方式
- for text in set(fix_td_list) - self.headerset:
- if len(text)<10 and re.search(self.head_rule_dic['bid_amount'], text):
- self.headerset.add(text)
- 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)):
- other_tenderer = ""
- other_tenderer2 = ""
- flag = True
- for i in range(len(td_list)) :
- text = td_list[i]
- text = re.sub('\s|[((]排名不分先后[))]', '', text)
- text = re.sub('排名价', '', text) # 20241225 修复 252208201 排名价(元)错误为排名
- text = re.sub('^人选', '入选', text)
- if text == '备选中标人':
- text = '第二候选人'
- if len(re.sub('(([\w、×*/]{1,20}))$', '', text)) > 15: # 长度大于15 不进行表头匹配
- continue
- if re.search('未(中标|成交|中选|入围|通过)原因', text): # 不提取此种表格 673143737
- return flag, contain_header, dict(), not_sure_winner
- num = 0
- for k, v in self.head_rule_dic.items():
- if re.search('评分|得分|分数|分值', text):
- continue
- if re.search(v, text):
- if k in ['tenderer'] and re.search('是否|未', text): # 修复103367444 未中标单位名称 作为中标
- continue
- if k == 'budget' and re.search('量', text): # 预算工作量 预算采购量 等不作为预算
- continue
- elif k == 'bid_amount' and re.search('分配方案|基准利率|BP值', text): # 517987084 中标资金分配方案
- continue
- elif k in header_dic:
- if k in ['budget', 'bid_amount'] and re.search('总(价|金?额)', text): # 总价替换单价
- header_dic[k] = (i, text)
- num += 1
- elif k == 'project_code' and text != header_dic[k][1] and 'package_code' not in header_dic\
- and re.search(self.head_rule_dic['package_code'], re.sub('\s', '', ','.join(td_list)))==None: # 如果出现两次项目编号且没有包号,把第二次出现的作为包号 例:472537470
- header_dic['package_code'] = (i, text)
- continue
- header_dic[k] = (i, text)
- num += 1
- elif re.search('^((中标|成交|中选|入围|入选)(候选)?)(人|单位|机构|中介(服务)?机构|供应商|客户|方|公司|厂商|商家?|社会资本方?|银行)(名称)?$', text) and re.search('未', text)==None:
- other_tenderer = (i, text)
- elif re.search('^((投标|应答|响应|候选)(人|单位|机构|中介(服务)?机构|供应商|客户|方|公司|厂商|商家?|社会资本方?|银行)|(存款|投标)?银行|供应商)(名称)?$|^机构名称$|^单位(名称)?$', text) and re.search('未', text)==None:
- other_tenderer2 = (i, text)
- if num>1:
- if re.search(self.head_rule_dic['project_code'], text) and re.search(self.head_rule_dic['package_code'], text): # 修复 528486798 分标编号-包号
- continue
- # print('表头错误,一个td匹配到两个表头:', header_dic)
- return flag, contain_header, dict(), not_sure_winner
- if text == '单位': # 20241128 补充金额单位
- header_dic['amount_unit'] = (i, text)
- if re.search(';金额((万?元))?;', ';'.join(td_list)): # 召回某些表格只写 金额 作为表头,不能识别为招标或中标金额
- if 'tenderer' in header_dic and 'bid_amount' not in header_dic:
- for i in range(len(td_list)):
- text = td_list[i]
- if re.search('^金额((万?元))?$',text):
- header_dic['bid_amount'] = (i, text)
- break
- elif 'tenderee' in header_dic and 'budget' not in header_dic:
- for i in range(len(td_list)):
- text = td_list[i]
- if re.search('^金额((万?元))?$', text):
- header_dic['budget'] = (i, text)
- break
- if all_winner == 1 and 'tenderer' not in header_dic: # 标题有存款、入库、入围等公告补充其他表达做中标人
- if other_tenderer!="":
- header_dic['tenderer'] = other_tenderer
- elif other_tenderer2!="":
- header_dic['tenderer'] = other_tenderer2
- if 'win_sort' not in header_dic:
- not_sure_winner = True
- elif 'tenderer' not in header_dic and 'win_or_not' in header_dic:
- if other_tenderer!="":
- header_dic['tenderer'] = other_tenderer
- # elif other_tenderer2!="": # 20260115 注释 723429011 单位 是否中标 不是中标人
- # header_dic['tenderer'] = other_tenderer2
- if all_winner == 1 and 'win_sort' in header_dic: # 标题有存管类公告不分排名
- header_dic.pop('win_sort')
- if 'tenderer' in header_dic and re.search('候选|入围|入选', header_dic['tenderer'][1]) and re.search('推荐的?((中标|成交|中选)候选人|(候选|入围|入选)供应商)', header_dic['tenderer'][1])==None and all_winner == False:
- header_dic.pop('tenderer')
- if ('project_code' in header_dic or 'package_code' in header_dic or 'project_name' in header_dic) and (
- 'tenderer' in header_dic): # 包含标段及招标金额或中标人的进行提取 # or'budget' in header_dic 20250919 改为有中标人的才提取,只有预算的容易与非表格提取重复 例子:647543768
- return flag, contain_header, header_dic, not_sure_winner
- elif ('tenderer' in header_dic) and ('bid_amount' in header_dic): # 包含中标人及中标金额的进行提取
- if 'win_sort' in header_dic: # 有排名的 用候选人提取类
- return flag, contain_header, dict(), not_sure_winner
- 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
- # print('只有供应商名称 没排名和包号的去掉')
- return flag, contain_header, dict(), not_sure_winner
- return flag,contain_header, header_dic, not_sure_winner
- elif 'tenderer' in header_dic and (re.search('(中标|中选|中价|成交|竞得)(人|单位|供应商|公司|企业|厂家|商家?|客户|供?方|银行)',header_dic['tenderer'][1]) or all_winner): # 有中标人,且有明确中标关键词的进行提取
- return flag, contain_header, header_dic, not_sure_winner
- # elif 'tenderer' in header_dic and 'serviceTime' in header_dic:
- # return flag, contain_header, header_dic, not_sure_winner
- elif len(set(fix_td_list) & self.headerset) >= 2 or (len(set(fix_td_list)) == 2 and len(set(td_list) & self.headerset) >= 1): # 如果包含两个表头以上或 只有两列且包含一个表头
- contain_header = True
- return flag, contain_header, dict(), not_sure_winner
- def extract_from_df(self, df, headers, web_source_name, all_winner=False):
- prem_dic = {}
- previous_package = "" # 上一行包号
- previous_project_name = "" # 上一行项目名称
- multi_same_package = False # 非连续的重复包号
- package_fix2raw = dict() # 处理后包号:处理前包号 字典
- link_set = set()
- tenderer_list = [] # 保存所有中标人
- serviceTime_list = []
- not_package = True if 'project_name' in headers and re.search('(货物|商品|产品|通用|主要标的)(名称?|内容)', headers['project_name'][1]) and \
- 'package_code' not in headers and 'budget' not in headers and "bid_amount" not in headers else False
- if set(['project_code', 'package_code', 'tenderee', 'tenderer']) & set(headers) == set() and ('project_name' not in headers # 补充没有项目名称或有项目名称且是货物的才过滤掉
- or re.search('(货物|商品|产品|设备|通用|主要标的)(名称?|内容)', headers['project_name'][1])): # 20240131修复只有货物名称及最高限价的错误作为多包 396636683; 补充避免423647863采购意向被过滤
- # print('没有包号及角色的不要')
- return {}
- have_bid_amount = False # 是否包含中标金额
- if "bid_amount" in headers and re.search('[1-9]+', '#'.join([it.strip() for it in df[headers['bid_amount'][0]]])):
- have_bid_amount = True
- for i in df.index:
- same_package = False # 连续重复包号,一般是 rowspan 造成;一包 多个采购
- project_code = df.loc[i, headers['project_code'][0]].strip().replace(' ', '') if "project_code" in headers else ""
- package_code_raw = df.loc[i, headers['package_code'][0]].strip().replace(' ', '') if "package_code" in headers else ""
- project_name = df.loc[i, headers['project_name'][0]].strip().replace(' ', '') if "project_name" in headers else ""
- tenderee = df.loc[i, headers['tenderee'][0]].strip().replace(' ', '') if "tenderee" in headers else ""
- tenderer = df.loc[i, headers['tenderer'][0]].strip().replace(' ', '') if "tenderer" in headers else ""
- budget_ = df.loc[i, headers['budget'][0]].strip().replace(' ', '') if "budget" in headers else ""
- bid_amount_ = df.loc[i, headers['bid_amount'][0]].strip().replace(' ', '') if "bid_amount" in headers else ""
- win_sort = df.loc[i, headers['win_sort'][0]].strip().replace(' ', '') if "win_sort" in headers else ""
- win_or_not = df.loc[i, headers['win_or_not'][0]].strip().replace(' ', '') if "win_or_not" in headers else ""
- serviceTime = df.loc[i, headers['serviceTime'][0]].strip().replace(' ', '') if "serviceTime" in headers else ""
- amount_unit = df.loc[i, headers['amount_unit'][0]].strip().replace(' ', '') if "amount_unit" in headers else ""
- joint = ''
- if set([project_code, package_code_raw, project_name,tenderee,tenderer,budget_,bid_amount_]) & self.headerset != set(): # 只要有一项为表头 停止匹配
- # print('只要有一项为表头 停止匹配', set([project_code, package_code_raw, project_name,tenderee,tenderer,budget_,bid_amount_,win_sort]) & self.headerset)
- break
- if len(set([project_code, package_code_raw, project_name,tenderee,tenderer,budget_,bid_amount_,win_sort])- set(['', ' '])) < 2 and tenderer=='': # 内容为空或全部一样 停止匹配
- # print('内容为空或全部一样 停止匹配')
- break
- if re.search('详见', project_name): # 去除某些表达: 详见招标文件
- project_name = ""
- if package_code_raw == "" and re.search('第?[0-9一二三四五六七八九十a-zA-Z]{1,4}(标[段号的包项]|([分子]?包|包[组件号]))$|^(标[段号的包项]|([分子]?包|包[组件号]))号?:?[0-9一二三四五六七八九十a-zA-Z]{1,4}$', project_name):
- package_code_raw = project_name
- project_name = ""
- package_code = package_code_raw
- if re.search('合计|总计', package_code+project_code+project_name):
- continue
- if package_code + project_code == previous_package: # 处理 208162730 一个包采购多种东西情况
- if project_name == previous_project_name:
- same_package = True
- if previous_package!="": # 有包号或项目编号且跟上一行相同时,去除项目名称
- project_name = ''
- if win_sort != "" and re.search('排名|排序|名次|推荐顺序', headers['win_sort'][1]): # 此类型表由 CandidateExtractor类提取 防止类似 328485591 作为多包
- break
- if win_or_not != "" and (re.search('(建议|推荐)(中标|成交|中选)|是|^(中标|成交|中选)', win_or_not)==None or re.search('\w', win_or_not)==None): # 2024/04/2 修复 252208201 为空的不中标
- continue
- elif 'win_or_not' in headers and win_or_not == '': # 2024/12/25 修复 334753545 中标情况为空的不中标
- continue
- if "win_sort" in headers and win_sort == "": # '表头有是否中标,内容却空白的,过滤掉'
- continue
- if win_sort == "" and "tenderer" in headers and re.search('候选|入围|入选', headers['tenderer'][1]) and re.search('推荐的?((中标|成交|中选)候选人|(候选|入围|入选)供应商)', headers['tenderer'][1])==None and all_winner == False:
- tenderer = ""
- if tenderer in ['采购失败', '废标']: # 避免类似 353867205 这篇只提取到一个
- continue
- # tenderee = tenderee if self.is_role(tenderee) else ""
- # tenderer = tenderer if self.is_role(tenderer) else ""
- package = uniform_package_name(package_code) if package_code else '自增1' # 没有包号的自动编号的修改为提取到多少个包,某些行未必中标
- if package == '自增1' and project_name != '' and 'project_name' in headers and re.search('包[段组件]|标[段包的项]|标段(包)|分[包标]', headers['project_name'][1]): # 避免 266252226 采购项目名称 做多包多招标人 修复 661973750 标段名称为多包
- package = "自增%s"%str(len(prem_dic) + 1) if previous_project_name != project_name else "自增%s"%str(len(prem_dic))
- if project_name != "" and package.startswith('自增'):
- pk_l = find_package(project_name)
- if len(pk_l)==1:
- package = uniform_package_name(pk_l[0].group(0))
- elif re.search('[一二三四五六七八九十]+标段:|标段[一二三四五六七八九十]+:', tenderer) and package.startswith('自增'):
- pk_l = find_package(tenderer)
- if len(pk_l) == 1:
- package = uniform_package_name(pk_l[0].group(0))
- tenderee = get_role(tenderee, self.nlp_enterprise) if tenderee!="" else tenderee
- # tenderer = get_role(tenderer, self.nlp_enterprise) if tenderer!='' else tenderer
- if tenderer!='':
- leader, joint = get_td_companys(tenderer, self.nlp_enterprise)
- if leader == '':
- log('表格角色内容不在公司列表:%s,docid:%s' % (tenderer, self.docid))
- tenderer = leader
- tenderee = cut_repeat_name(tenderee)
- tenderer = cut_repeat_name(tenderer)
- if len(set([project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_])) < 2:
- break
- if not_package:
- if (project_code, package_code, tenderee, tenderer, budget_, bid_amount_) in link_set:
- continue
- link_set.add((project_code, package_code, tenderee, tenderer, budget_, bid_amount_))
- else:
- if (project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_) in link_set:
- continue
- link_set.add((project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_))
- if project_code != "":
- uni_project_code= uniform_package_name(project_code)
- if uni_project_code != "" and uni_project_code!=package:
- if package.startswith('自增'): # 没有包号有项目编号的,直接用项目编号
- package = uni_project_code
- else:
- # print('重组包号:', '%s_%s'%(uni_project_code, package))
- package = '%s_%s'%(uni_project_code, package) # 同时包号项目编号及包号的,组合起来做包号
- if package_code_raw!='':
- if multi_same_package == False and package not in package_fix2raw: # 如果处理后的标段号 已经在列表里面,采用原始标段号文本
- package_fix2raw[package] = package_code_raw
- elif same_package == False:
- multi_same_package = True
- if multi_same_package:
- package = package_code_raw
- if package not in prem_dic or not same_package:
- prem_dic[package] = {
- 'code': '',
- 'name': '',
- 'roleList': [],
- 'tendereeMoney': 0,
- 'tendereeMoneyUnit': ""
- }
- prem_dic[package]['code'] = project_code
- prem_dic[package]['name'] = project_name
- if budget_ != "":
- if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税|(六个月|一年|\w{2,3})期加点\d+BP', '', budget_)) > 5: # 金额字段出现超过5个非金额字符,中断匹配
- prem_dic.pop(package)
- break
- budget_header = headers['budget'][1] if 'budget' in headers else ''
- if amount_unit!='' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and re.search('元', budget_+budget_header)==None : # 20241128 补充某些表格价格单位分开两列, 例:557953660
- budget_ += amount_unit
- budget, money_unit = money_process(budget_, budget_header)
- if re.search('元[/每]', amount_unit) or re.search('单价', budget_header):
- unit_tendereeMoney = budget
- budget = 0
- else:
- unit_tendereeMoney = 0
- if (re.search('费率|[上下]浮率|[%%‰折]|优惠率|期加点\d+BP',
- budget_header + budget_) and budget < 100) or budget > 50000000000: # 如果是费率或大于500亿的金额改为0
- budget = 0
- if budget > 0:
- if same_package and prem_dic[package]['tendereeMoney'] != budget: # 处理 类似 136839070 一包多物品多预算
- prem_dic[package]['tendereeMoney'] += budget
- else:
- prem_dic[package]['tendereeMoney'] = budget
- prem_dic[package]['tendereeMoneyUnit'] = money_unit
- if unit_tendereeMoney > 0:
- if 'unit_tendereeMoney' not in prem_dic[package]:
- prem_dic[package]['unit_tendereeMoney'] = 0
- if same_package and prem_dic[package]['unit_tendereeMoney'] != unit_tendereeMoney: # 处理 类似 136839070 一包多物品多预算
- prem_dic[package]['unit_tendereeMoney'] += unit_tendereeMoney
- else:
- prem_dic[package]['unit_tendereeMoney'] = unit_tendereeMoney
- if tenderee and not same_package:
- prem_dic[package]['roleList'].append({
- "address": "",
- "linklist": [],
- "role_money": {
- "discount_ratio": "",
- "downward_floating_ratio": "",
- "floating_ratio": "",
- "money": 0,
- "money_unit": ""
- },
- "role_name": "tenderee",
- "role_text": tenderee,
- "serviceTime": ""
- })
- if tenderer:
- if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税|(六个月|一年|\w{2,3})期加点\d+BP', '',
- bid_amount_)) > 5: # 金额字段出现超过5个非金额字符,中断匹配
- prem_dic.pop(package)
- break
- bid_amount_header = headers['bid_amount'][1] if bid_amount_ != "" else ''
- if amount_unit != '' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and bid_amount_!='' and re.search('元',
- bid_amount_ + bid_amount_header) == None:
- bid_amount_ += amount_unit
- bid_amount, money_unit = money_process(bid_amount_, headers['bid_amount'][1]) if bid_amount_ != "" and 'bid_amount' in headers else (0, '')
- if re.search('[%%‰折]|浮率|期加点\d+BP', bid_amount_) and 0 < bid_amount < 100:
- bid_amount = 0
- if re.search('元[/每]', amount_unit) or re.search('单价', bid_amount_header):
- unit_price = bid_amount
- bid_amount = 0
- else:
- unit_price = 0
- if web_source_name == '河钢供应链管理平台' and 'bid_amount' in headers and re.search('[%%‰折]|浮率', bid_amount_) == None and bid_amount == 0: # 有中标金额字段却金额为0的过滤掉,防止类似 河钢供应链管理平台 站源错误,金额不为0的才算中标
- if len(prem_dic[package]['roleList']) == 0 and prem_dic[package]['tendereeMoney'] == 0: # 只有项目编号和名称的包 丢弃
- prem_dic.pop(package)
- continue
- elif 'bid_amount' in headers and re.search('[%%‰折]|浮率', bid_amount_) == None and have_bid_amount and bid_amount_ in ['/','','0','0.0']: # 如果不是所有行中标金额都为0,则把为0的做非中标
- if len(prem_dic[package]['roleList']) == 0 and prem_dic[package]['tendereeMoney'] == 0: # 只有项目编号和名称的包 丢弃
- prem_dic.pop(package)
- continue
- if (re.search('费率|下浮率|[%%‰折]|优惠率',
- bid_amount_header + bid_amount_) and bid_amount < 100) or bid_amount > 50000000000: # 如果是费率或大于500亿的金额改为0
- bid_amount = 0
- if serviceTime:
- serviceTime_text = headers['serviceTime'][1] + serviceTime if headers['serviceTime'][1][-1] in [':',':'] else headers['serviceTime'][1] + ':' + serviceTime
- # print('serviceTime_text',serviceTime_text)
- serviceTime = extract_servicetime(serviceTime_text)
- serviceTime.sort(key=lambda x:x.get('begin_index',0))
- serviceTime = extract_serviceTime(serviceTime[0]['body'],"") if serviceTime else ""
- # print(serviceTime)
- if not same_package or len(prem_dic[package]['roleList'])==0:
- role_dic = {
- "address": "",
- "linklist": [],
- "role_money": {
- "discount_ratio": "",
- "downward_floating_ratio": "",
- "floating_ratio": "",
- "money": bid_amount,
- "money_unit": money_unit
- },
- "role_name": "win_tenderer",
- "role_text": tenderer,
- "serviceTime": serviceTime
- }
- if joint != '':
- role_dic['win_tenderer_joint'] = joint
- if unit_price > 0:
- role_dic['role_money']['unit_price'] = unit_price
- prem_dic[package]['roleList'].append(role_dic)
- elif prem_dic[package]['roleList'] and prem_dic[package]['roleList'][-1].get('role_name', '')=='win_tenderer':
- if 'multi_winner' not in prem_dic[package]['roleList'][-1]:
- prem_dic[package]['roleList'][-1]['multi_winner'] = prem_dic[package]['roleList'][-1]['role_text']
- prem_dic[package]['roleList'][-1]['multi_winner'] += ','+ tenderer
- elif tenderer not in prem_dic[package]['roleList'][-1]['multi_winner']:
- prem_dic[package]['roleList'][-1]['multi_winner'] += ','+ tenderer
- if bid_amount != 0 or unit_price > 0: # 有中标金额的才放进去
- if 'other_winner_dic' not in prem_dic[package]['roleList'][-1]:
- prem_dic[package]['roleList'][-1]['other_winner_dic'] = []
- prem_dic[package]['roleList'][-1]['other_winner_dic'].append({'role_text': tenderer, "money": bid_amount, "money_unit": money_unit, "serviceTime": serviceTime})
- tenderer_list.append(tenderer)
- serviceTime_list.append(serviceTime)
- if len(prem_dic[package]['roleList']) == 0 and prem_dic[package]['tendereeMoney'] == 0: # 只有项目编号和名称的 丢弃 并不再继续往下匹配
- prem_dic.pop(package)
- # break # 注释掉避免 400084571 某些包废标 中断匹配
- if multi_same_package: # 预处理后包号重复的,使用原始包号
- for k, v in package_fix2raw.items():
- if k in prem_dic:
- prem_dic[v] = prem_dic.pop(k)
- previous_package = package_code + project_code
- previous_project_name = project_name
- if len(tenderer_list)>2 and len(set(tenderer_list))==1 and "package_code" not in headers: # 没提取到包号且中标人一样应该是错误多包,需去掉多包 例 244355092 281854766
- total_money = 0
- for v in prem_dic.values():
- for d in v['roleList']:
- if d['role_name'] == "win_tenderer":
- total_money += d['role_money']['money']
- if 'other_winner_dic' in d:
- for other in d['other_winner_dic']:
- total_money += other.get('money', 0)
- return {'自增1': {
- 'code': '',
- 'name': '',
- 'roleList': [{
- "address": "",
- "linklist": [],
- "role_money": {
- "discount_ratio": "",
- "downward_floating_ratio": "",
- "floating_ratio": "",
- "money": total_money,
- "money_unit": ''
- },
- "role_name": "win_tenderer",
- "role_text": tenderer_list[0],
- "serviceTime": serviceTime_list[0]
- }],
- 'tendereeMoney': 0,
- 'tendereeMoneyUnit': ""
- }}
- return prem_dic
- def update_prem(self, rs_dic, tmp_dic):
- '''
- 合并更新 prem
- :param rs_dic: 返回结果
- :param tmp_dic: 待合并结果
- :return:
- '''
- if '自增1' in tmp_dic and '自增1' not in rs_dic and len(tmp_dic)==len(rs_dic):
- pass
- else:
- for pack in tmp_dic:
- if pack in rs_dic:
- for k in tmp_dic[pack]:
- if rs_dic[pack][k] in ['', 0]:
- rs_dic[pack][k] = tmp_dic[pack][k]
- elif rs_dic[pack][k] == []:
- rs_dic[pack][k] = tmp_dic[pack][k]
- elif k == 'roleList' and len(rs_dic[pack][k])>0 and rs_dic[pack][k][0].get('role_money', {}).get('money', 0) == 0:
- rs_dic[pack][k] = tmp_dic[pack][k]
- else:
- rs_dic[pack] = tmp_dic[pack]
- def get_prem(self, soup, web_source_name='', all_winner=False):
- tables = soup.find_all('table')
- tables.reverse()
- rs_dic = {}
- for table in tables:
- text = table.text.strip()
- pre_text = ""
- previous = None
- if table.findPreviousSibling() != None:
- previous = table.findPreviousSibling()
- pre_text = previous.text.strip()
- if pre_text == "" and table.findPreviousSibling().findPreviousSibling() != None: # 修复表格前一标签没内容,再前一个才有内容情况
- previous = table.findPreviousSibling().findPreviousSibling()
- pre_text = previous.text.strip()
- if re.search('项目业主|业\s*主', text) and re.search('业\s*绩', text+pre_text): # 包含业绩的表格过滤掉,不进行处理
- tb_ex = table.extract()
- if previous:
- sib = previous.extract()
- continue
- trs = self.tb.table2list(table)
- # table.extract()
- i = 0
- headers = ""
- table_prem = {}
- while i < len(trs) - 1:
- flag_, contain_header_, headers_, not_sure_winner = self.find_header(trs[i], all_winner, first_line=i==0)
- if flag_ and 'tenderer' in headers_ and not_sure_winner and re.search('中标|成交|中选|入围|入选', pre_text)==None:
- # print('过滤:',headers_)
- flag_ = False
- headers_ = {}
- if flag_ and headers_ != dict():
- table_items = []
- headers = headers_
- for j in range(i + 1, len(trs)):
- if len(trs[j]) == len(trs[i]):
- flag_2, contain_header_2, headers_2, not_sure_winner = self.find_header(trs[j], all_winner)
- if flag_2 or contain_header_2:
- if j == i+1 and flag_2:
- if len(headers_)<=len(headers_2):
- headers = headers_2
- continue
- elif trs[i] == trs[j]: # 修复表格重复表头多次出现情况 例:514890585
- continue
- break
- elif ''.join(trs[j]).strip() == '': # 修复整行为空的 例:514890585
- continue
- else:
- table_items.append(trs[j])
- else:
- # print('表头,内容 列数不一致', len(trs[i]), len(trs[j]))
- break
- if len(table_items) > 0:
- df = pd.DataFrame(table_items)
- prem_ = self.extract_from_df(df, headers, web_source_name, all_winner)
- # rs_dic.update(prem_)
- # table_prem.update(prem_)
- self.update_prem(table_prem, prem_)
- i = j - 1
- i += 1
- 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(): # 表格内没有标段的,从上一个兄弟标签找标段
- sib = table.find_previous_sibling()
- sib_text = sib.get_text()
- ser_sib = re.search('第?[0-9一二三四五六七八九十a-zA-Z]{1,4}(标[段号的包项]|([分子]?包|包[组件号]))|(标[段号的包项]|([分子]?包|包[组件号]))号?:?[0-9一二三四五六七八九十a-zA-Z]{1,4}|包名:[0-9一二三四五六七八九十]{1,4}', sib_text)
- if sib.name in ['p','div','dl','ol','ul','h1','h2','h3','h4','h5','h6'] and len(sib_text)<100 and ser_sib:
- package_sib = ser_sib.group(0)
- package_sib = uniform_package_name(package_sib)
- table_prem[package_sib] = table_prem.pop('自增1')
- if table_prem:
- # rs_dic.update(table_prem)
- self.update_prem(rs_dic, table_prem)
- table.extract()
- return rs_dic
- def predict(self, html, nlp_enterprise, web_source_name="", all_winner=False, docid=''):
- html = re.sub("<html>|</html>|<body>|</body>","",html)
- html = re.sub("##attachment##","",html)
- soup = BeautifulSoup(html, 'lxml')
- richText = soup.find(name='div', attrs={'class': 'richTextFetch'})
- self.nlp_enterprise = sorted(nlp_enterprise, key=lambda x:len(x), reverse=True)
- self.docid = docid
- in_attachment = False
- if richText:
- richText = richText.extract() # 过滤掉附件
- del_tabel_achievement(soup) # 20240819 过滤掉业绩表格
- prem = self.get_prem(soup, web_source_name, all_winner)
- if prem == {} and richText:
- del_tabel_achievement(richText) # 20240819 过滤掉业绩表格
- prem = self.get_prem(richText, web_source_name, all_winner)
- in_attachment = True
- if len(prem) == 1: # 只有一个包且包号为1 或 长度大于2 的大概率为自动增加编号包,改为Project
- k = list(prem)[0]
- if k.startswith('自增'):
- prem['Project'] = prem.pop(k)
- return prem, in_attachment
- class CandidateExtractor(object):
- def __init__(self):
- '''各要素表头规则'''
- self.head_rule_dic = {
- 'package_code': "(包[段组件]|标[段包]|分[包标])(序?号|$)|包号|^标段$",
- 'project_code': "(项目|招标|采购|计划|公告|包[段组件]|标[段包的]|标段(包)|分[包标])(编号|编码)",
- "project_name": "(包[段组件]|标[段包的项]|标段(包)|分[包标]|采购|项目|工程|货物|商品|产品|设备|通用|主要标的|^包)(名称?|内容)|^标的$",
- "win_sort": "排名|排序|名次|中标及备选结果|(中标|成交|中选|推荐)(次序|顺序)",
- 'win_or_not': '是否(建议|推荐)?(中标|成交)|是否入围|是否入库|入围结论|^选择设备$', # 补充站源特别表达:例:577351909 选择设备 1 为中标 0 非中标
- "candidate": "((候选|入围|入选|投标|应答|响应|参选|比选|参与)(供应商库)?的?(人|人?单位|机构|供应商|供货商|服务商|投标人|申请人|(中标)?公司|(中标)?企业|银行)|(通过)?名单|中标候选人)(名称|名单|全称|\d)?$|^(推荐的?)?供应商(名称|信息)?$|投标个人/单位|^(公司|单位)名称$|供应商单位名称$", #补充 368295593 投标个人/单位 提取
- "bid_amount": "投标[报总]?价|报价(总?金额|总价|总额)|总报价|^\w{,5}报价(([\w、/]{1,15}))?$|(中标|成交|合同)(不?含税|\w{,3}?))?([金总]额|[报均总]价|价[格款]?)|承包价|含税价|经评审的价格",
- "win_tenderer": "第[一1]名|第[一1]预?(中标|成交)?候选人",
- "second_tenderer": "第[二2]名|第[二2]预?(中标|成交)?候选人",
- "third_tenderer": "第[三3]名|第[三3]预?(中标|成交)?候选人",
- }
- '''非表格候选人正则'''
- # self.p = '((候选|入围|入选|投标)(供应商库)?的?(人|人?单位|机构|供应商|供货商|服务商|投标人|(中标)?公司|(中标)?企业|应答人)|(通过)?名单)(名称|名单|全称|\d)?:$'
- self.p = '((候选|入围|入选|投标|报价|成交|中标|中选|供[货应]|应答|响应)(人|方|人?单位|机构|厂?商|商家|服务商|公司|企业)|(通过|入围)名单)(名称|名单|全称|\d)?[是为:]?$'
- self.tb = TableTag2List()
- with open(INTERFACE_DIR+'/header_set.pkl', 'rb') as f:
- self.headerset = pickle.load(f)
- # self.headerset.update(set(['异议受理部门', '异议受理部门联系人', '招标投标监督部门']))
- # with open(os.path.dirname(__file__)+'/header_set.pkl', 'wb') as f:
- # pickle.dump(self.headerset, f)
- def find_header(self, td_list):
- fix_td_list = [re.sub('[::]$|^[一二三四五六七八九十0-9]{1,3}、|(([\w、×*/]{1,20}))$|(不?含税)|/万?元|拟|\s', '', it) for it in td_list] # 去除表头无关信息,方便匹配判断是否为表头
- header_dic = dict()
- flag = False
- contain_header = False
- 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)):
- flag = True
- for i in range(len(td_list)) :
- text = td_list[i]
- text = re.sub('\s|[((]排名不分先后[))]', '', text)
- if len(text) > 15: # 长度大于15 不进行表头匹配
- continue
- if re.search('未(中标|成交)原因', text): # 不提取此种表格
- return flag, contain_header, dict()
- num = 0
- for k, v in self.head_rule_dic.items():
- if k == 'candidate' and re.search('第[一二三]名|第[一二三]预?(中标|成交)?候选人', text):
- continue
- if re.search('评分|得分|分数|分值', text) and re.search('排名|排序', text)==None: # 692295071 综合得分排序
- continue
- if re.search(v, text):
- if k in ['candidate', 'win_tenderer', 'second_tenderer', 'third_tenderer'] and re.search('是否', text):
- continue
- elif k == 'win_or_not' and re.search('是否(中标|成交)候选人', text): # 修复 584112560 把第二作第一错误
- continue
- elif k in header_dic:
- if k in ['bid_amount'] and re.search('总(价|金?额)', text): # 总价替换单价
- header_dic[k] = (i, text)
- num += 1
- elif k == 'project_code' and text != header_dic[k][1] and 'package_code' not in header_dic\
- and re.search(self.head_rule_dic['package_code'], re.sub('\s', '', ','.join(td_list)))==None: # 如果出现两次项目编号且没有包号,把第二次出现的作为包号 例:472537470
- header_dic['package_code'] = (i, text)
- elif text == header_dic[k][1]: # 表头相同取后一个
- header_dic[k] = (i, text)
- continue
- header_dic[k] = (i, text)
- # if k != 'candidate': # candidate 可与前三候选重复
- num += 1
- if 'win_tenderer'in header_dic and 'second_tenderer' in header_dic and 'candidate' in header_dic:
- header_dic.pop('candidate')
- if num>1:
- # print('表头错误,一个td匹配到两个表头:', header_dic)
- return flag, contain_header, dict()
- if text == '单位': # 20241128 补充金额单位
- header_dic['amount_unit'] = (i, text)
- if ('candidate' in header_dic and 'win_sort' in header_dic) or ('win_tenderer' in header_dic and 'second_tenderer' in header_dic): # 有排名才返回表头进行提取
- return flag, contain_header, header_dic
- 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 单位名称 中标及备选结果
- contain_header = True
- if len(set(fix_td_list) & self.headerset) == 1:
- td_text = (set(fix_td_list) & self.headerset).pop()
- if re.match('第[一二三\d](名|(中标|成交)?候选人)$', td_text):
- contain_header = False
- return flag, contain_header, dict()
- def is_role(self, text):
- if len(text) > 25 or len(text) < 4:
- return False
- elif len(re.findall('有限责?任?公司', text)) > 1:
- return False
- elif re.search('[\w()]{4,}(有限责?任?公司|学校|学院|大学|中学|小学|医院|管理处|办公室|委员会|村委会|纪念馆|监狱|管教所|修养所|社区|农场|林场|羊场|猪场|石场|村|幼儿园|厂|中心|超市|门市|商场|工作室|文印室|城|部|店|站|馆|行|社|处)$', text):
- return True
- else:
- ners = selffool.ner(text)
- if len(ners[0]) == 1 and ('company' in ners[0][0] or 'org' in ners[0][0]):
- return True
- return False
- def extract_from_df(self, df, headers):
- prem_dic = {}
- link_set = set()
- candidate_set = set()
- role_dic = dict() # 保存一二三候选人并排的情况
- findtop3 = False
- findmoney = False
- line_num = 0
- line_package = None
- package_flag = 0
- if "package_code" in headers:
- package_flag = 1
- if len(df)!=len(set(df[headers["package_code"][0]])): # 如果有包号但重复,进行下列判断是否和跟其他字段组合包号
- if "project_code" in headers and df[headers["project_code"][0]][0] != df[headers["package_code"][0]][0]:
- package_flag = 2
- elif "project_name" in headers and find_package(df[headers["package_code"][0]][0]):
- package_flag = 3
- for i in df.index:
- package_code_raw = df.loc[i, headers['package_code'][0]].strip().replace(' ', '') if "package_code" in headers else ""
- project_code = df.loc[i, headers['project_code'][0]].strip().replace(' ', '') if "project_code" in headers else ""
- project_name = df.loc[i, headers['project_name'][0]].strip().replace(' ', '') if "project_name" in headers else ""
- candidate_ = df.loc[i, headers['candidate'][0]].strip().replace(' ', '') if "candidate" in headers else ""
- win_or_not = df.loc[i, headers['win_or_not'][0]].strip().replace(' ', '') if "win_or_not" in headers else ""
- # budget_ = df.loc[i, headers['budget'][0]] if "budget" in headers else ""
- bid_amount_ = df.loc[i, headers['bid_amount'][0]].strip().replace(' ', '') if "bid_amount" in headers else ""
- win_sort = df.loc[i, headers['win_sort'][0]].strip().replace(' ', '') if "win_sort" in headers else ""
- win_tenderer = df.loc[i, headers['win_tenderer'][0]].strip().replace(' ', '') if "win_tenderer" in headers else ""
- second_tenderer = df.loc[i, headers['second_tenderer'][0]].strip().replace(' ', '') if "second_tenderer" in headers else ""
- third_tenderer = df.loc[i, headers['third_tenderer'][0]].strip().replace(' ', '') if "third_tenderer" in headers else ""
- amount_unit = df.loc[i, headers['amount_unit'][0]].strip().replace(' ', '') if "amount_unit" in headers else ""
- joint = ''
- if set([package_code_raw, candidate_, win_or_not, bid_amount_, win_tenderer, second_tenderer, third_tenderer]) & self.headerset != set(): # 包含表头, 停止匹配 # 排除 ,win_sort 避免367940050漏提取
- # print('包含表头, 停止匹配')
- break
- if len(set([package_code_raw, candidate_,win_sort, win_or_not, bid_amount_, win_tenderer, second_tenderer, third_tenderer]) - set(['', ' '])) < 2: # 全部为空或内容一样 停止匹配
- # print('全部为空或内容一样 停止匹配')
- 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):
- line_package = win_sort
- continue
- elif win_sort == '' and candidate_ != '': # 修复 696375822 前几行为空
- continue
- else:
- break
- if candidate_ != "" and win_sort == "" and headers['candidate'][0] > 0: # 修复某些表头不说 排名,直接用候选人代替
- col_indx = headers['candidate'][0] -1
- pre_col = df.loc[i, col_indx]
- if col_indx > 0 and pre_col == candidate_:
- pre_col = df.loc[i, col_indx - 1]
- if re.search('第[一二三]名|第[一二三](中标)?候选人', pre_col):
- win_sort = pre_col
- package_code = package_code_raw
- if package_code == '' and line_package:
- package_code = line_package
- # candidate = candidate_ if self.is_role(candidate_) else ""
- # tenderer = tenderer if self.is_role(tenderer) else ""
- # candidate = get_role(candidate_, self.nlp_enterprise)
- if candidate_!='':
- leader, joint = get_td_companys(candidate_, self.nlp_enterprise)
- if leader == '':
- log('表格角色内容不在公司列表:%s,docid:%s' % (candidate_, self.docid))
- candidate = leader
- else:
- candidate = candidate_
- # if len(set([project_code, package_code, project_name, tenderee, tenderer, budget_, bid_amount_])) < 2:
- # break
- if(candidate_,win_tenderer, second_tenderer,third_tenderer, bid_amount_,package_code,project_code,win_sort) in link_set:
- continue
- link_set.add((candidate_, win_tenderer, second_tenderer, third_tenderer, bid_amount_,package_code,project_code,win_sort))
- package = package_code
- if package == "" and project_code != "": # 修复 395747178 多项目 只提取到一个
- package = project_code
- package = uniform_package_name(package) if package !="" else "Project"
- if package_flag == 2 and project_code != "":
- project_code_pk = uniform_package_name(project_code)
- package = "%s_%s"%(project_code_pk, package)
- elif package_flag == 3 and project_name != "":
- for iter in find_package(project_name):
- project_name_pk = uniform_package_name(iter.group(0))
- package = "%s_%s"%(project_name_pk, package)
- break
- if candidate:
- if win_or_not and re.search('否|未入围', win_or_not):
- candidate_set.add(candidate)
- # elif re.search('^((建议|推荐)(中标|成交)|是)$', win_or_not) and win_sort in ['', '参与投标单位及排名'] and win_tenderer=='':
- # win_sort = '第一名'
- # candidate_set.add(candidate)
- else:
- candidate_set.add(candidate)
- if win_tenderer and second_tenderer: # and third_tenderer 128778062 这篇只有 第一二候选人
- if re.search("(候选人|投标人|单位|公司)名?称?$", df.loc[i, 0]) or re.search("(候选人|投标人|单位|公司)名?称?", df.loc[i, 1]):
- findtop3 = True
- for type, text in zip(['win_tenderer', 'second_tenderer', 'third_tenderer'],
- [win_tenderer, second_tenderer, third_tenderer]):
- # text = get_role(text, self.nlp_enterprise)
- leader, joint = get_td_companys(text, self.nlp_enterprise)
- if leader == '':
- log('表格角色内容不在公司列表:%s,docid:%s' % (text, self.docid))
- text = leader
- if text:
- # if self.is_role(text):
- if type not in role_dic:
- role_dic[type] = dict()
- role_dic[type]['role_text'] = text
- candidate_set.add(text)
- if joint:
- role_dic[type]['win_tenderer_joint'] = joint
- elif re.search('投标报价|报价$', df.loc[i, 0]) or re.search('投标报价|报价$', df.loc[i, 1]):
- findmoney = True
- header = df.loc[i, 0] if re.search('投标报价|报价$', df.loc[i, 0]) else df.loc[i, 1]
- for type, text in zip(['win_tenderer', 'second_tenderer', 'third_tenderer'],
- [win_tenderer, second_tenderer, third_tenderer]):
- if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税', '',
- text)) > 5: # 金额字段出现超过5个非金额字符,中断匹配
- break
- if amount_unit != '' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and re.search('元', text+header)==None: # 补充另外在一列的金额单位
- text += amount_unit
- money, money_unit = money_process(text, header)
- if re.search('元[/每]', amount_unit) or re.search('单价', header):
- unit_price = money
- money = 0
- else:
- unit_price = 0
- if (re.search('费率|下浮率|[%%‰折]|优惠率', header+text) and money < 100) or money > 50000000000: # 如果是费率或大于500亿的金额改为0
- money = 0
- if money > 0:
- if type not in role_dic:
- role_dic[type] = dict()
- role_dic[type]['money'] = money
- role_dic[type]['money_unit'] = money_unit
- if unit_price > 0:
- if type not in role_dic:
- role_dic[type] = dict()
- role_dic[type]['unit_price'] = unit_price
- role_dic[type]['money_unit'] = money_unit
- else:
- line_num += 1
- if findtop3 and findmoney:
- break
- if line_num > 3:
- break
- elif candidate and win_sort:
- role_type = ""
- if re.search('第[一1]|^([一1]|01|中标人)$', win_sort) and re.search('第一备选人', win_sort)==None:
- role_type = "win_tenderer"
- if win_or_not in ['否', '未中标', '0']: # 修复特别站源表达 577351909 选择设备:0 不是中标
- role_type = ''
- elif re.search('第[二2]|^([二2]|02|第一备选人)$', win_sort) and re.search('第二备选人', win_sort)==None:
- role_type = "second_tenderer"
- if win_or_not in ['是', '1']:
- role_type = "win_tenderer"
- elif re.search('第[三3]|^([三3]|03|第二备选人)$', win_sort):
- role_type = "third_tenderer"
- if role_type != "":
- if package not in prem_dic:
- prem_dic[package] = {
- 'code': '',
- 'name': '',
- 'roleList': [],
- 'tendereeMoney': 0,
- 'tendereeMoneyUnit': ""
- }
- prem_dic[package]['code'] = project_code
- prem_dic[package]['name'] = project_name
- if len(re.sub('[金额万元()()::零壹贰叁肆伍陆柒捌玖拾佰仟萬億圆十百千万亿元角分¥整\s\d,.]|人民币|不?含税', '', bid_amount_))> 5: # 金额字段出现超过5个非金额字符,中断匹配
- break
- header = headers['bid_amount'][1] if "bid_amount" in headers else ''
- if amount_unit != '' and re.search('^[万亿]?元|%|折[\w/]{,6}$', amount_unit) and re.search('元',
- bid_amount_ + header) == None: # 补充另外在一列的金额单位
- bid_amount_ += amount_unit
- bid_amount, money_unit = money_process(bid_amount_, headers['bid_amount'][1]) if "bid_amount" in headers else (0, "")
- if re.search('元[/每]', amount_unit) or re.search('单价', header):
- unit_price = bid_amount
- bid_amount = 0
- else:
- unit_price = 0
- if (re.search('费率|下浮率|[%%‰折]|优惠率',
- header + bid_amount_) and bid_amount < 100) or bid_amount > 50000000000: # 如果是费率或大于500亿的金额改为0
- bid_amount = 0
- tmp_role_dic = {
- "address": "",
- "linklist": [],
- "role_money": {
- "discount_ratio": "",
- "downward_floating_ratio": "",
- "floating_ratio": "",
- "money": bid_amount,
- "money_unit": money_unit
- },
- "role_name": role_type,
- "role_text": candidate,
- "serviceTime": ""
- }
- if joint != '':
- tmp_role_dic['win_tenderer_joint'] = joint
- if unit_price > 0:
- tmp_role_dic['role_money']['unit_price'] = unit_price
- if role_type in str(prem_dic[package]['roleList']): # 一种角色只保留一个,解决 682633825 有多个第二名情况
- continue
- prem_dic[package]['roleList'].append(tmp_role_dic)
- if len(prem_dic[package]['roleList']) == 0: # 只有项目编号和名称的 丢弃
- prem_dic.pop(package)
- if role_dic and prem_dic == dict():
- if package not in prem_dic:
- prem_dic[package] = {
- 'code': '',
- 'name': '',
- 'roleList': [],
- 'tendereeMoney': 0,
- 'tendereeMoneyUnit': ""
- }
- for role_type, v in role_dic.items():
- role_text = v.get('role_text', '')
- if role_text == "":
- continue
- money = v.get('money', 0)
- money_unit = v.get('money_unit', '')
- tmp_d = {
- "address": "",
- "linklist": [],
- "role_money": {
- "discount_ratio": "",
- "downward_floating_ratio": "",
- "floating_ratio": "",
- "money": money,
- "money_unit": money_unit
- },
- "role_name": role_type,
- "role_text": role_text,
- "serviceTime": ""
- }
- if 'win_tenderer_joint' in v:
- tmp_d['win_tenderer_joint'] = v['win_tenderer_joint']
- prem_dic[package]['roleList'].append(tmp_d)
- if len(prem_dic[package]['roleList']) == 0: # 只有项目编号和名称的 丢弃
- prem_dic.pop(package)
- return prem_dic, candidate_set
- def get_prem(self, soup):
- tables = soup.find_all('table')
- tables.reverse()
- rs_dic = {}
- candidate_set = set()
- for table in tables:
- trs = self.tb.table2list(table)
- i = 0
- headers = ""
- while i < len(trs) - 1:
- flag_, contain_header_, headers_ = self.find_header(trs[i])
- if flag_ and headers_ != dict():
- table_items = []
- headers = headers_
- for j in range(i + 1, len(trs)):
- if len(trs[j]) == len(trs[i]):
- flag_, contain_header_, headers_ = self.find_header(trs[j])
- if flag_ or contain_header_:
- break
- else:
- table_items.append(trs[j])
- else:
- # print('表头,内容 列数不一致', len(trs[i]), len(trs[j]))
- break
- # print('内容:', len(table_items), table_items)
- if len(table_items) >= 1:
- df = pd.DataFrame(table_items)
- # print('候选人表格行数:', len(df))
- prem_, candidate_set_ = self.extract_from_df(df, headers)
- # print('prem_: ', prem_)
- rs_dic.update(prem_)
- candidate_set.update(candidate_set_)
- i = j - 1
- i += 1
- if rs_dic and 'package_code' not in headers and 'Project' in rs_dic and (table.find('caption') or table.find_previous_sibling()): # 一个表格只有两行且没有标段的,从上一个兄弟标签找标段
- sib = table.find('caption') or table.find_previous_sibling() # 693211745 table下是caption标签写标段信息
- sib_text = sib.get_text().strip()
- ser_sib = re.search('第?[0-9一二三四五六七八九十a-zA-Z]{1,4}(标[段号的包项]|([分子]?包|包[组件号]))|(标[段号的包项]|([分子]?包|包[组件号]))号?:?[0-9一二三四五六七八九十a-zA-Z]{1,4}|包名:[0-9一二三四五六七八九十]{1,4}', sib_text)
- if sib.name in ['p', 'div', 'caption'] and len(sib_text)<100 and ser_sib:
- package_sib = ser_sib.group(0)
- package_sib = uniform_package_name(package_sib)
- rs_dic[package_sib] = rs_dic.pop('Project')
- table.extract()
- return rs_dic, candidate_set
- def get_candidates_from_text(self, list_sentences, list_entitys):
- candidates = set()
- tenderee_or_agency = set()
- sentences = sorted(list_sentences[0], key=lambda x: x.sentence_index)
- for ent in list_entitys[0]:
- if ent.entity_type in ['org', 'company']:
- sen_index = ent.sentence_index
- text = sentences[sen_index].sentence_text
- b = ent.wordOffset_begin
- e = ent.wordOffset_end
- if ent.label in [2,3,4]: # 直接加实体预测的候选人, 否则规则检查是否为候选人
- candidates.add(ent.entity_text)
- elif isinstance(b, int) and isinstance(e, int) and ent.label in [5]:
- foreword = text[max(0, b - 10):b]
- if re.search(self.p, foreword):
- candidates.add(ent.entity_text)
- if ent.label in [0, 1] and ent.values[ent.label]>0.5:
- tenderee_or_agency.add(ent.entity_text)
- candidates -= tenderee_or_agency # 2024/05/10 463166661 把 四川省第二中医医院作为候选人 过滤掉为招标或代理角色 的候选人
- return candidates
- def predict(self, html, list_sentences, list_entitys, nlp_enterprise, docid=''):
- self.nlp_enterprise = sorted(nlp_enterprise, key=lambda x:len(x), reverse=True)
- self.docid = docid
- html = html.replace('比选申请单位', '中标候选人') # 82347769
- html = re.sub("<html>|</html>|<body>|</body>","",html)
- html = re.sub("##attachment##","",html)
- soup = BeautifulSoup(html, 'lxml')
- richText = soup.find(name='div', attrs={'class': 'richTextFetch'})
- in_attachment = False
- if richText:
- richText = richText.extract() # 过滤掉附件
- del_tabel_achievement(soup) # 20240819 过滤掉业绩表格 例:500817166
- prem, candidate_set = self.get_prem(soup)
- if prem == {} and richText:
- del_tabel_achievement(richText) # 20240819 过滤掉业绩表格
- prem, candidate_set = self.get_prem(richText)
- in_attachment = True
- candidate_set2 = self.get_candidates_from_text(list_sentences, list_entitys)
- candidate_set.update(candidate_set2)
- return prem, {'candidate': ','.join(candidate_set)}, in_attachment
- def get_header_line(list_item):
- '''
- 判断列表内文本哪些是表头,哪些不是
- :param list_item: [ '批复结果', '许可/同意', '批复文号',]
- :return:
- '''
- rs = []
- x = []
- for item in list_item:
- if len(item.strip()) > 30: # 大于30字不可能是表头,直接替换为无意义字符
- item = 'xxx'
- x.append(getPredictor("form").encode(item))
- predict_y = getPredictor("form").predict(np.array(x), type="item")
- for item, values in zip(list_item, list(predict_y)):
- item = str(item).replace(' ', '')
- lb = 1 if values[1] > 0.5 else 0
- if item in ['许可/同意', '办结(通过)', '办结(准予许可)','批准', '合格', '民间投资', '备案','综合排序第一','满足采购文件要求','未否决']:
- lb = 0
- elif item in ['环境影响评价机构', '建设单位或地方政府作出的相关环保承诺', '环境影响评价技术服务机构', '报告全本'] or re.search('^比例\d{1,2}%$', item):
- lb = 1
- elif lb == 0 and item in header_set:
- lb = 1
- rs.append(lb)
- return rs
|