# -*- coding: utf-8 -*- """``DocChannel`` — 公告类型/生命周期模型+规则混合预测。 Phase 5 从 ``interface/predictor.py``(约 4722-5551 行)迁出。 原 ``from common.Utils import *`` / ``from common.nerUtils import *`` 已替换为显式 import;``os.path.dirname(__file__)`` 路径引用替换为 ``predictors._common.INTERFACE_DIR``。 """ from __future__ import absolute_import import os import re import json import numpy as np import tensorflow as tf from BiddingKG.dl.common.logging import log from BiddingKG.dl.foolnltk import selffool from BiddingKG.dl.model_runtime.vocab import getIndexOfWords from BiddingKG.dl.model_runtime.embed import embedding from BiddingKG.dl.predictors._common import INTERFACE_DIR __all__ = ["DocChannel"] class DocChannel(): def __init__(self, life_model='channel_savedmodel/channel.pb', type_model='channel_savedmodel/doctype.pb',config=None): self.lift_sess, self.lift_title, self.lift_content, self.lift_prob, self.lift_softmax,\ self.mask, self.mask_title = self.load_life(life_model,config) self.type_sess, self.type_title, self.type_content, self.type_prob, self.type_softmax,\ self.type_mask, self.type_mask_title = self.load_type(type_model) self.sequen_len = 200 # 150 200 self.title_len = 30 self.sentence_num = 10 self.kws = '供货商|候选人|供应商|入选人|项目|选定|预告|中标|成交|补遗|延期|报名|暂缓|结果|意向|出租|补充|合同|限价|比选|指定|工程|废标|取消|中止|流标|资质|资格|地块|招标|采购|货物|租赁|计划|宗地|需求|来源|土地|澄清|失败|探矿|预审|变更|变卖|遴选|撤销|意见|恢复|采矿|更正|终止|废置|报建|流拍|供地|登记|挂牌|答疑|中选|受让|拍卖|竞拍|审查|入围|更改|条件|洽谈|乙方|后审|控制|暂停|用地|询价|预' lb_type = ['采招数据', '土地矿产', '拍卖出让', '产权交易', '新闻资讯'] lb_life = ['采购意向', '招标预告', '招标公告', '招标答疑', '公告变更', '资审结果', '中标信息', '合同公告', '废标公告'] self.id2type = {k: v for k, v in enumerate(lb_type)} self.id2life = {k: v for k, v in enumerate(lb_life)} self.load_pattern() def load_pattern(self): self.type_dic = { '土地矿产': '供地结果|(土地|用地|宗地|地块|海域|矿)的?(基本信息|基本情况|概况|信息|详情|来源|用途|性质|编号|位置|坐落|使用年限|出让年限)|(土地|山地|农田)(经营权)?(出让|出租|招租|租赁|承包|流转)|流转土地', '拍卖出让': '(拍卖|变卖|流拍|竞拍|竞买)的?(公告|活动|信息|结果|成交|主体|标的|资产|财产|方式|类型|流程|程序|规则|价格|保证金|时间)|(公开|进行|密封)(拍卖|变卖|竞拍)|第[一二三四五六七八九\d]次拍卖|(资产|司法|网络)拍卖|交易方式.{,2}拍卖|拍卖会|(拍卖.?方式|起拍价)[::]|竞买人资格|竞买人资质要求', '产权交易': '(产权|资产|权证)的?(类型|类别|用途|性质|状态|信息|名称|编号|(基本)?情况)|(经营权|承包权|使用权|租赁权|股权|债权|排污权|化学需氧量|储备量)(挂牌|转让|出让)|竞价销售|销售结果|房屋所有权房产|免租期限|交易期限|(受让|转让|承租|出租)(人|方)|(店面|店铺|商铺|铺位?|门面|门市|食堂|饭堂|校舍|车位|停车场|厂?房|仓?库|馆|资产|物业|房产|房屋|场地|农田|鱼?塘)\w{,4}(处置|招租|出租|续租|租赁|转让)|(出租|转让|产权|资产)(项目|中标|成交|流标|废标)|出租(用途|类型)|转让底价|租赁(标的物|情况)|看[样货](时间|地[点址]|方式|仓库|验货)|最小加价|加价[幅梯]度|交易模式[::\s]*延时竞价销售|挂牌(开始|结束)时间|挂牌价格?', '采招数据': '(采购|招标)(条件|范围|文件|内容)|(申请人|投标人|供应商|报价人|参选人)的?资格要求;|采购需求清单|最低价排序|竞争性采购方式|采购进行公开竞价|竞价模式[::\s]*一次报价|预算金额|代理银行资格选定' # |变更|答疑|澄清|中标|成交|合同|废标|流标 |(采购|招标|代理)(人|机构|单位)| } self.title_type_dic = { # ‘**2’ 为类别强相关关键词,不会被其他规则修正 '土地矿产': '(土地|用地|宗地|荒地|山地|海域|矿)(出让|出租|招租|租赁|承包|流转|使用权|经营权|征收|划拨|中标|成交)|供地结果|矿业权|探矿权|采矿权|(土地|用地|宗地|地块)(使用权)?(终止|中止|网上)?(挂牌|出让|拍卖|招拍|划拨)|征收土地|流转土地', '土地矿产2': '(土地|用地|宗地|荒地|山地|海域|矿)(出让|出租|招租|租赁|承包|流转|使用权|经营权|征收|划拨|中标|成交)|供地结果|矿业权|探矿权|采矿权|(土地|用地|宗地|地块)(使用权)?(终止|中止|网上)?(挂牌|出让|拍卖|招拍|划拨)|征收土地|流转土地', '拍卖出让': '(拍卖|变卖|拍(变)卖|流拍|竞拍|竞买)[\)\])】]?的?(公告|公示|预告|告知书)|拍卖|变卖|流拍|竞拍|第[一二三四五六七八九\d]次拍卖|拍卖会$', '拍卖出让2': '(拍卖|变卖|拍(变)卖|流拍|竞拍|竞买)[\)\])】]?的?(公告|公示|预告|告知书)|拍卖|变卖|流拍|第[一二三四五六七八九\d]次拍卖|拍卖会$', '产权交易': '经营权|承包权|使用权|租赁权|股权|债权|排污权|化学需氧量|储备量|竞价销售|销售结果|出租|招租|拍租|竞租|续租|挂牌|出让|废[旧弃]?(物资|设备|资源|金属|钢筋|料)处[置理]', '产权交易2': '使用权|租赁权|股权|债权|排污权|竞价销售|销售结果|出租|招租|拍租|竞租|续租|挂牌|出让|废[旧弃]?(物资|设备|资源|金属|钢筋|料)处[置理]', # '采招数据': '(采购|招标|询价|议价|比价|比选|遴选|邀请|邀标|磋商|洽谈|约谈|谈判|征询|调研)的?(公告|公示|中标|成交|结果|$)|工程招标|定点服务|(设备|服务|\w{2})[直采]购|(建设|改造)项目|工程|拦标价|控制价|银行|资格选定|资金|公款|存款|存放|现金管理|招募|入围|入库', '采招数据': '(采购|招标|询价|议价|比价|比选|遴选|邀请|邀标|磋商|洽谈|约谈|谈判|征询|调研)的?(公告|公示|中标|成交|结果|$)|工程招标|定点服务|(设备|服务|\w{2})[直采]购|(建设|改造)项目|拦标价|控制价|资格选定|资格认定|资金|公款|存款|现金管理|招募|入库|遴选.{,25}(服务|事务所|机构)', # |竞价 采招/产权都有竞价方式 # 意向|需求|预公?告|报建|总承包|工程|施工|设计|勘察|代理|监理 |变更|答疑|澄清|中标|成交|合同|废标|流标 '新闻资讯': '(考试|面试|笔试)成绩|入围面试|面试人员名单|成绩的?(公告|公示|公布)|公开招聘|招聘(公告|简章|启事|合同制)|疫情防控\s{,5}(通知|情况|提示)|行政审批结果|收回收购(存量)?(闲置)?土地.{,3}价格.{,2}公示', '新闻资讯2': '面试人员名单|疫情防控\s{,5}(通知|情况|提示)|行政审批结果|收回收购(存量)?(闲置)?土地.{,3}价格.{,2}公示|(印发|转发)《.{2,30}》.{,3}通知|清退.{,8}保证金|专项资金管理清单.{,2}使用情况|关于依法处置' } self.life_dic = { '采购意向': '采购意向|招标意向|选取意向|意向公告|意向公示', '采购意向neg': '发布政府采购意向|采购意向公告已于', '招标预告': '(预计|计划)(招标|采购|发标|发包)(时间|日期)|采购(计划编号|需求方案|预告|预案)|(预|需求)公示|需求(方案|信息|论证|公告|公示)', '招标公告': '(采购|招标|竞选|报名)条件|报名(时间|流程|方法|要求|\w{,5}材料)[:\s]|[^\w]成交规则|参加竞价采购交易资格|(申请人|投标人|供应商|报价人|参选人)的?资格(要求|条件)|获取(采购|招标|询价|议价|竞价|比价|比选|遴选|邀请|邀标|磋商|洽谈|约谈|谈判|竞谈|应答)文件|(采购|招标|询价|议价|竞价|比价|比选|遴选|邀请|邀标|磋商|洽谈|约谈|谈判|竞谈|应答)文件的?(获取|领取)|评选方式:?\s*价格最低|求购信息', '资审结果': '资审及业绩公示|资审结果及业绩|资格后审情况报告|资格(后审|预审|审查)结果(公告|公示)|(预审|审查)工作已经?结束|未通过原因', #|资格 '招标答疑': '现澄清(为|如下)|答疑补遗|澄清内容如下|第[0-9一二三四五]次澄清|答疑澄清|(最高(投标)?限价|控制价|拦标价)公示|澄清与修改', # |异议的回复 '公告变更': '第[\d一二]次变更|(更正|变更)(公告|公示|信息|内容|事项|原因|理由|日期|时间|如下)|原公告((主要)?(信息|内容)|发布时间)|(变更|更正)[前后]内容|现?在?(变更|更正|修改|更改)(内容)?为|(公告|如下|信息|内容|事项|结果|文件|发布|时间|日期)(更正|变更)|恢复[\u4e00-\u9fa5]{,8}(招标?投标|招标|投标)活动', '公告变更neg': '履约变更内容', '候选人公示': '候选人公示|评标结果公示|中标候选人名单公示|现将中标候选人(进行公示|公[示布]如下)|(中标|中选)候选人[\u4e00-\u9fa5]{0,2}(信息|情况|第[123一二三]名)[::\s]', '候选人公示neg': '中标候选人公示期|中标候选人公示前', '中标信息': '供地结果信息|采用单源直接采购的?情况说明|[特现]?将\w{,4}(成交|中标|中选|选定结果|选取结果|入围结果|竞价结果)\w{,4}(进行公示|公[示布]如下)|(询价|竞价|遴选)?(成交|中标|中选)(公告|公示)|(成交|中标|中选|选定|选取|入围|询价)结果(如下|公告|公示)|(中标|中选)(供应商|承包商|候选人|入围单位)如下|拟定供应商的情况|((中标|中选)(人|成交)|成交)\w{,3}(信息|情况)[::\s]', '中标信息2': '\s(成交|中标|中选)(信息|日期|时间|总?金额|价格)[::\s]|(成交|中标|中选)价格\s*[\d.,]+(?万?元|(采购|招标|成交|中标|中选|评标)结果|单一来源(采购|招标)?的?(中标|成交|结果)|项目已结束|中标公示 ', # |单一来源采购原因|拟采取单一来源方式采购|单一来源采购公示 '中标信息3': '(中标|中选|成交|拟定|拟选用|最终选定的?|受让)(供应商|供货商|服务商|机构|企业|公司|单位|候选人|人)(信息[,:]?)?(名称)?[::\s]|[、\s](第一名|(拟定|推荐|入围)?(供应商|供货商)|(中选|中标|供货)单位|中选人)[::\s]|确定[\w()]{6,25}为中标人', # |唯一 '中标信息neg': '按项目控制价下浮\d%即为成交价|成交原则|不得确定为(中标|成交)|招标人按下列原则选择中标人|评选成交供应商:|拟邀请供应商|除单一来源采购项目外|单一来源除外|(各.{,5}|尊敬的)(供应商|供货商)[:\s]|竞拍起止时间:|询价结果[\s\n::]*不公开|本项目已具备招标条件|现对该项目进行招标公告|发布\w{2}结果后\d天内送达|本次\w{2}结果不对外公示|供应商\s*资格要求|成交情况:\s*[流废]标|中标单位:本次招标拟?中标单位\d家|通知中标单位|影响(成交|中标)结果|确定为成交供应商|(成交|中标|中选)公[告示](发布|\w{,2})后|竞价成交后', # 503076535 按照服务方案的优劣 确定为成交供应商 # |确定成交供应商[:,\s] '合同公告': '合同(公告|公示|信息|内容)|合同(编号|名称|主体|基本情况|完成(日期|时间))|(供应商乙方|乙方供应商):|合同总?金额|履约信息', '合同公告neg': '合同金额页|合同金额[\u4e00-\u9fa5]{,2}不[少低高多]于', '废标公告': '(终止|中止|废标|流标|流采|失败|作废|异常|撤销)(结果)?(公告|公示|招标|采购|竞价)|(谈判结果为|结果类型|采购结果):?(废标|流标)|((本|该)(项目|标段|合同|合同包|采购包|次)\w{,5})((失败|终止|流标|废标)|予以(废标|流标)|(按|做|作)?(流标|废标|废置)处理)|(采购|招标|询价|议价|竞价|比价|比选|遴选|邀请|邀标|磋商|洽谈|约谈|谈判|竞谈|应答|项目)(终止|中止|废标|流标|失败|作废|异常|撤销)|(废标|流标)处理', '废标公告2': '(无效|中止|终止|废标|流标|失败|作废|异常|撤销)([\((](无效|中止|终止|废标|流标|失败|作废|异常|撤销)[\))])?的?(原因|理由)|本项目因故取消|本(项目|次)(公开)?\w{2}失败|已终止\s*原因:|(人|人数|供应商|单位)(不足|未达\w{,3}数量)|已终止|不足[3三]家|无(废标)|成交情况:\s*[流废]标|现予以废置|报名未够三家', '废标公告neg': '超过此报价将作为[废流]标处理|否则按[废流]标处理|终止规则:|成交规则:|视为流标|竞价失败的一切其他情形|是否废标:否|若不足三家公司参与|供应商数量:?\s*报名供应商不足三家|有效报价不足三家,\s*系统自动废标|如遇项目流[标采]|[如若][\u4e00-\u9fa5]{,10}(不足|不够|未够|未达)[3三]家' # 503076535 供应商数量: 报名供应商不足三家。 } self.title_life_dic = { '采购意向': '采购意向|招标意向|选取意向|意向公告|意向公示|意向公开', '招标预告': '预公?告|预公示|报建公告|(批前|标前)公示|(供应|招标)计划(表|书|任务|公告|公示|发布)?$|[\[【](供应|招标)计划[\]】]|(论证|征求|征集)(供应商)?意见|意见征询|需求评审公告|需求(公告|公示|意见)', '公告变更': '第[\d一二]次变更|(变更|更正(事项)?|更改|延期|暂停|恢复)(招标|采购)?的?(公告|公示|通知)|变更$|更正$', '招标答疑': '质疑|澄清|答疑(文件)?|补遗书?|(最高(投标)?限价|控制价|拦标价)(公示|公告|$)|补充说明', '废标公告': '(终止|中止|废标|废除|废置|流标|失败|作废|异常|撤销|撤回|取消成?交?|流拍|停止)(结果|竞价|项目)?的?(公告|公示|$)|(终止|中止)(采购|招标|询价|议价|竞价|比价|比选|遴选|邀请|邀标|磋商|洽谈|约谈|谈判|拍卖|招租|交易|出让)|关于废置|(招标|询价|询比价?|议价|比选|遴选|磋商)结束的?(公告|公示)|(废标|流标)处理', '合同公告': '(合同(成交|变更)?)(公告|公示|信息|公式|公开|签订)|合同备案|合同书|合同$', # |(履约|验收)(结果)? '候选人公示': '候选人(变更)?公示|评标(结果)?(公[告示]|报告)|评审结果|(中标|中选)候选人$|候选人投标文件', #中标前公示|中标预公示| '中标信息': '(中标|中选|中价|中租|成交)?|入选|确认)(候选人|人|供应商|记录|结果|变更|情况)?的?(公告|公示|结果)|未?入围(公示|公告)|(遴选|采购|招标|竞价|议价|比选|询比?价|评选|谈判|邀标|邀请|洽谈|约谈|评标|发包|磋商|交易|出让|抽取|抽签)\w{,2}结果|单一来源(采购|招标)?的?(中标|成交|结果)|中标通知书|中标$|项目中标|(项目|工程|服务|定点)的?结果公[告示]|超市直购订单', # |开标(记录|信息|情况) '资审结果': '((资格|资质)(审查|预审|后审|审核)|资审)结果(公告|公示)?|(资质|资格)(预审|后审)公示|资审及业绩公示', '招标公告': '(采购|招标|询价|询比价?|议价|竞价|比价|比选|遴选|邀请|邀标|磋商|洽谈|约谈|谈判|拍卖|招租|交易|出让)的?(公告|公示|$)|公开(采购|招标|招租|拍卖|挂牌|出让|采招|招采)|(资审|预审|后审)公告', '开标记录': '开标记录|截标信息|评委名单公示|开标安排|开标数据表|开标信息|开标情况|开标一览表|开标结果|开标会|评审专家公示|开标日程', '验收合同': '(验收|履约)(公告|公示)|(验收|履约)(结果|报告|意见|单|记录)(公告|公示)|预留项目执行情况|项目验收$' } def load_life(self,life_model,config): with tf.Graph().as_default() as graph: output_graph_def = graph.as_graph_def() with open(os.path.join(INTERFACE_DIR, life_model), 'rb') as f: output_graph_def.ParseFromString(f.read()) tf.import_graph_def(output_graph_def, name='') # print("%d ops in the final graph" % len(output_graph_def.node)) del output_graph_def sess = tf.Session(graph=graph,config=config) sess.run(tf.global_variables_initializer()) inputs = sess.graph.get_tensor_by_name('inputs/inputs:0') prob = sess.graph.get_tensor_by_name('inputs/dropout:0') title = sess.graph.get_tensor_by_name('inputs/title:0') mask = sess.graph.get_tensor_by_name('inputs/mask:0') mask_title = sess.graph.get_tensor_by_name('inputs/mask_title:0') # logit = sess.graph.get_tensor_by_name('output/logit:0') softmax = sess.graph.get_tensor_by_name('output/softmax:0') return sess, title, inputs, prob, softmax, mask, mask_title def load_type(self,type_model): with tf.Graph().as_default() as graph: output_graph_def = graph.as_graph_def() with open(os.path.join(INTERFACE_DIR, type_model), 'rb') as f: output_graph_def.ParseFromString(f.read()) tf.import_graph_def(output_graph_def, name='') # print("%d ops in the final graph" % len(output_graph_def.node)) del output_graph_def sess = tf.Session(graph=graph) sess.run(tf.global_variables_initializer()) inputs = sess.graph.get_tensor_by_name('inputs/inputs:0') prob = sess.graph.get_tensor_by_name('inputs/dropout:0') title = sess.graph.get_tensor_by_name('inputs/title:0') mask = sess.graph.get_tensor_by_name('inputs/mask:0') mask_title = sess.graph.get_tensor_by_name('inputs/mask_title:0') # logit = sess.graph.get_tensor_by_name('output/logit:0') softmax = sess.graph.get_tensor_by_name('output/softmax:0') return sess, title, inputs, prob, softmax, mask, mask_title def predict_process(self, docid='', doctitle='', dochtmlcon=''): # print('准备预处理') def get_kw_senten(s, span=10): doc_sens = [] tmp = 0 num = 0 end_idx = 0 for it in re.finditer(self.kws, s): # '|'.join(keywordset) left = s[end_idx:it.end()].split() right = s[it.end():].split() tmp_seg = s[tmp:it.start()].split() if len(tmp_seg) > span or tmp == 0: doc_sens.append(' '.join(left[-span:] + right[:span])) end_idx = it.end() + 1 + len(' '.join(right[:span])) tmp = it.end() num += 1 if num >= self.sentence_num: break if doc_sens == []: doc_sens.append(s) return doc_sens def word2id(wordlist, max_len=self.sequen_len): ids = [getIndexOfWords(w) for w in wordlist] ids = ids[:max_len] if len(ids) >= max_len else ids + [0] * (max_len - len(ids)) assert len(ids) == max_len return ids cost_time = dict() datas = [] datas_title = [] try: segword_title = ' '.join(selffool.cut(doctitle)[0]) segword_content = dochtmlcon except: segword_content = '' segword_title = '' if isinstance(segword_content, float): segword_content = '' if isinstance(segword_title, float): segword_title = '' segword_content = segword_content.replace(' 中 选 ', ' 中选 ').replace(' 中 标 ', ' 中标 ').replace(' 补 遗 ', ' 补遗 '). \ replace(' 更 多', '').replace(' 更多', '').replace(' 中 号 ', ' 中标 ').replace(' 中 选人 ', ' 中选人 '). \ replace(' 点击 下载 查看', '').replace(' 咨询 报价 请 点击', '').replace('终结', '终止') segword_title = re.sub('[^\s\u4e00-\u9fa5]', '', segword_title) segword_content = re.sub('[^\s\u4e00-\u9fa5]', '', segword_content) doc_word_list = segword_content.split() if len(doc_word_list) > self.sequen_len / 2: doc_sens = get_kw_senten(' '.join(doc_word_list[100:500])) doc_sens = ' '.join(doc_word_list[:100]) + '\n' + '\n'.join(doc_sens) else: doc_sens = ' '.join(doc_word_list[:self.sequen_len]) # print('标题:',segword_title) # print('正文:',segword_content) datas.append(doc_sens.split()) datas_title.append(segword_title.split()) # print('完成预处理') return datas, datas_title def is_houxuan(self, title, content): ''' 通过标题和中文内容判断是否属于候选人公示类别 :param title: 公告标题 :param content: 公告正文文本内容 :return: 1 是候选人公示 ;0 不是 ''' if re.search('候选人的?公示|评标结果|评审结果|中标公示', title): # (中标|成交|中选|入围) if re.search('变更公告|更正公告|废标|终止|答疑|澄清', title): return 0 return 1 if re.search('候选人的?公示', content[:100]): if re.search('公示(期|活动)?已经?结束|公示期已满|中标结果公告|中标结果公示|变更公告|更正公告|废标|终止|答疑|澄清', content[:100]): return 0 return 1 else: return 0 def predict(self, title='', list_sentence='', web_source_no='', original_docchannel=''): not_extract_dic = { 104: '招标文件', 106: '法律法规', 107: '新闻资讯', 108: '拟建项目', 109: '展会推广', 110: '企业名录', 111: '企业资质', 112: '全国工程人员', 113: '业主采购' } if original_docchannel in not_extract_dic: return {'docchannel': {'docchannel':'', 'doctype':not_extract_dic[original_docchannel], "original_docchannel_id": str(original_docchannel)}} if web_source_no in ['02104-7']: return {'docchannel': {'docchannel':'', 'doctype':'采招数据'}} if isinstance(list_sentence, list): token_l = [it.tokens for it in list_sentence] tokens = [it for l in token_l for it in l] content = ' '.join(tokens[:500]) title = re.sub('[^\u4e00-\u9fa5]', '', title) if len(title)>50: title = title[:20]+title[-30:] data_content, data_title = self.predict_process(docid='', doctitle=title[-50:], dochtmlcon=content) # 标题最多取50字 text_len = len(data_content[0]) if len(data_content[0])]*richTextFetch', html) # if ser and len(re.sub('[^\u4e00-\u9fa5]', '', html[:ser.start()]))>500: # html = html[:ser.start()]+'##richTextFetch##' if ser: if len(re.sub('[^\u4e00-\u9fa5]', '', html[:ser.start()])) > 200: html = html[:ser.start()] + '##richTextFetch##' else: html = html[:ser.start() + 500] text = re.sub('<[^<]*?>', '', html).replace(' ', ' ') # text = re.sub('http[0-9a-zA-Z-.:/]+|[0-9a-zA-Z-./@]+', '', text) text = re.sub('\s+', ' ', text) # text = re.sub('[/|[()()]', '', text) text = cut_single_cn_space(text) return text[:20000] def count_diffser(pattern, text): num = 0 kw = [] for p in pattern.split(';'): if re.search(p, text): num += 1 kw.append(re.search(p, text).group(0)) return num, ';'.join(kw) def is_single_source(bidway, title): if re.search('单一来源|单一性采购', title): return True elif bidway == '单一来源': return True else: return False def get_type(title, text): if re.search(self.title_type_dic['新闻资讯2'], title): return '新闻资讯', re.search(self.title_type_dic['新闻资讯2'], title).group(0) if re.search(self.title_type_dic['土地矿产'], title) or re.search(self.type_dic['土地矿产'],text): # and re.search('(土地|用地|宗地|地块)(经营权)?(流转|承包|出租|招租|租赁|确权)', text)==None if re.search(self.title_type_dic['采招数据'], text.strip().split(' ')[0] + title) \ and not re.search(self.title_type_dic['土地矿产2'], title): return '采招数据', re.search(self.title_type_dic['采招数据'], text.strip().split(' ')[0] + title).group(0) return '土地矿产', (re.search(self.title_type_dic['土地矿产'], title) or re.search(self.type_dic['土地矿产'], text)).group(0) if (re.search(self.title_type_dic['拍卖出让'], title) or re.search(self.type_dic['拍卖出让'], text)): if re.search(self.title_type_dic['采招数据'], text.strip().split(' ')[0] + title) \ and not re.search(self.title_type_dic['拍卖出让2'], title): return '采招数据', re.search(self.title_type_dic['采招数据'], text.strip().split(' ')[0] + title).group(0) return '拍卖出让', (re.search(self.title_type_dic['拍卖出让'], title) or re.search(self.type_dic['拍卖出让'], text)).group(0) if re.search(self.title_type_dic['产权交易'], title) or re.search(self.type_dic['产权交易'], text): if re.search(self.title_type_dic['采招数据'], text.strip().split(' ')[0] + title) \ and not re.search(self.title_type_dic['产权交易2'], title): return '采招数据', re.search(self.title_type_dic['采招数据'], text.strip().split(' ')[0] + title).group(0) return '产权交易', (re.search(self.title_type_dic['产权交易'], title) or re.search(self.type_dic['产权交易'], text)).group(0) if re.search(self.title_type_dic['采招数据'], title) or re.search(self.type_dic['采招数据'], title + text): return '采招数据', (re.search(self.title_type_dic['采招数据'], title) or re.search(self.type_dic['采招数据'], title + text)).group(0) if (re.search(self.title_type_dic['采招数据'], title +text.strip().split(' ')[0]) or re.search("银行|资格选定|资格认定|资金|公款|存款|存放|现金管理|招募|入围|入库", title +text.strip().split(' ')[0])) and \ not re.search(self.title_type_dic['新闻资讯2'], title): return '采招数据', (re.search(self.title_type_dic['采招数据'], title +text.strip().split(' ')[0]) or re.search("银行|资格选定|资格认定|资金|公款|存款|存放|现金管理|招募|入围|入库", title +text.strip().split(' ')[0])).group(0) if re.search(self.title_type_dic['新闻资讯'], title): return '新闻资讯', re.search(self.title_type_dic['新闻资讯'], title).group(0) # else: return '', '没有公告类型关键词,返回空' def get_life(title, text ,doc_type=None): def words_dump(list_words): _words_l_l = [] list_words.sort(key=lambda x: len(x)) for _l in list_words: _exists = False for l1 in _words_l_l: if l1 in _l: _exists = True break if not _exists: _words_l_l.append(_l) return _words_l_l title = re.sub('[-()()0-9a-z]|第?[二三四]次公?告?', '', title) first_line = text.split()[0] if len(text.split()) > 2 else '' if title.strip()[-2:] not in ['公告', '公示'] and 5 < len(first_line) < 50 and first_line[-2:] in ['公告', '公示']: # print('title: ', title, first_line) title += first_line def count_score(l): return len(l) + len(set(l)) * 2 life_kw_title = {} life_kw_content = {} life_score = {} # msc = "" # 查找标题每个类别关键词 for k, v in self.title_life_dic.items(): k2 = re.sub('[\da-z]', '', k) if k2 not in life_kw_title: life_kw_title[k2] = [] for it in re.finditer(v, title): life_kw_title[k2].append(it.group(0)) # 查找正文每个类别关键词 for k, v in self.life_dic.items(): k2 = re.sub('[\da-z]', '', k) if k2 not in life_kw_content: life_kw_content[k2] = {'pos': [], 'neg': []} for it in re.finditer(v, text): if 'neg' not in k: life_kw_content[k2]['pos'].append(it.group(0)) else: life_kw_content[k2]['neg'].append(it.group(0)) for k2 in life_kw_content: life_kw_content[k2]['pos'] = words_dump(life_kw_content[k2]['pos'])# 关键词去重 life_kw_content[k2]['neg'] = words_dump(life_kw_content[k2]['neg']) life_score[k2] = count_score(life_kw_content[k2]['pos']) - count_score( life_kw_content[k2]['neg']) life_kw_title = {k: v for k, v in life_kw_title.items() if v != []} life_kw_content = {k: v for k, v in life_kw_content.items() if life_score[k] > 0} msc = [life_kw_title, life_kw_content, life_score] msc = json.dumps(msc, ensure_ascii=False) max_score = 0 life_list = [] for k in life_score.keys(): if life_score[k] > max_score: max_score = life_score[k] life_list = [k] elif life_score[k] == max_score and life_score[k] > 0: life_list.append(k) #add 关键词规则明确时可直接返回,符合多个规则时调用新模型 if doc_type=="采招数据" and len(text)>300: life_list_in_title = [l for l in life_list if l in life_kw_title]# 标题正文同时有关键词的 if len(life_list_in_title)==1: life_list = life_list_in_title life_kw_title = {life_list_in_title[0]:life_kw_title[life_list_in_title[0]]} # elif len(life_list_in_title)==0: # return "", msc if len(life_kw_title)==0: if len(life_list)==0 or len(life_list)>1:# 关键词最高score有多个相同,直接用新模型 return "", msc if '采购意向' in life_kw_title or '采购意向' in life_list: if '中标信息' in life_kw_title or '中标信息' in life_list: return '中标信息', msc elif '候选人公示' in life_kw_title: return '候选人公示', msc elif set(['候选人公示', '合同公告']) & set(life_kw_title) != set(): return '', msc return '采购意向', msc elif '招标预告' in life_kw_title or '招标预告' in life_list: if '中标信息' in life_kw_title or '中标信息' in life_list: return '中标信息', msc elif '候选人公示' in life_kw_title: return '候选人公示', msc elif set(['候选人公示', '合同公告']) & set(life_kw_title) != set(): return '', msc return '招标预告', msc elif '公告变更' in life_kw_title or '公告变更' in life_list: if life_score.get('候选人公示', 0) > 3 or '候选人公示' in life_kw_title: return '候选人公示', msc elif life_score.get('合同公告', 0) > 3 or '合同公告' in life_kw_title: return '合同公告', msc elif life_score.get('中标信息', 0) > 3 or '中标信息' in life_kw_title: return '中标信息', msc elif '招标公告' in life_kw_title and re.search('变更|更正|更改|延期', title[-4:])==None and life_score.get('公告变更', 0) < 4: return '招标公告', msc return '公告变更', msc elif '招标答疑' in life_kw_title or '招标答疑' in life_list: if '招标公告' in life_kw_title and life_score.get('招标答疑', 0) < 4: return '招标公告', msc elif life_score.get('招标答疑', 0) < max_score: if max_score > 3 and len(life_list) == 1: return life_list[0], msc return '', msc return '招标答疑', msc elif '开标记录' in life_kw_title: return '开标记录', msc elif '验收合同' in life_kw_title: return '验收合同', msc elif '候选人公示' in life_kw_title or '候选人公示' in life_list: if '招标公告' in life_kw_title and '候选人公示' not in life_kw_title: # and life_score.get('招标公告', 0) > 3 return '招标公告', msc elif '废标公告' in life_kw_title or life_score.get('废标公告', 0) > 5: return '废标公告', msc return '候选人公示', msc elif '合同公告' in life_kw_title or '合同公告' in life_list: if '招标公告' in life_kw_title and life_score.get('招标公告', 0) > 3: return '招标公告', msc elif '废标公告' in life_kw_title or life_score.get('废标公告', 0) > 5: return '废标公告', msc return '合同公告', msc elif '中标信息' in life_kw_title or '中标信息' in life_list: if '招标公告' in life_kw_title and '中标信息' not in life_kw_title and life_score.get('招标公告',0) >= life_score.get('中标信息',0): # (life_score.get('招标公告', 0)>2 or life_score.get('中标信息', 0)<4) 0.7886409793924245 return '招标公告', msc elif '废标公告' in life_kw_title or life_score.get('废标公告', 0) > 5: return '废标公告', msc elif life_score.get('候选人公示', 0) > 3: return '候选人公示', msc elif life_score.get('合同公告', 0) > 5: return '合同公告', msc return '中标信息', msc elif '废标公告' in life_kw_title or '废标公告' in life_list: if life_score.get('招标公告', 0) > 3 and '废标公告' not in life_kw_title: return '招标公告', msc return '废标公告', msc elif '资审结果' in life_kw_title or '资审结果' in life_list: return '资审结果', msc elif '招标公告' in life_kw_title or '招标公告' in life_list: return '招标公告', msc return '', msc def get_model_inputs(list_sentence): list_sentence = sorted(list_sentence, key=lambda x:x.sentence_index) token_l = [it.tokens for it in list_sentence] tokens = [it for l in token_l for it in l] content = ' '.join(tokens[:500]) data_content, data_title = self.predict_process(docid='', doctitle=title[-50:], dochtmlcon=content) # 标题最多取50字 text_len = len(data_content[0]) if len(data_content[0]) < self.sequen_len else self.sequen_len title_len = len(data_title[0]) if len(data_title[0]) < self.title_len else self.title_len array_content = embedding(data_content, shape=(len(data_content), self.sequen_len, 128)) array_title = embedding(data_title, shape=(len(data_title), self.title_len, 128)) return array_content, array_title ,text_len, title_len, content def type_model_predict(): pred = self.type_sess.run(self.type_softmax, feed_dict={ self.type_title: array_title, self.type_content: array_content, self.type_mask: [[0] * text_len + [1] * (self.sequen_len - text_len)], self.type_mask_title: [[0] * title_len + [1] * (self.title_len - title_len)], self.type_prob: 1} ) id = np.argmax(pred, axis=1)[0] prob = pred[0][id] return id, prob def life_model_predict(): pred = self.lift_sess.run(self.lift_softmax, feed_dict={ self.lift_title: array_title, self.lift_content: array_content, self.mask: [[0] * text_len + [1] * (self.sequen_len - text_len)], self.mask_title: [[0] * title_len + [1] * (self.title_len - title_len)], self.lift_prob: 1} ) id = np.argmax(pred, axis=1)[0] prob = pred[0][id] return id, prob not_extract_dic = { 104: '招标文件', 106: '法律法规', 107: '新闻资讯', 108: '拟建项目', 109: '展会推广', 110: '企业名录', 111: '企业资质', 112: '全国工程人员', 113: '业主采购' } origin_dic = {51: '公告变更', 52: '招标公告', 101: '中标信息', 102: '招标预告', 103: '招标答疑', 104: '招标文件', 105: '资审结果', 106: '法律法规', 107: '新闻资讯', 108: '拟建项目', 109: '展会推广', 110: '企业名录', 111: '企业资质', 112: '全国工程', 113: '业主采购', 114: '采购意向', 115: '拍卖出让', 116: '土地矿产', 117: '产权交易', 118: '废标公告', 119: '候选人公示', 120: '合同公告'} title = re.sub('[^\u4e00-\u9fa5《》]+|出租车', '', title) if len(title) > 50: title = title[:20] + title[-30:] text = html2text(html) self.origin_dic = origin_dic self.title = title self.text = text if original_docchannel in not_extract_dic: return {'docchannel': {'docchannel': '', 'doctype': not_extract_dic[original_docchannel], 'life_docchannel': origin_dic.get(original_docchannel, '原始类别')}}, '公告类别不在提取范围' if web_source_no in ['02104-7', '04733', 'DX007628-6']: # 这些数据源无法识别 return {'docchannel': {'docchannel': origin_dic.get(original_docchannel, '原始类别'), 'doctype': '采招数据', 'life_docchannel': origin_dic.get(original_docchannel, '原始类别')}}, '此数据源公告分类不明确,返回数据源类别' if original_docchannel == 303: return {'docchannel': {'docchannel': '处罚公告', 'doctype': '处罚公告', 'life_docchannel': '处罚公告'}}, "源类别为处罚公告" result = {'docchannel': {'docchannel': '', 'doctype': ''}} doc_type, type_kw = get_type(title, text) # print(doc_type, type_kw) # doc_life, life_kw = get_life(title, text, prem_json, bidway, original_docchannel) # doc_life, life_kw = get_life(title, text) doc_life, life_kw = get_life(title, text[:5000],doc_type=doc_type) # print('doc_life',doc_life, life_kw) if doc_type in self.title_type_dic: result['docchannel']['doctype'] = doc_type if doc_life in self.title_life_dic: result['docchannel']['docchannel'] = doc_life # print('channel正则预测结果:', result) msc = '正则结果:类型:%s, 关键词:%s, 周期:%s, 关键词:%s'%(doc_type, type_kw,doc_life, life_kw)+'\n'+'模型结果:' # print('类型:%s, 关键词:%s, 周期:%s, 关键词:%s'%(doc_type, type_kw,doc_life, life_kw)) if doc_type == "" or doc_life == "" or (doc_type != '采招数据' and origin_dic.get(original_docchannel, '原始类别') in ['招标公告', '中标信息', '招标预告', '采购意向']): array_content, array_title, text_len, title_len, content = get_model_inputs(list_sentence) if doc_type == "" or (doc_type != '采招数据' and origin_dic.get(original_docchannel, '原始类别') in ['招标公告', '中标信息', '招标预告', '采购意向']) and \ not re.search(self.title_type_dic["新闻资讯2"],title): type_id, type_prob = type_model_predict() type_model = self.id2type[type_id] if type_model == '新闻资讯' and doc_life!='': # 修复bug 78584245 "docchannel": "合同公告", "doctype": "新闻资讯", result['docchannel']['doctype'] = '采招数据' msc += '模型结果为新闻资讯,生命周期不为空,改为采招数据;' else: result['docchannel']['doctype'] = type_model msc += type_model + ' 概率:%.4f;'%type_prob # print('公告类别:', self.id2type[id], '概率:',prob) if result['docchannel']['doctype'] in ('产权交易', '土地矿产', '拍卖出让'): # 竞买类公告,取最低价成交的修正为‘采招数据’ if re.search("竞买",title) and re.search("最低价.{,2}成交|中标为最低报价|出价最低者为中标",text): result['docchannel']['doctype'] = '采招数据' # doc_type为采招数据时重新预测doc_life if result['docchannel']['doctype'] == "采招数据": doc_life, life_kw = get_life(title, text[:5000], doc_type="采招数据") if doc_life=="" and result['docchannel']['doctype'] not in ['', '新闻资讯']: if result['docchannel']['doctype'] != "采招数据" or len(text)<=300: # doc_type为采招数据或公告过短时,使用新模型预测 # if len(text)>150 and re.search(self.kws, content): if len(text)>100 and re.search(self.kws, content): life_id, life_prob = life_model_predict() if life_prob>=0.8 or (len(text)<=300 and life_prob>=0.6): life_model = self.id2life[life_id] result['docchannel']['docchannel'] = life_model msc += life_model + ' 概率:%.4f;\n'%life_prob # msc = final_change(msc) # print('channel ', msc) return result, msc def final_change(self, result, prem, original_docchannel,nlp_enterprise,nlp_enterprise_attachment, msc): ''' :param result: channel 结果字典 :param prem: :param title: 标题 :param text: 正文 :param original_docchannel: 站源类别 :param msc: 备注 :return: channel结果字典 ''' ''' 修改逻辑: 1、中标公告、合同公告无中标人且原始为非中标,返回原类型 2、废标公告有中标人且标题无废标关键词,返回中标信息 3、答疑公告标题无答疑关键且原始为招标,返回原始类别 4、招标公告有中标人且原始为中标,返回中标信息 5、预测为招标,原始为预告、意向,返回原始类别 6、预测及原始均在变更、答疑,返回原始类别 7、预测为采招数据,原始为产权且有关键词,返回原始类别 8、废标公告原始为招标、预告且标题无废标关键期,返回原始类别 9、若预测为非采招数据且源网为采招数据且有招标关键词返回采招数据 10、招标公告有中标人,且标题有直购关键词,改为中标信息 11、预测预告,原始为意向、招标且标题无预告关键词,返回原始类别 ''' def is_contain_winner(extract_json): if re.search('win_tenderer', extract_json): return True else: return False tenderee = "" agency = "" try: for k, v in prem['prem'].items(): for link in v['roleList']: if link['role_name'] == 'tenderee' and tenderee == "": tenderee = link['role_text'] if link['role_name'] == 'agency' and agency == "": agency = link['role_text'] except Exception as e: # print('解析prem 获取招标人、代理人出错') pass origin_dic = self.origin_dic title = self.title text = self.text # 剔除招标单位名称影响 if tenderee: title = title.replace(tenderee, " ") text = text.replace(tenderee, " ") if agency: title = title.replace(agency, " ") text = text.replace(agency, " ") enterprise_list = list(set(nlp_enterprise + nlp_enterprise_attachment)) enterprise_list.sort(key = lambda x:len(x), reverse=True) for enterprise in enterprise_list: if enterprise and enterprise not in (tenderee,agency): text = text.replace(enterprise, " ") prem_json = json.dumps(prem, ensure_ascii=False) if result['docchannel']['docchannel'] in ['中标信息', '合同公告'] and origin_dic.get( original_docchannel, '') in ['招标公告', '采购意向', '招标预告', '公告变更'] and is_contain_winner( prem_json) == False and re.search(self.title_life_dic['中标信息'], title+text[:40]) == None: result['docchannel']['docchannel'] = origin_dic.get(original_docchannel, '') msc += '最终规则修改:中标公告、合同公告无中标人且原始为非中标,返回原类型' elif result['docchannel']['docchannel'] in ['中标信息'] and is_contain_winner(prem_json) == False \ and re.search("监督(抽查|检查)结果|抽查结果", title): result['docchannel']['doctype'] = "新闻资讯" result['docchannel']['docchannel'] = "" msc += '最终规则修改:中标公告无中标人且包含新闻资讯关键词,返回新闻资讯类型' elif result['docchannel']['docchannel'] in ['中标信息'] and is_contain_winner(prem_json) == False \ and (re.search(self.title_life_dic['废标公告'],title) or re.search(self.life_dic['废标公告'],text[:400]) or re.search(self.life_dic['废标公告2'],text[:400])): result['docchannel']['docchannel'] = "废标公告" msc += '最终规则修改:中标公告无中标人且包含废标公告关键词,改为废标公告' elif result['docchannel']['docchannel'] == '废标公告' and is_contain_winner(prem_json) and re.search( self.title_life_dic['废标公告'], title) == None: if re.search(self.title_life_dic['合同公告'], title): result['docchannel']['docchannel'] = '合同公告' else: result['docchannel']['docchannel'] = '中标信息' msc += '最终规则修改:预测为废标却有中标人且标题无废标关键词改为中标信息;' elif result['docchannel']['docchannel'] in ['招标答疑'] and re.search( self.title_life_dic['招标答疑'], title) == None and origin_dic.get( original_docchannel, '') in ['招标公告', '采购意向', '招标预告']: result['docchannel']['docchannel'] = origin_dic.get(original_docchannel, '') msc += '最终规则修改:答疑公告标题无答疑关键且原始为招标,返回原始类别;' elif result['docchannel']['docchannel'] == '招标公告' and is_contain_winner(prem_json) and (origin_dic.get( original_docchannel, '') == '中标信息' or re.search('直接采购|单源直采|直采', title)): # 20241025补充 标题包含直接采购且有中标人的为中标公告 result['docchannel']['docchannel'] = '中标信息' msc += '最终规则修改:预测为招标公告却有中标人且原始为中标改为中标信息;' elif result['docchannel']['docchannel'] in ['招标公告'] and origin_dic.get( original_docchannel, '') in ['采购意向', '招标预告']: result['docchannel']['docchannel'] = origin_dic.get(original_docchannel, '') msc += '最终规则修改:预测为招标,原始为预告、意向,返回原始类别' elif result['docchannel']['docchannel'] in ['招标预告'] and origin_dic.get( original_docchannel, '') in ['采购意向', '招标公告'] and re.search( self.title_life_dic['招标预告'], title) == None and re.search( '预公?告|预公示|报建公告|(批前|标前)公示|(供应|招标|采购)计划表?|(论证|征求|征集)(供应商)?意见|意见征询|需求评审公告|需求(公告|公示|意见)',text[:40]) == None: result['docchannel']['docchannel'] = origin_dic.get(original_docchannel, '') msc += '最终规则修改:预测预告,原始为意向、招标且标题无预告关键词,返回原始类别' elif result['docchannel']['docchannel'] in ['招标答疑', '公告变更'] and origin_dic.get( original_docchannel, '') in ['招标答疑', '公告变更']: if re.search(self.title_life_dic['公告变更'], title): result['docchannel']['docchannel'] = "公告变更" msc += '最终规则修改:预测及原始均在答疑、变更,返回原始类别' elif re.search(self.title_life_dic['招标答疑'], title): result['docchannel']['docchannel'] = "招标答疑" msc += '最终规则修改:预测及原始均在答疑、变更,返回原始类别' else: result['docchannel']['docchannel'] = origin_dic.get(original_docchannel, '') msc += '最终规则修改:预测及原始均在答疑、变更,返回原始类别' elif result['docchannel']['doctype'] == '采招数据' and origin_dic.get( original_docchannel, '') in ['产权交易', '土地矿产'] and re.search('产权|转让|受让|招租|出租|承租|竞价', text) and not re.search('公开(采购|招标|采招|招采)', title): result['docchannel']['doctype'] = origin_dic.get(original_docchannel, '') # print(re.findall('产权|转让|受让|招租|出租|承租|竞价', text)) msc += '最终规则修改:预测为采招数据,原始为产权且有关键词,返回原始类别' elif result['docchannel']['docchannel'] == '废标公告' and origin_dic.get( original_docchannel, '') in ['招标公告', '采购意向', '招标预告'] and re.search( self.title_life_dic['废标公告'], title+text[:50]) == None and re.search( self.life_dic['废标公告'], title+text[:50]) == None and re.search( self.life_dic['废标公告2'], title+text[:50]) == None and re.search( self.title_life_dic['中标信息'], title) == None: result['docchannel']['docchannel'] = origin_dic.get(original_docchannel, '') msc += '最终规则修改:废标公告原始为招标、预告且标题无废标关键词,返回原始类别;' elif result['docchannel']['docchannel'] in ['招标公告', '招标预告'] and is_contain_winner( prem_json) and re.search('直购', title): result['docchannel']['docchannel'] = '中标信息' msc += "最终规则修改:预测为招标却有中标人且标题有直购关键词返回中标" elif result['docchannel']['docchannel'] == '开标记录' and '开标结果' in title and is_contain_winner(prem_json): msc += "最终规则修改:开标结果包含中标人的作为中标信息" result['docchannel']['docchannel'] = '中标信息' if result['docchannel']['doctype'] in ['产权交易', '土地矿产', '拍卖出让'] and origin_dic.get( original_docchannel, '') not in ['产权交易', '土地矿产', '拍卖出让'] \ and (re.search(self.title_type_dic['采招数据'], title) or re.search('采购|询价|磋商', title) or re.search('(采购|招投?标|投标)(信息|内容|项目|公告|数量|人|单位|方式)|(建设|工程|服务|施工|监理|勘察|设计)项目|(%s)' % self.type_dic['采招数据'], text) ): # print('test',re.findall('(采购|招投?标|投标)(信息|内容|项目|公告|数量|人|单位|方式)|(建设|工程|服务|施工|监理|勘察|设计)项目|(%s)' % self.type_dic['采招数据'], text)) result['docchannel']['doctype'] = '采招数据' msc += ' 最终规则修改:预测为非采招数据,原始为采招数据且有招标关键词,返回采招数据' elif result['docchannel']['doctype'] in ['土地矿产'] and origin_dic.get(original_docchannel, '') in ['拍卖出让', '产权交易']: if origin_dic.get(original_docchannel, '') in ['拍卖出让'] and ( re.search(self.title_type_dic['拍卖出让'], title) or re.search(self.type_dic['拍卖出让'], text)): result['docchannel']['doctype'] = '拍卖出让' msc += "最终规则修改:预测为土地矿产原始为拍卖且有拍卖关键词,返回拍卖" elif (re.search(self.title_type_dic['产权交易'], title) or re.search(self.type_dic['产权交易'], text)): result['docchannel']['doctype'] = '产权交易' msc += "最终规则修改:预测为土地矿产原始为产权交易且有产权交易关键词,返回产权交易" '''下面是新格式增加返回字段''' if result['docchannel']['docchannel'] != '': # 预测到生命周期的复制到life_docchannel,否则用数据源结果 result['docchannel']['life_docchannel'] = result['docchannel']['docchannel'] else: result['docchannel']['life_docchannel'] = origin_dic.get(original_docchannel, '原始类别') return result, msc