Utils.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. # -*- coding: utf-8 -*-
  2. '''``common/Utils.py`` — Phase 3 拆分后的兼容层。
  3. 按 ARCHITECTURE.md Phase 3 拆分建议,原 ``common/Utils.py`` 中的函数和全局
  4. 变量已按职责迁移到以下 7 个目标模块:
  5. | 目标文件 | 迁出内容 | 类型 |
  6. |---|---|---|
  7. | ``common/json_encoder.py`` | ``MyEncoder`` | RULE FRAMEWORK |
  8. | ``model_runtime/viterbi.py`` | ``viterbi_decode`` | CORE |
  9. | ``model_runtime/vocab.py`` | ``vocab_word``、char/id 映射等 | CORE |
  10. | ``model_runtime/embed.py`` | w2v 加载、``embedding`` 等 | CORE |
  11. | ``services/external_inference/paieas.py`` | PAI-EAS 客户端 | INFRA |
  12. | ``common/context_utils.py`` | 上下文窗口、金额标准化等纯函数 | CORE |
  13. | ``common/logging.py`` | ``log`` / ``debug`` / ``logger`` | INFRA |
  14. 本文件通过 re-export 保持全部旧 import 路径可用:
  15. ``from BiddingKG.dl.common.Utils import viterbi_decode`` 仍然有效
  16. ``from BiddingKG.dl.common.Utils import log`` 仍然有效
  17. ``from BiddingKG.dl.common.Utils import *`` 仍然有效
  18. 未迁移的函数(时间处理、keras metrics、HTML 工具、pickle save/load、
  19. encodeInput 等)仍保留在本文件中,后续 Phase 再拆。
  20. '''
  21. from __future__ import absolute_import
  22. import os
  23. import re
  24. import time
  25. import pickle
  26. import sys
  27. import traceback
  28. import numpy as np
  29. from keras import backend as K
  30. from lxml import etree
  31. from datetime import date
  32. import calendar
  33. # ============================================================
  34. # Re-exports from model_runtime
  35. # ============================================================
  36. from BiddingKG.dl.model_runtime.viterbi import viterbi_decode
  37. from BiddingKG.dl.model_runtime.embed import (
  38. model_w2v,
  39. lock_model_w2v,
  40. model_word,
  41. lock_model_word,
  42. model_word_file,
  43. Lazy_load,
  44. getLazyLoad,
  45. getw2vfilepath,
  46. getFileFromSysPath,
  47. getModel_w2v,
  48. getModel_word,
  49. embedding,
  50. embedding_word,
  51. embedding_word_forward,
  52. formEncoding,
  53. )
  54. from BiddingKG.dl.model_runtime.vocab import (
  55. vocab_word,
  56. vocab_words,
  57. file_vocab_word,
  58. file_vocab_words,
  59. fool_char_to_id,
  60. getIndexOfWord,
  61. getIndexOfWords,
  62. getIndexOfWord_fool,
  63. getVocabAndMatrix,
  64. changeIndexFromWordToWords,
  65. )
  66. # ============================================================
  67. # Re-exports from services
  68. # ============================================================
  69. from BiddingKG.dl.services.external_inference.paieas import (
  70. USE_PAI_EAS,
  71. API_URL,
  72. USE_API,
  73. tf_predict_pb2,
  74. selffool_authorization,
  75. selffool_url,
  76. selffool_seg_authorization,
  77. selffool_seg_url,
  78. codename_authorization,
  79. codename_url,
  80. form_item_authorization,
  81. form_item_url,
  82. person_authorization,
  83. person_url,
  84. role_authorization,
  85. role_url,
  86. money_authorization,
  87. money_url,
  88. codeclasses_authorization,
  89. codeclasses_url,
  90. limitRun,
  91. get_values,
  92. vpc_requests,
  93. )
  94. # ============================================================
  95. # Re-exports from common submodules
  96. # ============================================================
  97. from BiddingKG.dl.common.json_encoder import MyEncoder
  98. from BiddingKG.dl.common.logging import log, debug, logger
  99. from BiddingKG.dl.common.context_utils import (
  100. # 上下文窗口
  101. spanWindow,
  102. get_context,
  103. findAllIndex,
  104. find_index,
  105. # 金额标准化
  106. getUnifyMoney,
  107. getDigitsDic,
  108. getMultipleFactor,
  109. partMoney,
  110. uniform_num,
  111. uniform_package_name,
  112. money_process,
  113. get_money_entity,
  114. # 其他纯函数
  115. combine,
  116. fitDataByRule,
  117. clean_company,
  118. cut_repeat_name,
  119. is_all_winner,
  120. is_deposit_project,
  121. find_package,
  122. # 模块级正则
  123. package_number_pattern,
  124. filter_package_pattern,
  125. )
  126. # ============================================================
  127. # 以下为未迁移函数,仍保留在本文件中
  128. # ============================================================
  129. def getCurrent_date(format="%Y-%m-%d %H:%M:%S"):
  130. _time = time.strftime(format, time.localtime())
  131. return _time
  132. def encodeInput(data, word_len, word_flag=True, userFool=False):
  133. result = []
  134. out_index = 0
  135. for item in data:
  136. if out_index in [0]:
  137. list_word = item[-word_len:]
  138. else:
  139. list_word = item[:word_len]
  140. temp = []
  141. if word_flag:
  142. for word in list_word:
  143. if userFool:
  144. temp.append(getIndexOfWord_fool(word))
  145. else:
  146. temp.append(getIndexOfWord(word))
  147. list_append = []
  148. temp_len = len(temp)
  149. while temp_len < word_len:
  150. if userFool:
  151. list_append.append(0)
  152. else:
  153. list_append.append(getIndexOfWord("<pad>"))
  154. temp_len += 1
  155. if out_index in [0]:
  156. temp = list_append + temp
  157. else:
  158. temp = temp + list_append
  159. else:
  160. for words in list_word:
  161. temp.append(getIndexOfWords(words))
  162. list_append = []
  163. temp_len = len(temp)
  164. while temp_len < word_len:
  165. list_append.append(getIndexOfWords("<pad>"))
  166. temp_len += 1
  167. if out_index in [0, 1]:
  168. temp = list_append + temp
  169. else:
  170. temp = temp + list_append
  171. result.append(temp)
  172. out_index += 1
  173. return result
  174. def encodeInput_form(input, MAX_LEN=30):
  175. x = np.zeros([MAX_LEN])
  176. for i in range(len(input)):
  177. if i >= MAX_LEN:
  178. break
  179. x[i] = getIndexOfWord(input[i])
  180. return x
  181. def save(object_to_save, path):
  182. '''
  183. 保存对象
  184. @Arugs:
  185. object_to_save: 需要保存的对象
  186. @Return:
  187. 保存的路径
  188. '''
  189. with open(path, 'wb') as f:
  190. pickle.dump(object_to_save, f)
  191. def load(path):
  192. '''
  193. 读取对象
  194. @Arugs:
  195. path: 读取的路径
  196. @Return:
  197. 读取的对象
  198. '''
  199. with open(path, 'rb') as f:
  200. object1 = pickle.load(f)
  201. return object1
  202. # 时间合法性判断
  203. def isValidDate(year, month, day):
  204. try:
  205. date(year, month, day)
  206. except:
  207. return False
  208. else:
  209. return True
  210. time_format_pattern = re.compile("((?P<year>20\d{2}|\d{2}|二[零〇0][零〇一二三四五六七八九0]{2})\s*[-/年.]\s*(?P<month>\d{1,2}|[一二三四五六七八九十]{1,3})\s*[-/月.]?\s*(?P<day>\d{1,2}|[一二三四五六七八九十]{1,3})?)")
  211. from BiddingKG.dl.ratio.re_ratio import getUnifyNum
  212. def get_maxday(year, month):
  213. # calendar.monthrange(year, month) 返回一个元组,其中第一个元素是那个月第一天的星期几(0-6代表周一到周日),
  214. # 第二个元素是那个月的天数。
  215. _, last_day = calendar.monthrange(year, month)
  216. return last_day
  217. def timeFormat(_time, default_first_day=True):
  218. '''
  219. 日期格式化:年-月-日
  220. :param _time:
  221. :param default_first_day: True取当月第一天,否则取最后一天
  222. :return:
  223. '''
  224. current_year = time.strftime("%Y", time.localtime())
  225. all_match = re.finditer(time_format_pattern, _time)
  226. for _match in all_match:
  227. if len(_match.group()) > 0:
  228. legal = True
  229. year = ""
  230. month = ""
  231. day = ""
  232. for k, v in _match.groupdict().items():
  233. if k == "year":
  234. year = v
  235. if k == "month":
  236. month = v
  237. if k == "day":
  238. day = v
  239. if year != "":
  240. if re.search("^\d+$", year):
  241. if len(year) == 2:
  242. year = "20" + year
  243. if int(year) - int(current_year) > 10:
  244. legal = False
  245. else:
  246. _year = ""
  247. for word in year:
  248. if word == '0':
  249. _year += word
  250. else:
  251. _year += str(getDigitsDic(word))
  252. year = _year
  253. else:
  254. legal = False
  255. if month != "":
  256. if re.search("^\d+$", month):
  257. if int(month) > 12:
  258. legal = False
  259. else:
  260. month = int(getUnifyNum(month))
  261. if month >= 1 and month <= 12:
  262. month = str(month)
  263. else:
  264. legal = False
  265. else:
  266. legal = False
  267. if day == None:
  268. day = "01" if (default_first_day or legal == False) else str(get_maxday(int(year), int(month)))
  269. if day != "":
  270. if re.search("^\d+$", day):
  271. if int(day) > 31:
  272. legal = False
  273. else:
  274. day = int(getUnifyNum(day))
  275. if day >= 1 and day <= 31:
  276. day = str(day)
  277. else:
  278. legal = False
  279. else:
  280. legal = False
  281. # print(year,month,day)
  282. if not isValidDate(int(year), int(month), int(day)):
  283. legal = False
  284. if legal:
  285. return "%s-%s-%s" % (year, month.rjust(2, "0"), day.rjust(2, "0"))
  286. return ""
  287. def del_tabel_achievement(soup):
  288. if re.search('中标|成交|入围|结果|评标|开标|候选人', soup.text[:800]) == None or re.search('业绩|类似项目', soup.text) == None:
  289. return None
  290. p1 = '(中标|成交)(单位|候选人)的?(企业|项目|项目负责人|\w{,5})?业绩|类似(项目)?业绩|\w{,10}业绩$|业绩(公示|情况|荣誉)|近年完成的项目类似项目情况表|类似项目'
  291. '''删除前面标签 命中业绩规则;当前标签为表格且公布业绩相关信息的去除'''
  292. for tag in soup.find_all('table'):
  293. pre_text = ""
  294. if tag.findPreviousSibling() != None:
  295. pre_text = tag.findPreviousSibling().text.strip()
  296. if pre_text == "" and tag.findPreviousSibling().findPreviousSibling() != None: # 修复表格前一标签没内容,再前一个才有内容情况
  297. pre_text = tag.findPreviousSibling().findPreviousSibling().text.strip()
  298. tr_text = tag.find('tr').text.strip() if tag.find('tr') != None else ""
  299. if len(pre_text) < 20 and re.search('^[((]?[\d一二三四五六七八九十]+[)、)]近年完成的(项目)?类似项目情况表|类似项目历史成交信息|类似项目的?采购预算', pre_text): # 删除 650469910 评标公告 历史业绩 547305313
  300. del_tag = tag.extract()
  301. # print('删除表格业绩内容', del_tag.text)
  302. # print(re.search(p1, pre_text),pre_text, len(pre_text), re.findall('序号|中标候选人名称|项目名称|工程名称|合同金额|建设单位|业主', tr_text))
  303. elif re.search(p1, pre_text) and len(pre_text) < 20 and tag.find('tr') != None and len(tr_text) < 100:
  304. _count = 0
  305. for td in tag.find('tr').find_all('td'):
  306. td_text = td.text.strip()
  307. if len(td_text) > 25:
  308. break
  309. if len(td_text) < 25 and re.search('中标候选人|第[一二三四五1-5]候选人|(项目|业绩|工程)名称|\w{,10}业绩$|合同(金额|价格)|建设单位|采购单位|业主|甲方|发包人', td_text):
  310. _count += 1
  311. if _count >= 2:
  312. pre_tag = tag.findPreviousSibling().extract()
  313. del_tag = tag.extract()
  314. # print('删除表格业绩内容', pre_tag.text + del_tag.text)
  315. break
  316. elif re.search('业绩名称', tr_text) and re.search('建设单位|采购单位|业主', tr_text) and len(tr_text) < 100:
  317. del_tag = tag.extract()
  318. # print('删除表格业绩内容', del_tag.text)
  319. elif re.search('^项目管理机构主要人员$', tr_text): # 修复598985057 去除表格业绩
  320. del_tag = tag.extract()
  321. # print('删除表格业绩内容', del_tag.text)
  322. del_trs = []
  323. '''删除表格某些行公布的业绩信息'''
  324. for tag in soup.find_all('table'):
  325. text = tag.text
  326. if re.search('业绩', text) == None:
  327. continue
  328. # for tr in tag.find_all('tr'):
  329. trs = tag.find_all('tr')
  330. i = 0
  331. while i < len(trs):
  332. tr = trs[i]
  333. if len(tr.find_all('td')) == 2 and tr.td != None and tr.td.findNextSibling() != None:
  334. td1_text = tr.td.text
  335. td2_text = tr.td.findNextSibling().text
  336. if re.search('业绩', td1_text) != None and len(td1_text) < 10 and len(re.findall('(\d、|(\d))?[-\w()、]+(工程|项目|勘察|设计|施工|监理|总承包|采购|更新)', td2_text)) >= 2:
  337. # del_tag = tr.extract()
  338. # print('删除表格业绩内容', del_tag.text)
  339. del_trs.append(tr)
  340. elif tr.td != None and re.search('^业绩|业绩$', tr.td.text.strip()) and len(tr.td.text.strip()) < 25:
  341. rows = tr.td.attrs.get('rowspan', '')
  342. cols = tr.td.attrs.get('colspan', '')
  343. if rows.isdigit() and int(rows) > 2:
  344. for j in range(int(rows)):
  345. if i + j < len(trs):
  346. del_trs.append(trs[i + j])
  347. i += j
  348. elif cols.isdigit() and int(cols) > 3 and len(tr.find_all('td')) == 1 and i + 2 < len(trs):
  349. next_tr_cols = 0
  350. td_num = 0
  351. for td in trs[i + 1].find_all('td'):
  352. td_num += 1
  353. if td.attrs.get('colspan', '').isdigit():
  354. next_tr_cols += int(td.attrs.get('colspan', ''))
  355. if next_tr_cols == int(cols):
  356. del_trs.append(tr)
  357. for j in range(1, len(trs) - i):
  358. if len(trs[i + j].find_all('td')) == 1:
  359. break
  360. elif len(trs[i + j].find_all('td')) >= td_num - 1:
  361. del_trs.append(trs[i + j])
  362. else:
  363. break
  364. i += j
  365. i += 1
  366. for tr in del_trs:
  367. del_tag = tr.extract()
  368. # print('删除表格业绩内容', del_tag.text)
  369. def recall(y_true, y_pred):
  370. '''
  371. 计算召回率
  372. @Argus:
  373. y_true: 正确的标签
  374. y_pred: 模型预测的标签
  375. @Return
  376. 召回率
  377. '''
  378. c1 = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
  379. c3 = K.sum(K.round(K.clip(y_true, 0, 1)))
  380. if c3 == 0:
  381. return 0
  382. recall = c1 / c3
  383. return recall
  384. def f1_score(y_true, y_pred):
  385. '''
  386. 计算F1
  387. @Argus:
  388. y_true: 正确的标签
  389. y_pred: 模型预测的标签
  390. @Return
  391. F1值
  392. '''
  393. c1 = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
  394. c2 = K.sum(K.round(K.clip(y_pred, 0, 1)))
  395. c3 = K.sum(K.round(K.clip(y_true, 0, 1)))
  396. precision = c1 / c2
  397. if c3 == 0:
  398. recall = 0
  399. else:
  400. recall = c1 / c3
  401. f1_score = 2 * (precision * recall) / (precision + recall)
  402. return f1_score
  403. def precision(y_true, y_pred):
  404. '''
  405. 计算精确率
  406. @Argus:
  407. y_true: 正确的标签
  408. y_pred: 模型预测的标签
  409. @Return
  410. 精确率
  411. '''
  412. c1 = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
  413. c2 = K.sum(K.round(K.clip(y_pred, 0, 1)))
  414. precision = c1 / c2
  415. return precision
  416. def is_adjacent_or_have_adjacent_parents(element1, element2):
  417. """
  418. 判断两个标签是否相邻,或它们的父级标签是否相邻
  419. """
  420. # 获取两个元素的父元素
  421. parent1 = element1.getparent()
  422. parent2 = element2.getparent()
  423. # 如果两个元素有相同的父元素,检查它们是否相邻
  424. if parent1 is parent2:
  425. siblings = parent1.xpath('./*')
  426. index1 = siblings.index(element1)
  427. index2 = siblings.index(element2)
  428. # 检查索引差值是否为1(相邻)
  429. return abs(index1 - index2) == 1
  430. elif len(parent1.xpath('./*')) == len(parent2.xpath('./*')) == 1:
  431. # 如果父元素不同,检查父元素是否相邻(有相同的父元素)
  432. grandparent1 = parent1.getparent() if parent1 is not None else None
  433. grandparent2 = parent2.getparent() if parent2 is not None else None
  434. if grandparent1 is grandparent2 and grandparent1 is not None:
  435. # 检查父元素是否相邻
  436. # siblings = list(grandparent1)
  437. siblings = grandparent1.xpath('./*')
  438. index1 = siblings.index(parent1)
  439. index2 = siblings.index(parent2)
  440. # 检查索引差值是否为1(相邻)
  441. return abs(index1 - index2) == 1
  442. # 如果以上都不满足,返回False
  443. return False
  444. def merge_single_row_tables(html):
  445. tree = etree.HTML(html)
  446. if tree is None:
  447. return '<div>' + html + '</div>'
  448. # 步骤1:用XPath定位所有只有一行的表格
  449. # XPath逻辑:table下直接子节点tr的数量等于1
  450. single_row_tables = tree.xpath('//table[count(.//tr) = 1]')
  451. # 步骤2:筛选相邻的单行列表格并合并
  452. i = 0
  453. shold_merge_tables = []
  454. while i < len(single_row_tables) - 1:
  455. current_table = single_row_tables[i]
  456. next_table = single_row_tables[i + 1]
  457. if len(current_table.xpath('.//tr')) == len(
  458. next_table.xpath('.//tr')) == 1 and is_adjacent_or_have_adjacent_parents(current_table, next_table):
  459. shold_merge_tables.append(current_table)
  460. shold_merge_tables.append(next_table)
  461. j = i + 1
  462. while j < len(single_row_tables) - 1:
  463. current_table = single_row_tables[j]
  464. next_table = single_row_tables[j + 1]
  465. if len(current_table.xpath('.//tr')) == len(
  466. next_table.xpath('.//tr')) == 1 and is_adjacent_or_have_adjacent_parents(current_table,
  467. next_table):
  468. shold_merge_tables.append(next_table)
  469. j = j + 1
  470. else:
  471. break
  472. first_table = shold_merge_tables[0]
  473. n = 0
  474. for next_table in shold_merge_tables[1:]:
  475. n += 1
  476. first_table.extend(next_table.xpath('./*'))
  477. parent = next_table.getparent()
  478. parent.remove(next_table)
  479. shold_merge_tables = []
  480. i = j
  481. else:
  482. i += 1
  483. merged_html = etree.tostring(tree, encoding='unicode', pretty_print=True)
  484. return merged_html
  485. if __name__ == "__main__":
  486. # print(fool_char_to_id[">"])
  487. print(getUnifyMoney('伍仟贰佰零壹拾伍万零捌佰壹拾元陆角伍分'))
  488. # model = getModel_w2v()
  489. # vocab,matrix = getVocabAndMatrix(model, Embedding_size=128)
  490. # save([vocab,matrix],"vocabMatrix_words.pk")