|
| 1 | +# -*- coding=utf-8 -*- |
| 2 | +# library: jionlp |
| 3 | +# author: dongrixinyu |
| 4 | +# license: Apache License 2.0 |
| 5 | + |
| 6 | +# github: https://github.com/dongrixinyu/JioNLP |
| 7 | +# description: Preprocessing tool for Chinese NLP |
| 8 | + |
| 9 | + |
| 10 | +import re |
| 11 | + |
| 12 | +from jionlp.util.funcs import bracket, bracket_absence, absence |
| 13 | +from jionlp.rule.rule_pattern import MONEY_PREFIX_STRING, \ |
| 14 | + MONEY_SUFFIX_STRING, MONEY_NUM_MIDDLE_STRING, MONEY_NUM_STRING, \ |
| 15 | + MONEY_KUAI_MAO_JIAO_FEN_STRING, MONEY_PREFIX_CASE_STRING, MONEY_SUFFIX_CASE_STRING |
| 16 | +from jionlp.gadget.money_parser import MoneyParser |
| 17 | + |
| 18 | + |
| 19 | +class MoneyExtractor(object): |
| 20 | + """ 货币金额抽取器。不依赖模型,将文本中的货币金额进行抽取,并对其做金额解析。 |
| 21 | +
|
| 22 | + Args: |
| 23 | + text(str): 输入待抽取货币金额的文本 |
| 24 | + with_parsing(bool): 指示返回结果是否包含解析信息,默认为 True |
| 25 | + ret_all(bool): 某些货币金额表达,在大多数情况下并非表达货币金额,如 “几分” 之于 “他有几分不友善”,默认按绝大概率处理, |
| 26 | + 即不返回此类伪货币金额表达,该参数默认为 False;若希望返回所有抽取到的货币金额表达,须将该参数置 True。 |
| 27 | +
|
| 28 | + Returns: |
| 29 | + list(dict): 包含货币金额的列表,其中包括 text、type、offset 三个字段,和工具包中 NER 标准处理格式一致。 |
| 30 | +
|
| 31 | + Example: |
| 32 | + >>> import jionlp as jio |
| 33 | + >>> text = '海航亏损7000万港元出售香港公寓。12月12日,据《香港经济日报》报道,' \ |
| 34 | + '海航集团将持有的部分位于香港铜锣湾Yoo Residence大楼中的物业以2.6亿港元的价格出售' |
| 35 | + >>> res = jio.ner.extract_money(text, with_parsing=False) |
| 36 | + >>> print(res) |
| 37 | +
|
| 38 | + """ |
| 39 | + def __init__(self): |
| 40 | + self.parse_money = None |
| 41 | + |
| 42 | + def _prepare(self): |
| 43 | + self.parse_money = MoneyParser() |
| 44 | + self.money_string_pattern = re.compile( |
| 45 | + ''.join([absence(MONEY_PREFIX_STRING), |
| 46 | + absence(MONEY_PREFIX_CASE_STRING), '(', MONEY_NUM_STRING, '+', |
| 47 | + bracket(MONEY_NUM_MIDDLE_STRING + MONEY_NUM_STRING + '+'), '*', |
| 48 | + MONEY_SUFFIX_CASE_STRING, ')+', |
| 49 | + bracket_absence(MONEY_NUM_STRING), |
| 50 | + absence(MONEY_SUFFIX_STRING)])) |
| 51 | + |
| 52 | + # 此类表达虽然可按货币金额解析,但是文本中很大概率并非表示货币金额,故以大概率进行排除, |
| 53 | + # 并设参数 ret_all,即返回所有进行控制,默认为 False,即根据词典进行删除 |
| 54 | + # 删除性正则 |
| 55 | + # - 单纯包含 分、角、块,而无其它格式货币的 |
| 56 | + # - 特殊词汇如 “多元” 等 |
| 57 | + self.money_kuai_map_jiao_fen_pattern = re.compile(MONEY_KUAI_MAO_JIAO_FEN_STRING) |
| 58 | + self.non_money_string_list = ['多元'] |
| 59 | + |
| 60 | + def __call__(self, text, with_parsing=True, ret_all=False): |
| 61 | + if self.parse_money is None: |
| 62 | + self._prepare() |
| 63 | + |
| 64 | + candidates_list = self.extract_money_candidates(text) |
| 65 | + |
| 66 | + money_entity_list = list() |
| 67 | + for candidate in candidates_list: |
| 68 | + offset = [0, 0] |
| 69 | + bias = 0 |
| 70 | + while candidate['offset'][0] + offset[1] < candidate['offset'][1]: |
| 71 | + # 此循环意在找出同一个 candidate 中包含的多个 money_entity |
| 72 | + |
| 73 | + true_string, result, offset = self.grid_search( |
| 74 | + candidate['money_candidate'][bias:]) |
| 75 | + |
| 76 | + if true_string is not None: |
| 77 | + |
| 78 | + # rule 1: 判断字符串是否为大概率非货币金额语义 |
| 79 | + if (true_string in self.non_money_string_list) and (not ret_all): |
| 80 | + bias += offset[1] |
| 81 | + continue |
| 82 | + |
| 83 | + if with_parsing: |
| 84 | + money_entity_list.append( |
| 85 | + {'text': true_string, |
| 86 | + 'offset': [candidate['offset'][0] + bias + offset[0], |
| 87 | + candidate['offset'][0] + bias + offset[1]], |
| 88 | + 'type': 'money', |
| 89 | + 'detail': result}) |
| 90 | + else: |
| 91 | + money_entity_list.append( |
| 92 | + {'text': true_string, |
| 93 | + 'offset': [candidate['offset'][0] + bias + offset[0], |
| 94 | + candidate['offset'][0] + bias + offset[1]], |
| 95 | + 'type': 'money'}) |
| 96 | + bias += offset[1] |
| 97 | + else: |
| 98 | + break |
| 99 | + |
| 100 | + return money_entity_list |
| 101 | + |
| 102 | + def grid_search(self, money_candidate): |
| 103 | + """ 全面搜索候选货币金额字符串,从长至短,较优 """ |
| 104 | + length = len(money_candidate) |
| 105 | + for i in range(length): # 控制总长,若想控制单字符的串也被返回考察,此时改为 length + 1 |
| 106 | + for j in range(i): # 控制偏移 |
| 107 | + try: |
| 108 | + offset = [j, length - i + j + 1] |
| 109 | + sub_string = money_candidate[j: offset[1]] |
| 110 | + result = self.parse_money(sub_string) |
| 111 | + |
| 112 | + return sub_string, result, offset |
| 113 | + except (ValueError, Exception): |
| 114 | + continue |
| 115 | + |
| 116 | + return None, None, None |
| 117 | + |
| 118 | + def _grid_search_2(self, money_candidate): |
| 119 | + """ 全面搜索候选货币金额字符串,从前至后,从长至短 """ |
| 120 | + print(money_candidate) |
| 121 | + length = len(money_candidate) |
| 122 | + for i in range(length - 1): # 控制起始点 |
| 123 | + for j in range(length, i, -1): # 控制终止点 |
| 124 | + try: |
| 125 | + offset = [i, j] |
| 126 | + sub_string = money_candidate[i: j] |
| 127 | + print(sub_string) |
| 128 | + # 处理假阳性。检查子串,对某些产生歧义的内容进行过滤。 |
| 129 | + # 原因在于,parse_money 会对某些不符合要求的字符串做正确解析. |
| 130 | + if not MoneyExtractor._filter(sub_string): |
| 131 | + continue |
| 132 | + |
| 133 | + result = self.parse_money(sub_string, strict=True) |
| 134 | + |
| 135 | + return sub_string, result, offset |
| 136 | + except (ValueError, Exception): |
| 137 | + continue |
| 138 | + |
| 139 | + return None, None, None |
| 140 | + |
| 141 | + def extract_money_candidates(self, text): |
| 142 | + """ 获取所有的候选货币金额字符串,其中包含了货币金额 """ |
| 143 | + idx_count = 0 |
| 144 | + text_length = len(text) |
| 145 | + money_candidates_list = list() |
| 146 | + while idx_count < text_length: |
| 147 | + matched_res = self.money_string_pattern.search(text[idx_count:]) |
| 148 | + |
| 149 | + if matched_res is not None: |
| 150 | + tmp_str = matched_res.group() |
| 151 | + if len(tmp_str) > 1: |
| 152 | + if len(''.join(self.money_kuai_map_jiao_fen_pattern.findall(tmp_str))) == 1 and ( |
| 153 | + '元' not in tmp_str and '钱' not in tmp_str): |
| 154 | + # 仅有一个 `分毛角块` 字符且无 `元钱` 字符 |
| 155 | + idx_count += matched_res.span()[1] |
| 156 | + continue |
| 157 | + |
| 158 | + money_candidates_list.append( |
| 159 | + {'money_candidate': matched_res.group(), |
| 160 | + 'offset': [idx_count + matched_res.span()[0], |
| 161 | + idx_count + matched_res.span()[1]], |
| 162 | + 'context': text[max(0, idx_count - 5 + matched_res.span()[0]): |
| 163 | + min(text_length, idx_count + 5 + matched_res.span()[1])]} |
| 164 | + ) |
| 165 | + idx_count += matched_res.span()[1] |
| 166 | + else: |
| 167 | + break |
| 168 | + |
| 169 | + return money_candidates_list |
| 170 | + |
| 171 | + |
| 172 | +if __name__ == '__main__': |
| 173 | + text = '''海航亏损7000万港元出售香港公寓。12月12日,据《香港经济日报》报道, |
| 174 | + 海航集团将持有的部分位于香港铜锣湾Yoo Residence大楼中的物业以2.6亿港元的价格出售,相对于去年入手时3.3亿港元的价格来看, |
| 175 | + 海航此次出售该物业以公司股权转让的模式转售,亏损了7000多万港元。该物业包括一个顶层复式豪华公寓、1个分层物业及5个车位。 |
| 176 | + 报道称,两个月前,海航在市场上为该部分物业寻找买家,一度报价达到几千万美元。此外,海航在数月前将去年同时买下的一个地下连1楼的商铺 |
| 177 | + 以8650万港元的价格出售,买家为香港一家名为荣企的公司,较去年近1.2亿港元入手的价格亏损了约3350万港元。 |
| 178 | + 以此来看,海航投资Yoo Residence在一年内亏损逾1亿港元。今年以来,海航在香港连续出售其持有的地产类资产。 |
| 179 | + 2月份,海航集团把香港启德区6565号地块和6562号地块以159.59亿港元卖给了香港恒基兆业地产(00012.HK),股价为二十三块四毛钱。 |
| 180 | + 3月份,海航又把位于九龙启德第1L区1号地盘新九龙内地段第6564号以63.59亿港元的价格卖给了会德丰(00020.HK)。 |
| 181 | + 已在5个月前为其融到了50.47亿港元。除了出售地块之外,海航还卖掉了在香港金钟的一处办公室。3月21日,据香港当地媒体《明报》报道, |
| 182 | + 海航已于今日出售位于香港金钟力宝中心的一处办公室,成交价为4000多万港元,折合单价为28000港元/平方英尺(折合243300元/平方米), |
| 183 | + 较该物业的市场价值38000港元/平方英尺低了近两成。截至目前,海航在香港出售地产类物业已套现至少227亿港元。''' |
| 184 | + |
| 185 | + extract_money = MoneyExtractor() |
| 186 | + res = extract_money(text, with_parsing=False) |
| 187 | + print(res) |
| 188 | + |
0 commit comments