2 Commits e7ee0b3a91 ... 9de21e9aa7

Author SHA1 Message Date
  lsm 9de21e9aa7 Merge remote-tracking branch 'origin/master' 3 weeks ago
  lsm d885c281d5 优化表格业绩清除;更新地区提取;角色金额优化;大纲提取补充 3 weeks ago

+ 7 - 4
BiddingKG/dl/common/Utils.py

@@ -984,7 +984,7 @@ package_number_pattern = re.compile(
 |(([a-zA-Z]包[:()]?)?第?([一二三四五六七八九十]{1,3}|[ⅠⅡⅢⅣⅤⅥⅦ]{1,3}|[a-zA-Z0-9]{1,9}\-?[a-zA-Z0-9-]{,9})[分子]?(标[段包项]?|合同[包段]))\
 |(([,;。、:(]|第)?([一二三四五六七八九十]{1,3}|[ⅠⅡⅢⅣⅤⅥⅦ]{1,3}|[a-zA-Z0-9]{1,9}\-?[a-zA-Z0-9-]{,9})[分子]?(标[段包项]?|包[组件标]?|合同[包段]))\
 |((标[段包项]|品目|标段(包)|包[组件标]|[标分子(]包)(\[|【)?:?([一二三四五六七八九十]{1,3}|[ⅠⅡⅢⅣⅤⅥⅦ]{1,3}|[a-zA-Z0-9]{1,9}\-?[a-zA-Z0-9-]{,9}))\
-|([,;。、:(]|^)(标的?|(招标|采购)?项目|子项目?)(\[|【)?:?([一二三四五六七八九十]+|[0-9]{1,9})\
+|([,;。、:(]|^)(标的?|(招标|采购)?项目|子项目?|采购包()(\[|【)?:?([一二三四五六七八九十]+|[0-9]{1,9})\
 |((([标分子(]|合同|项目|采购)包|[,。]标的|子项目|[分子]标|标[段包项]|包[组件标]?)编?号[::]?[a-zA-Z0-9一二三四五六七八九十ⅠⅡⅢⅣⅤⅥⅦ]{1,9}[a-zA-Z0-9一二三四五六七八九十ⅠⅡⅢⅣⅤⅥⅦ-]{0,9})\
 |[,;。、:(]?(合同|分|子)?包:?([一二三四五六七八九十]{1,3}|[ⅠⅡⅢⅣⅤⅥⅦ]{1,3}|[a-zA-Z0-9]{1,9}\-?[a-zA-Z0-9-]{,9})')
 filter_package_pattern =  'CA标|(每个?|所有|相关|个|各|不分)[分子]?(标[段包项]?|包[组件标]?|合同包)|(质量|责任)三包|包[/每]|标段(划分|范围)|(承|压缩|软|皮|书|挂)包\
@@ -1058,9 +1058,9 @@ def cut_repeat_name(s):
     return s
 
 def del_tabel_achievement(soup):
-    if re.search('中标|成交|入围|结果|评标|开标|候选人', soup.text[:800]) == None or re.search('业绩', soup.text)==None:
+    if re.search('中标|成交|入围|结果|评标|开标|候选人', soup.text[:800]) == None or re.search('业绩|类似项目', soup.text)==None:
         return None
-    p1 = '(中标|成交)(单位|候选人)的?(企业|项目|项目负责人|\w{,5})?业绩|类似(项目)?业绩|\w{,10}业绩$|业绩(公示|情况|荣誉)'
+    p1 = '(中标|成交)(单位|候选人)的?(企业|项目|项目负责人|\w{,5})?业绩|类似(项目)?业绩|\w{,10}业绩$|业绩(公示|情况|荣誉)|近年完成的项目类似项目情况表|类似项目'
     '''删除前面标签 命中业绩规则;当前标签为表格且公布业绩相关信息的去除'''
     for tag in soup.find_all('table'):
         pre_text = ""
@@ -1070,6 +1070,9 @@ def del_tabel_achievement(soup):
                 pre_text = tag.findPreviousSibling().findPreviousSibling().text.strip()
 
         tr_text = tag.find('tr').text.strip() if tag.find('tr') != None else ""
+        if len(pre_text) < 20 and re.search('^[((]?[\d一二三四五六七八九十]+[)、)]近年完成的(项目)?类似项目情况表|类似项目历史成交信息|类似项目的?采购预算', pre_text): # 删除 650469910 评标公告 历史业绩 547305313
+            del_tag = tag.extract()
+            # print('删除表格业绩内容', del_tag.text)
         #     print(re.search(p1, pre_text),pre_text, len(pre_text), re.findall('序号|中标候选人名称|项目名称|工程名称|合同金额|建设单位|业主', tr_text))
         if re.search(p1, pre_text) and len(pre_text) < 20 and tag.find('tr') != None and len(tr_text)<100:
             _count = 0
@@ -1077,7 +1080,7 @@ def del_tabel_achievement(soup):
                 td_text = td.text.strip()
                 if len(td_text) > 25:
                     break
-                if len(td_text) < 25 and re.search('中标候选人|第[一二三四五1-5]候选人|(项目|业绩|工程)名称|\w{,10}业绩$|合同金额|建设单位|采购单位|业主|甲方', td_text):
+                if len(td_text) < 25 and re.search('中标候选人|第[一二三四五1-5]候选人|(项目|业绩|工程)名称|\w{,10}业绩$|合同(金额|价格)|建设单位|采购单位|业主|甲方|发包人', td_text):
                     _count += 1
                 if _count >=2:
                     pre_tag = tag.findPreviousSibling().extract()

+ 25 - 18
BiddingKG/dl/interface/Preprocessing.py

@@ -719,7 +719,8 @@ def tableToText(soup, docid=None, return_kv=False):
                                                    "发行时间" ,"批次" ,"发行额" ,"发行利率" ,"所属债券" ,"专项债作资本金发行额" ,"调整记录","资产面积",
                                                    "交易底价","交易地点"] and predict_list[i][j]!=1:
                     inner_table[i][j] = [origin_inner_table[i][j][0], 1]
-                elif predict_list[i][j]!=1 and re.search('^(中标|中选|成交|(参与)?投标|招标|采购|招租)(单位|人)$|^(中标|中选|成交)(金额|价格)$', origin_inner_table[i][j][0]):
+                elif predict_list[i][j]!=1 and re.search('^(中标|中选|成交|承包|(参与)?投标|招标|采购|招租|发包)(单位|人)(名称|地址|电话)?$'
+                                                         '|^(中标|中选|成交)(金额|价格)$|^项目(名称|编号)$', origin_inner_table[i][j][0]):
                     inner_table[i][j] = [origin_inner_table[i][j][0], 1]
                 elif origin_inner_table[i][j][0] in ['经评审的最低评标价法'] and predict_list[i][j]==1:
                     inner_table[i][j] = [origin_inner_table[i][j][0], 0]
@@ -1807,15 +1808,15 @@ def tableToText(soup, docid=None, return_kv=False):
     tbody_index = 1
     while tbody_index < len(tbodies)+1:
         tbody,_in_attachment = tbodies[len(tbodies)-tbody_index]
-        if len(tbody.find_all('tr')) == 1 and tbody_index > 0 and len(
-                tbodies[tbody_index - 1][0].find_all('tr')) == 1 and len(tbody.find_all(['th', 'td'])) == len(
-                tbodies[tbody_index - 1][0].find_all(['th', 'td'])): # 处理相邻表格都只有一行的情况;例:526321576
+        current_index = len(tbodies)-tbody_index
+        if current_index > 0 and len(tbodies[current_index - 1][0].find_all('tr')) == 1 and len(tbody.find_all('tr'))>=1 and len(tbody.tr.find_all(['th', 'td'])) == len(
+                tbodies[current_index - 1][0].tr.find_all(['th', 'td'])): # 处理相邻表格都只有一行的情况;例:526321576 641881540
             for row in tbody.find_all('tr'):
-                if tbodies[tbody_index - 1][0].tbody:
-                    tbodies[tbody_index - 1][0].tbody.append(row)
+                if tbodies[current_index - 1][0].tbody:
+                    tbodies[current_index - 1][0].tbody.append(row)
                 else:
-                    tbodies[tbody_index - 1][0].append(row)
-            inner_table = trunTable(tbodies[tbody_index - 1][0], _in_attachment)
+                    tbodies[current_index - 1][0].append(row)
+            inner_table = trunTable(tbodies[current_index - 1][0], _in_attachment)
             if inner_table:
                 list_innerTable.append(inner_table)
             tbody_index += 2
@@ -1845,15 +1846,16 @@ def tableToText(soup, docid=None, return_kv=False):
     # for tbody_index in range(1,len(tbodies)+1):
     while tbody_index < len(tbodies) + 1:
         tbody,_in_attachment = tbodies[len(tbodies)-tbody_index]
-        if len(tbody.find_all('tr')) == 1 and tbody_index > 0 and len(
-                tbodies[tbody_index - 1][0].find_all('tr')) == 1 and len(tbody.find_all(['th', 'td'])) == len(
-                tbodies[tbody_index - 1][0].find_all(['th', 'td'])): # 处理相邻表格都只有一行的情况;例:526321576
+        current_index = len(tbodies) - tbody_index
+        if current_index > 0 and len(
+                tbodies[current_index - 1][0].find_all('tr')) == 1 and len(tbody.find_all('tr'))>=1 and len(tbody.tr.find_all(['th', 'td'])) == len(
+                tbodies[current_index - 1][0].tr.find_all(['th', 'td'])): # 处理相邻表格都只有一行的情况;例:526321576
             for row in tbody.find_all('tr'):
-                if tbodies[tbody_index - 1][0].tbody:
-                    tbodies[tbody_index - 1][0].tbody.append(row)
+                if tbodies[current_index - 1][0].tbody:
+                    tbodies[current_index - 1][0].tbody.append(row)
                 else:
-                    tbodies[tbody_index - 1][0].append(row)
-            inner_table = trunTable(tbodies[tbody_index - 1][0], _in_attachment)
+                    tbodies[current_index - 1][0].append(row)
+            inner_table = trunTable(tbodies[current_index - 1][0], _in_attachment)
             if inner_table:
                 list_innerTable.append(inner_table)
             tbody_index += 2
@@ -2488,7 +2490,7 @@ def segment(soup,final=True):
     # print(soup)
     # print("====")
     #segList = ["tr","div","h1", "h2", "h3", "h4", "h5", "h6", "header"]
-    subspaceList = ["td",'a',"span","p"]
+    subspaceList = ["td","span","p"] # 20250723去掉 a 标签,角色会被补充a标签
     if soup.name in subspaceList:
         #判断有值叶子节点数
         _count = 0
@@ -2545,6 +2547,8 @@ def segment(soup,final=True):
         #     child.insert_after("#sube"+str(child.name)+"#")
         # if child.name in spaceList:
         #     child.insert_after(" ")
+        if child.name in subspaceList and len(child.get_text()) > 5 and re.search('\w$', child.get_text()): # 20250718 补充逗号避免642261504 分隔不了 联系邮箱:2034758276@qq.com中治交通建设集团有限公司北京分公司2025年6月25日,
+            child.insert_after(",")
     text = str(soup.get_text())
     #替换英文冒号为中文冒号
     text = re.sub("(?<=[\u4e00-\u9fa5]):|:(?=[\u4e00-\u9fa5])",":",text)
@@ -3366,6 +3370,8 @@ def get_preprocessed_article(articles,cost_time = dict(),useselffool=True):
         # print(article_processed)
         article_processed = segment(article_processed)
 
+        article_processed = re.sub('[,,]*有[,,]*限[,,]*公[,,]*司', '有限公司', article_processed) # 20250723 去掉由于附件处理导致的不合理逗号
+
         article_processed = article_processed.replace('(', '(').replace(')', ')')  #2022/8/10 统一为中文括号
         article_processed = article_processed.replace('侯选人', '候选人')  #2024/09/03 修复错别字避免预测错误。
         article_processed = article_processed.replace('人选人', '入选人')  #2024/09/03 修复错别字避免预测错误。
@@ -3397,7 +3403,7 @@ def get_preprocessed_article(articles,cost_time = dict(),useselffool=True):
             article_processed = re.sub('状态:(进行中|已结束)单位', ',项目单位', article_processed)  # 376225646
         if web_source_no.startswith('DX006116-') and re.search('结果公告如下:.{5,50},单位名称:', article_processed):  # 2023/11/20 特殊处理 381591924 381592533 这种提取不到情况
             article_processed = re.sub(',单位名称:', ',供应商名称:', article_processed)
-        ser = re.search('(采购|招标|比选)人(名称)?/(采购|招标|比选)?代理机构(名称)?:(?P<tenderee>[\w()]{4,25}(/[\w()]{4,25})?)/(?P<agency>[\w()]{4,25})[,。]', article_processed)
+        ser = re.search('(采购|招采|招标|比选)人(名称)?/(采购|招标|比选)?代理机构(名称)?:(?P<tenderee>[\w()]{4,25}(/[\w()]{4,25})?)/(?P<agency>[\w()]{4,25})[,。]', article_processed)
         if ser:
             article_processed = article_processed.replace(ser.group(0), '采购人名称:%s,采购代理机构名称:%s,' % (ser.group('tenderee'), ser.group('agency')))
 
@@ -3683,7 +3689,8 @@ def get_preprocessed_entitys(list_sentences,useselffool=True,cost_time=dict()):
 
     list_entitys = []
     not_extract_roles = ['黄埔军校', '国有资产管理处', '五金建材', '铝合金门窗', '华电XX发电有限公司', '华电XXX发电有限公司',
-                         '中标(成交)公司', '贵州茅台', '贵州茅台酒', '陕西省省级国', '纪检监察部门', '融安金桔', '海达源组织', '海达源织'] # 需要过滤掉的企业单位
+                         '中标(成交)公司', '贵州茅台', '贵州茅台酒', '陕西省省级国', '纪检监察部门', '融安金桔', '海达源组织',
+                         '海达源织', '成交出版社', '中国大学'] # 需要过滤掉的企业单位
     for list_sentence in list_sentences:
         sentences = []
         list_entitys_temp = []

BIN
BiddingKG/dl/interface/district_tuple.pkl


+ 2 - 2
BiddingKG/dl/interface/extract.py

@@ -395,7 +395,7 @@ def predict(doc_id,text,title="",page_time="",web_source_no='',web_source_name="
 
     '''规则调整角色概率'''
     start_time = time.time() #
-    predictor.getPredictor("rolegrade").predict(list_sentences,list_entitys,original_docchannel)
+    predictor.getPredictor("rolegrade").predict(list_sentences,list_entitys,doc_id, outlines = out_lines)
     cost_time["rolegrade"] = round(time.time()-start_time,2)
 
     '''规则调整金额概率'''
@@ -524,7 +524,7 @@ def predict(doc_id,text,title="",page_time="",web_source_no='',web_source_name="
 
     # data_res = Preprocessing.union_result(Preprocessing.union_result(codeName, prem),list_punish_dic)[0]
     # data_res = Preprocessing.union_result(Preprocessing.union_result(Preprocessing.union_result(codeName, prem),list_punish_dic), list_channel_dic)[0]
-    version_date = {'version_date': '2025-07-11'}
+    version_date = {'version_date': '2025-07-29'}
     data_res = dict(codeName[0], **prem[0], **channel_dic, **product_attrs[0], **product_attrs[1], **payment_way_dic, **fail_reason, **industry, **district, **candidate_dic, **version_date, **all_moneys, **pb_json)
 
     if original_docchannel == 302:

+ 1 - 1
BiddingKG/dl/interface/getAttributes.py

@@ -5116,7 +5116,7 @@ def rule_add_role(docid, prem, channel, content, web_source_no, nlp_enterprise):
         match = re.search('(招标|采购|招商)(人|方|商|单位|部门)(信息[,:]?)?(名称)?((甲方))?:(?P<name>[\w()—-]{4,35})([,。]|$)', content)
         if match:
             ent_name = match.group('name')
-            if re.search('测试|演示|某|\d号|\*|XX', ent_name)==None and re.search('^\w{1,5}[省市县区][\w()]{2,25}[厂店铺市场行部城室馆中心站处社会狱所园关局司署段厅院队小学村]((个体工商户)?|(普通合伙)?)?$',
+            if re.search('测试|演示|某|\d号|\*|XX', ent_name)==None and re.search('^\w{1,5}[省市县区][\w()]{2,25}([厂店铺市场行部城室馆中心站处社会狱所园关局司署段厅院队小学村]|社区)((个体工商户)?|(普通合伙)?)?$',
                          ent_name):  #  or is_enterprise_exist(ent_name)
                 log('规则补充招标人角色:%s,docid:%s'%(ent_name, docid))
                 add_role(ent_name, "tenderee", prem)

BIN
BiddingKG/dl/interface/header_set.pkl


+ 3 - 1
BiddingKG/dl/interface/modelFactory.py

@@ -87,7 +87,7 @@ class Model_role_classify_word():
         :param text:
         :return:
         '''
-        text = re.sub('第[一二三1-3]([条项章]|中学|医院|附属)|第三方(服务机构)?|(中标|成交|中选)(候选|结果)?(单位|人)?公[示告]', 'xxx', text)
+        text = re.sub('第[一二三1-3]([条项章轮次场]|中学|医院|附属)|第三方(服务机构)?|(中标|成交|中选)(候选|结果)?(单位|人)?公[示告]', 'xxx', text) # 修复 642200681 第二轮推荐中选供应商:第一中选候选人: 预测错为第二名
         text = re.sub('第01(中标|成交)?候选人', '第一中标候选人', text)
         text = re.sub('(标[段的包项]?|品目)[一二三1-3]', '标段', text)
         text = re.sub('第?[一二三1-3](标段?|[分子标]?包)', 'd标段', text)
@@ -109,6 +109,8 @@ class Model_role_classify_word():
         text = re.sub('采购方式', 'xxxx', text) # 修复 499096797 招标人预测错误
         text = re.sub('中标人\d名称', '中标人名称', text) # 修复 499096797 中标人预测错误
         text = re.sub('\|候选人', '、候选人', text) # 修复 626660259 排名:1|候选人:库尔勒海南广电工程有限责任公司
+        text = re.sub('(中标|成交|中选)(出版社|回收商)', '成交供应商', text) # 修复 642076501 成交出版社:科学出版社成都有限责任公司,
+        text = re.sub('((品牌|签名|盖章))?:', ' ', text) # 修复 642283019 成交人(品牌: 预测为5
         return text.replace('(', '(').replace(')', ')').replace('單', '单').replace('稱','承').replace('標', '标').replace('採購', '采购').replace('機構', '机构')
 
     def encode_word(self, sentence_text, begin_index, end_index, size=20, **kwargs):

+ 18 - 2
BiddingKG/dl/interface/outline_extractor.py

@@ -94,13 +94,22 @@ def extract_parameters(parse_document):
         _data = list_data[_data_i]
         _type = _data["type"]
         _text = _data["text"].strip()
+        _text = _text.replace('(', '(').replace(')', ')') # 20250729 统一为中文括号
         # print(_data.keys())
         if _type=="sentence":
             if _data["sentence_title"] is not None:
-                if re.search('[((][一二三四五六七八九十}]+[))]|[一二三四五六七八九十]+\s*[..、]|^\d{1,2}[..、][\u4e00-\u9fa5]', _text[:10]):
+                if re.search('[((][一二三四五六七八九十]+[))]|[一二三四五六七八九十]+\s*[..、]|^\d{1,2}[..、][\u4e00-\u9fa5]', _text[:10]):
                     idx = _text.replace(':', ':').find(':')
                     outline_text = _text[:idx] if idx >= 4 else _text
-                    out_lines.append((outline_text, _data['sentence_index'], _data['wordOffset_begin']))
+                    # out_lines.append((outline_text, _data['sentence_index'], _data['wordOffset_begin']))
+                    childs = get_childs([_data])
+                    if len(childs) > 0:
+                        scope = ((childs[0]['sentence_index'], childs[0]['wordOffset_begin']),
+                                 (childs[-1]['sentence_index'], childs[-1]['wordOffset_end']))
+                    else:
+                        scope = ((_data['sentence_index'], _data['wordOffset_begin']),
+                                 (_data['sentence_index'], _data['wordOffset_end']))
+                    out_lines.append((outline_text, _data['sentence_title'], _data['title_index'], _data['next_index'], scope))
 
                 if re.search(requirement_pattern,_text[:30]) is not None and re.search('符合采购需求,', _text[:30])==None:
                     b = (_data['sentence_index'], _data['wordOffset_begin'])
@@ -243,6 +252,13 @@ def extract_parameters(parse_document):
         pinmu_name = pinmu_name[ser.end():]
         if re.search('[^\w]$', pinmu_name):
             pinmu_name = pinmu_name[:-1]
+
+    if len(out_lines) < 3: # 小于三个的大纲去掉
+        out_lines = []
+    else:
+        text_, title_type, title_index, next_index, scope = out_lines[-1]
+        if scope[0][0] < scope[1][0]:# 最后一个大纲范围取当句,避免错误
+            out_lines[-1] = (text_, title_type, title_index, next_index,(scope[0], (scope[0][0]+1, 0)))
     return requirement_text, aptitude_text, addr_bidopen_text, addr_bidsend_text, out_lines, requirement_scope, pinmu_name, list_policy, winter_scope,correction_content
 
 def extract_addr(content):

+ 98 - 22
BiddingKG/dl/interface/predictor.py

@@ -940,7 +940,7 @@ class PREMPredict():
                     values[label] = 0.5
                 elif re.search('现由$', front) and re.search('^作为\d个单位的牵头(单位|公司)?', behind): # 修复 469369884 站源批量预测错误 现由第七合同段保利长大工程有限公司作为6个单位的牵头单位,
                     label = 5
-                elif re.search('\w(中标|成交)?|结果)?)(人|公告|公示),$|中标人信息:$', front): # 20250227修复中标错误 588005167 现确定贵公司为该项目的中标人,中国二冶集团有限公司,2025年01月26日,
+                elif re.search('\w(中标|成交)?|结果)?)(公告|公示),$|中标人信息:$|买受人:$', front): # 642237534 买受人在招标中算招标人,在拍卖中算中标人
                     label = 5
                 elif re.search('确定$', front) and re.search('^\w{,5}(项目|采购|招标)', behind):
                     label = 5
@@ -950,7 +950,9 @@ class PREMPredict():
                 elif re.search('^为\w{,10}第二(成交|中标)单位', behind): # 中标预测错误,例:601143888 河南省创慧新材料科技有限公司为铸咀采购项目第二成交单位
                     label = 3
                     values[3] = 0.5
-                elif re.search('中标单位,$|被确定为$|如果?我方成功中选$', front): # 632523961 现通知:贵司被确定为广州地铁传媒有限公司贵阳地铁广告媒体服务项目(2025年)的执行单位。 632380222 如我方成功中选 中国人民保险 采购项目,
+                elif re.search('被确定为$|如果?我方成功中选$', front): # 632523961 现通知:贵司被确定为广州地铁传媒有限公司贵阳地铁广告媒体服务项目(2025年)的执行单位。 632380222 如我方成功中选 中国人民保险 采购项目,
+                    label = 5
+                elif re.search('\w{4,}(成交|中标|中选)(人|单位),$', front): #  20250227修复中标错误 588005167 现确定贵公司为该项目的中标人,中国二冶集团有限公司,2025年01月26日,
                     label = 5
                 elif re.search('^为预备中标单位', behind):
                     label = 3
@@ -985,7 +987,7 @@ class PREMPredict():
                 elif re.search('代理公司$', front) and re.search('^销售', behind): # 修复 103462671 且该软件平台由博导前程软件公司在浙江的官方代理公司杭州楚沩教育科技有限公司销售。
                     label = 5
             elif label in [3,4]:
-                if re.search('第[二三]分(公司|店),中标(人|供应商|单位|公司):$', front):
+                if re.search('第[二三]分(公司|店),中标(人|供应商|单位|公司):$|拟中标人:$', front): # 修复 642061144 第三中标候选人:/拟中标人:
                     label = 2
                     values[label] = 0.7
                 elif re.search('决定选择第[二三]名', front) and re.search('^作为(中标|成交)(人|供应商|单位|公司)', behind):
@@ -1006,9 +1008,22 @@ class PREMPredict():
             elif re.search('(中标|成交)通知书[,:]$', front) and re.search('^:', behind) and label != 2:
                 label = 2
                 values[label] = 0.8
-            elif label==5 and re.search('^拟(招标|采购)一批|^须购置一批', behind):
-                label = 0
-                values[label] = 0.7
+            elif label==5:
+                if re.search('^拟(招标|采购)一批|^须购置一批', behind):
+                    label = 0
+                    values[label] = 0.7
+                elif re.search('投标人名称:$', front) and re.search('^,排[序名]1;', behind): # 修复排序在后面 642045526
+                    label = 2
+                    values[label] = 0.6
+                elif re.search('投标人名称:$', front) and re.search('^,排[序名]2;', behind):
+                    label = 3
+                    values[label] = 0.6
+                elif re.search('投标人名称:$', front) and re.search('^,排[序名]3;', behind):
+                    label = 4
+                    values[label] = 0.6
+                elif re.search(',$', front) and re.search('^,?中[选标]', behind): # 642105225 风机外委维修项目中标公告, 甘肃振邦鼓风动力制造有限公司 ,中选,
+                    label = 2
+                    values[label] = 0.6
             entity.set_Role(label, values)
 
     def predict_money(self,list_sentences,list_entitys):
@@ -1506,17 +1521,17 @@ class RoleRulePredictor():
                                      "(人|方|单位|组织|用户|业主|主体|部门|公司|企业|工厂)|[转流]出方|文章来源|委托机构|产权所有人|承包权人|结算单位|收货地址)" \
                                      "[))]?(信息|联系方式|概况)?[,,::]?([((](1|2|1.1|1.2)[))])?((公司|单位)?名称)?([((](全称|盖章|异议受理部门)[))])?(是|为|:|:|\s*)+$|(采购商|招标人):(\w{2,10}-)?$|实施主体(基本情况,)?名称:$)"
         self.pattern_tenderee_center = "(?P<tenderee_center>(受.{5,20}的?委托|现将[\w()()]{5,20}[\d年月季度至()]+采购意向|尊敬的供应商(伙伴)?:\w{5,20}(以下简称“\w{2,5}”)))"
-        self.pattern_tenderee_right = "(?P<tenderee_right_50>^(机关)?([((](以下简称)?[,\"“]*((招标|采购)(人|单位|机构)|(服务)?购买方)[,\"”]*[))]|^委托|^将于[\d年月日,::]+进行|^现委托|^的\w{2,10}正在进行|[\d年月季度至]+采购意向|^)?的招标工作已圆满结束)|^([拟须需]|计划)(采购|招标|购置|购买)|^须购[买置]一批|作为(采购|招标)(人|单位))"  #|(^[^.。,,::](采购|竞价|招标|施工|监理|中标|物资)(公告|公示|项目|结果|招标))|的.*正在进行询比价) # 20250605去掉 |^关于 根据《广东省自然资源厅关于开展2024年度耕地资源分区分类评价更新工作的通知》
+        self.pattern_tenderee_right = "(?P<tenderee_right_50>^(机关)?,?([((](以下简称)?[,\"“]*((招标|采购)(人|单位|机构)|(服务)?购买方)(名称)?[,\"”]*[))]|^委托|^将于[\d年月日,::]+进行|^现委托|^的\w{2,10}正在进行|[\d年月季度至]+采购意向|^)?的招标工作已圆满结束)|^([拟须需]|计划)(采购|招标|购置|购买)|^须购[买置]一批|作为(采购|招标)(人|单位))"  #|(^[^.。,,::](采购|竞价|招标|施工|监理|中标|物资)(公告|公示|项目|结果|招标))|的.*正在进行询比价) # 20250605去掉 |^关于 根据《广东省自然资源厅关于开展2024年度耕地资源分区分类评价更新工作的通知》
         self.pattern_tendereeORagency_right = "(?P<tendereeORagency_right>(^拟对|^现?就|^现对))"
         self.pattern_agency_left = "(?P<agency_left>((代理|拍卖)(?:人|机构|公司|企业|单位|组织)|专业采购机构|集中采购机构|招标组织机构|交易机构|集采机构|[招议))]+标机构|(采购|招标)代理)(名称|.{,4}名,?称|全称)?(是|为|:|:|[,,]?\s*)$|(受.{5,20}委托,?$))"
-        self.pattern_agency_right = "(?P<agency_right>^([((](以下简称)?[,\"“]*(代理)(人|单位|机构)[,\"”]*[))])|^受.{5,20}委托|^受委?托,)"  # |^受托  会与 受托生产等冲突,代理表达一般会在后面有逗号
+        self.pattern_agency_right = "(?P<agency_right>^,?([((](以下简称)?[,\"“]*(代理)(人|单位|机构)(名称)?[,\"”]*[))])|^受.{5,20}委托|^受委?托,)"  # |^受托  会与 受托生产等冲突,代理表达一般会在后面有逗号
         # 2020//11/24 大网站规则 中标关键词添加 选定单位|指定的中介服务机构
         self.pattern_winTenderer_left_50 = "(?P<winTenderer_left_51>" \
-               "(乙|竞得|受让|买受|签约|供货|供应?|合作|承做|承包|承建|承销|承保|承接|承制|承担|承修|承租((包))?|入围|入选|竞买)(候选|投标)?(人|单位|机构|供应商|方|公司|企业|厂商|商|社会资本方?|银行)(:?单位名称|:?名称|盖章)?[::是为]+$" \
+               "(乙|竞得|受让|签约|供货|供应?|合作|承做|承包|承建|承销|承保|承接|承制|承担|承修|承租((包))?|入围|入选|竞买)(候选|投标)?(人|单位|机构|供应商|方|公司|企业|厂商|商|社会资本方?|银行)(:?单位名称|:?名称|盖章)?[::是为]+$" \
                "|(选定单位|指定的中介服务机构|实施主体|中标银行|中标通知书,致|征集结果|选择中介|选择结果|成交对象|勘察人|(,|审计|处置|勘察|设计)服务单位|受托[人方]|直接采购对象)[::是为]+$" \
                "|((评审结果|名次|排名|中标结果)[::]*第?[一1]名?)[::是为]+$|成交供应商信息[,:]?(序号1)?:?|供应商名称$|竞争性选择申请人名称:$" \
                "|单一来源(采购)?(供应商|供货商|服务商|方式向)$|((中标|成交)(结果|信息))[::是为]+$|(中标|成交)供应商、(中标|成交)(金额|价格),$|合作伙伴名称:$|供应商(乙方)-?$|合作单位:$" \
-               "|现(公布|宣布|公示)中标单位如下:$|现将中标单位(公布|公示)如下:$|现宣布以下(企业|单位|公司)中标:$|经讨论,决定采用$|第\d+(包件?|标段?)(中标|中选|成交)候选人:$|入围供应商如下(排名不分先后)[,:]$)"  # 承办单位:不作为中标 83914772  |施工 单位不作为中标人 例:386692187
+               "|现(公布|宣布|公示)中标单位如下:$|现将中标单位(公布|公示)如下:$|现宣布以下(企业|单位|公司)中标:$|经讨论,决定采用$|第\d+(包件?|标段?)(中标|中选|成交)候选人:$|入围供应商如下(排名不分先后)[,:]$)"  # 承办单位:不作为中标 83914772  |施工 单位不作为中标人 例:386692187 买受人招标公告可能为招标人,拍卖中应该是中标人
         self.pattern_winTenderer_left_60 = "(?P<winTenderer_left_60>" \
                                            "(,|。|:|^)((中标(投标)?|[拟预]中标|中选|中价|中签|成交)(人|单位|机构|中介(服务)?机构|供应商|客户|方|公司|企业|厂商|商家?|社会资本方?|银行)|(中标候选人)?第?[一1]名|第[一1](中标|中选|成交)?候选人|服务机构)" \
                                            "(:?单位名称|:?名称|盖章)?[,,]?([((]按综合排名排序[))]|:择优选取)?[::,,]$|选取(情况|说明):中选,中介机构名称:$|排名如下:1、$|第[一1]名,?投标(人|单位|银行|公司):$)"  # 解决表头识别不到加逗号情况,需前面为,。空 20240621补充 中选 云南省投资审批中介超市 补充排名如下 南阳师范学院
@@ -2437,7 +2452,7 @@ class RoleGrade():
         self.thirdTenderer_left_9 = "(?P<thirdTenderer_left_9>(第[三3](中标|中选|中价|成交)?候选(人|单位|供应商|公司)|第[三3](名|候选)|排[名序]:3|名次:3))"
         self.pattern_list = [self.tenderee_left_9,self.tenderee_center_8, self.tenderee_left_8,self.tenderee_left_6,self.tenderee_left_5,self.agency_left_9,
                              self.winTenderer_left_9,self.winTenderer_left_8, self.winTenderer_right_9, self.winTenderer_left_6, self.secondTenderer_left_9, self.thirdTenderer_left_9] # 概率要由高到低 274941849
-    def predict(self, list_sentences, list_entitys, original_docchannel, span=15, min_prob=0.7):
+    def predict(self, list_sentences, list_entitys, docid, span=15, min_prob=0.7, outlines=[]):
         '''
         根据规则给角色分配不同等级概率;分三级:0.9-1,0.8-0.9,0.7-0.8;附件0.7-0.8,0.6-0.7,0.5-0.6
         修改概率小于0.6的且在大数据代理集合里面的招标人为代理人
@@ -2447,6 +2462,17 @@ class RoleGrade():
         :param codeName:
         :return:
         '''
+        bid_info = []
+        if outlines:
+            win_pattern = re.compile("^([((][一二三四五六七八九十\d]+[))]|[一二三四五六七八九十]+\s*[..、])[\w(]{,5}((中标|成交)[))]?(人|单位)?的?(基本|主要)?(信息|情况|概况|结果)|(中标|成交)[))]?(人|供应商|单位)|中标公示单位:)")
+            for outline in outlines:
+                text_, title_type, title_index, next_index, scope = outline
+                if re.search(win_pattern, text_) and re.search('(未|没|是否)(中标|成交)|中标单位合同签订主体|业绩|提供', text_)==None:
+                    bid_info.append(scope)
+                    log("中标大纲:%s, docid:%s"%(text_, docid))
+        have_winner = False
+        bid_info_company = []
+
         sentences = sorted(list_sentences[0], key=lambda x:x.sentence_index)
         role2id = {"tenderee": 0, "agency": 1, "winTenderer": 2, "secondTenderer": 3, "thirdTenderer": 4}
         org_winner = []
@@ -2539,6 +2565,20 @@ class RoleGrade():
             if entity.entity_type in ['org', 'company'] and entity.label in [1, 0] and 0.6<entity.values[entity.label]: # 由0.5调为0.6,避免367217504 同时为低概率招标、中标被改
                 all_tenderee_agency.append(entity.entity_text)
 
+            if bid_info and entity.entity_type in ['org', 'company']: # 中标大纲下实体
+                for scope in bid_info:
+                    if scope[0][0] == scope[1][0]:
+                        if entity.sentence_index == scope[0][0] and scope[0][1] < entity.wordOffset_begin and entity.wordOffset_end <= scope[1][1]:
+                            bid_info_company.append(entity)
+                    else:
+                        if entity.sentence_index == scope[0][0] and entity.wordOffset_begin > scope[0][1]:
+                            bid_info_company.append(entity)
+                        elif scope[0][0] < entity.sentence_index < scope[1][0]:
+                            bid_info_company.append(entity)
+                        elif entity.sentence_index == scope[1][0] and entity.wordOffset_end <scope[1][1]:
+                            bid_info_company.append(entity)
+                if entity.label == 2 and entity.values[entity.label] > 0.5:
+                    have_winner = True
 
         if org_tenderee == [] and agency_like_tenderee:
             for entity in agency_like_tenderee:
@@ -2572,6 +2612,19 @@ class RoleGrade():
             #             # log('如果同时包含org和company中标人,降低org中标人概率为0.6:%s, %s' % (ent.entity_text, ent.values[2]))
             #             ent.values[2] = 0.6
 
+        if have_winner == False and bid_info_company: # 优化 638075055 二、推荐中标候选人信息,邯郸市婷元紧固件制造有限公司第一名,
+            for entity in bid_info_company:
+                text = sentences[entity.sentence_index].sentence_text
+                b = entity.wordOffset_begin
+                e = entity.wordOffset_end
+                if re.search('(单位名称|投标(单位|人)(名称)?|公司名称):$', text[max(0, b-span-2):b]) and entity.label == 5:
+                    entity.label = 2
+                    entity.values[entity.label] = 0.55
+                    log('大纲规则补充中标人:%s, docid:%s'%(entity.entity_text, docid))
+                elif len(bid_info_company) == 1 and entity.label != 2:
+                    entity.label = 2
+                    entity.values[entity.label] = 0.55
+                    log('大纲规则补充中标人:%s, docid:%s' % (entity.entity_text, docid))
 
 class MoneyGrade():
     def __init__(self):
@@ -3353,6 +3406,9 @@ class ProductAttributesPredictor():
         html = re.sub("<html>|</html>|<body>|</body>","",html)
         html = re.sub("##attachment##","",html)
         soup = BeautifulSoup(html, 'lxml')
+
+        del_tabel_achievement(soup) # 删除业绩表格
+
         # flag_yx = True if re.search('采购意向', html) else False
         flag_yx = True if re.search('采购意向|招标意向|选取意向|意向公告|意向公示|意向公开', html) else False
         tables = soup.find_all(['table'])
@@ -6139,11 +6195,12 @@ class DistrictPredictor():
         :return:
         '''
         province_l, city_l, district_l = [], [], []
+        citys = []
 
         text = str(text).replace('(', '(').replace(')', ')')
         text = re.sub('\d{2,4}年度?|[\d/-]{1,5}[月日]|\d+|[a-zA-Z0-9]+', ' ', text)
         text = re.sub(
-            '复合肥|海南岛|兴业银行|双河口|阳光|杭州湾|新城区|中粮屯河|老城(区|改造|更新|升级|翻新)|沙县小吃|北京时间|福田汽车|中山(大学|公园|纪念堂)|孙中山|海天水泥|阳光采购|示范县|珠江城|西九龙站|广州路北|安阳山村|电信|联通|北京现代|祁连山', # 570445994 广州路北侧 预测为 广州 路北
+            '复合肥|海南岛|兴业银行|双河口|阳光|杭州湾|新城区|中粮屯河|老城(区|改造|更新|升级|翻新)|沙县小吃|北京时间|福田汽车|中山(大学|公园|纪念堂)|孙中山|海天水泥|阳光采购|示范县|珠江城?|西九龙站|广州路北|安阳山村|电信|联通|北京现代|祁连山', # 570445994 广州路北侧 预测为 广州 路北
             ' ', text)  # 544151395 赤壁市老城区燃气管道老化更新改造
         text = re.sub('珠海城市', '珠海', text)  # 修复 426624023 珠海城市 预测为海城市
         text = re.sub('怒江州', '怒江傈僳族自治州', text)  # 修复 423589589  所属地域:怒江州 识别为广西 - 崇左 - 江州
@@ -6152,6 +6209,7 @@ class DistrictPredictor():
         text = re.sub('横州市', '横县', text)  # 例:547363890 修复广西南宁横州 不在地区表问题
         text = re.sub('广东中山', '广东中山市', text)
         text = re.sub('朝阳柳城经济开发区', '朝阳市', text)
+        text = re.sub('安徽徽运城市', '安徽', text) # 653054198 安徽徽运城市运营管理有限公司
         ser = re.search('海南(昌江|白沙|乐东|陵水|保亭|琼中)(黎族)?', text)
         if ser and '黎族' not in ser.group(0):
             text = text.replace(ser.group(0), ser.group(0) + '黎族')
@@ -6195,9 +6253,12 @@ class DistrictPredictor():
                                     score += 1
                             score += it.end(k) / len(text) / 10  # 优化 572840045 上海铁路公安局合肥公安处 这种表达
                             city_l.append((v, score * weight))
+                            citys.append(v)
                         elif k in ['dist', 'dist1', 'dist2']:
                             if v in ['东区', '西区', '城区', '郊区', '矿区', '东至']:
                                 continue
+                            elif v.endswith('城市') and text[it.start(k)-1:it.start(k)+1] in citys: # 修复 上海城市 珠海城市 等 预测为 海城市
+                                continue
                             if v in ['向阳区', '宝山区', '南沙区', '和平区', '新城区', '鼓楼区', '南山区', '白云区', '朝阳区',
                                      '江北区', '城关区', '永定区', '普陀区', '长安区', '市中区', '西安区', '通州区', '西湖区',
                                      '龙华区', '城中区', '河东区', '桥西区', '青山区', '新华区', '铁西区', '铁东区', '海州区']: # 多个城市有的区概率降低
@@ -6425,6 +6486,9 @@ class DistrictPredictor():
         in_content = False
         not_sure = True # 是否不确定地区
 
+        if web_source_name in ('中原云商', '中原云商电子招投标平台'): # 修复某些公告没地区
+            web_source_name = '河南中原云商'
+
         province_l, city_l, district_l = self.find_whole_areas('%s %s'%(title, addr_project), self.pettern, self.area_variance_dic, self.full_dic)
         pro_ids, city_ids, dis_ids = self.merge_score(province_l, city_l, district_l, self.full_dic, self.short_dic, self.idx_dic)
         big_area_1, pred_pro_1, pred_city_1, pred_dis_1, prob, max_score, code_dic_1 = self.get_final_addr(pro_ids, city_ids, dis_ids, self.idx_dic)
@@ -6434,6 +6498,7 @@ class DistrictPredictor():
         # print('分数:', pro_ids, city_ids, dis_ids, prob, max_score)
         if pred_city_1 == "" or prob < 0.7 or max_score<2:
             ree, addr = self.get_ree_addr(prem)
+            have_bus, bus_dic = get_business_data(ree) if ree != '' else False, {}
             if ree in title:
                 ree = '##'
             rule_ree_addr = self.get_role_address(content)
@@ -6456,7 +6521,8 @@ class DistrictPredictor():
                 not_sure = False
                 big_area, pred_pro, pred_city, pred_dis, code_dic = big_area_1, pred_pro_1, pred_city_1, pred_dis_1, code_dic_1
             if not_sure and (pred_city_2 == "" or prob < 0.7 or max_score<2):
-                province_l3, city_l3, district_l3 = self.find_whole_areas('%s; %s; %s'%(addr_contact, addr_bidopen, addr_bidsend), self.pettern, self.area_variance_dic, self.full_dic, weight=0.6)
+                addr_bus = '%s %s'%(bus_dic.get('province', ''), bus_dic.get('city', ''))
+                province_l3, city_l3, district_l3 = self.find_whole_areas('%s %s; %s; %s'%(addr_bus, addr_contact, addr_bidopen, addr_bidsend), self.pettern, self.area_variance_dic, self.full_dic, weight=0.6)
                 province_l.extend(province_l3)
                 city_l.extend(city_l3)
                 district_l.extend(district_l3)
@@ -6487,10 +6553,6 @@ class DistrictPredictor():
                     # print('输入:', '站源:%s, 角色:%s, 地址:%s' % (web_source_name, tenderees, all_addr))
                     # print('分数:', pro_ids, city_ids, dis_ids, prob, max_score)
 
-        if pred_city in ['北京', '天津', '上海', '重庆']:
-            pred_city = pred_dis
-            pred_dis = ""
-
         if big_area != "":
             area_dic['area'] = big_area
         if pred_pro != "":
@@ -6502,6 +6564,16 @@ class DistrictPredictor():
         for k, v in code_dic.items():
             if v != '':
                 area_dic[k] = v
+
+        if pred_city in ['北京', '天津', '上海', '重庆']: # 直辖市调整县级
+            area_dic['city'] = area_dic['district']
+            area_dic['district'] = '未知'
+            if 'district_code' in area_dic:
+                area_dic['city_code'] = area_dic['district_code']
+                area_dic.pop('district_code')
+            if area_dic['city'] == '未知' and 'city_code' in area_dic:
+                area_dic.pop('city_code')
+
         area_dic['is_in_text'] = in_content
         # area_dic['prob'] = prob
         # area_dic['max_score'] = max_score
@@ -7661,11 +7733,11 @@ class CandidateExtractor(object):
             "project_name": "(包[段组件]|标[段包的项]|标段(包)|分[包标]|采购|项目|工程|货物|商品|产品|设备|通用|主要标的|^包)(名称?|内容)|^标的$",
             "win_sort": "排名|排序|名次|推荐顺序",
             'win_or_not': '是否(建议|推荐)?(中标|成交)|是否入围|是否入库|入围结论|^选择设备$', # 补充站源特别表达:例:577351909 选择设备 1 为中标 0 非中标
-            "candidate": "((候选|入围|入选|投标|应答|响应|参选|比选)(供应商库)?的?(人|人?单位|机构|供应商|供货商|服务商|投标人|(中标)?公司|(中标)?企业|银行)|(通过)?名单|中标候选人)(名称|名单|全称|\d)?$|^供应商(名称|信息)?$|投标个人/单位|^公司名称$|供应商单位名称$", #补充 368295593 投标个人/单位 提取
+            "candidate": "((候选|入围|入选|投标|应答|响应|参选|比选)(供应商库)?的?(人|人?单位|机构|供应商|供货商|服务商|投标人|(中标)?公司|(中标)?企业|银行)|(通过)?名单|中标候选人)(名称|名单|全称|\d)?$|^供应商(名称|信息)?$|投标个人/单位|^(公司|单位)名称$|供应商单位名称$", #补充 368295593 投标个人/单位 提取
             "bid_amount": "投标[报总]?价|报价(总?金额|总价|总额)|总报价|^\w{,5}报价(([\w、/]{1,15}))?$|(中标|成交|合同))?([金总]额|[报均总]价|价[格款]?)|承包价|含税价|经评审的价格",
-            "win_tenderer": "第一名|第一(中标|成交)?候选人",
-            "second_tenderer": "第二名|第二(中标|成交)?候选人",
-            "third_tenderer": "第三名|第三(中标|成交)?候选人",
+            "win_tenderer": "第一名|第一预?(中标|成交)?候选人",
+            "second_tenderer": "第二名|第二预?(中标|成交)?候选人",
+            "third_tenderer": "第三名|第三预?(中标|成交)?候选人",
         }
         '''非表格候选人正则'''
         # self.p = '((候选|入围|入选|投标)(供应商库)?的?(人|人?单位|机构|供应商|供货商|服务商|投标人|(中标)?公司|(中标)?企业|应答人)|(通过)?名单)(名称|名单|全称|\d)?:$'
@@ -7673,6 +7745,10 @@ class CandidateExtractor(object):
         self.tb = TableTag2List()
         with open(os.path.dirname(__file__)+'/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] # 去除表头无关信息,方便匹配判断是否为表头
@@ -7690,7 +7766,7 @@ class CandidateExtractor(object):
                     return flag, contain_header, dict()
                 num = 0
                 for k, v in self.head_rule_dic.items():
-                    if k == 'candidate' and re.search('第[一二三]名|第[一二三](中标|成交)?候选人', text):
+                    if k == 'candidate' and re.search('第[一二三]名|第[一二三]预?(中标|成交)?候选人', text):
                         continue
                     if re.search('评分|得分|分数|分值', text):
                         continue