Maxcomputer表判定联系方式是否是正常的联系方式的两个方式

本文涉及的产品
云原生大数据计算服务MaxCompute,500CU*H 100GB 3个月
云原生大数据计算服务 MaxCompute,5000CU*H 100GB 3个月
简介: 在数据预处理的过程中,手机号作为联系方式的一种重要形式,经常需要进行格式验证和去重等操作。然而,在实际应用中,我们常常遇到手机号格式不统一、线上业务还好点,但是有部分线下的业务手机号是手工录入的数据等问题。这些问题导致正则清洗的工作量很大,而且容易出现错误,影响数据的质量。为了解决这些问题,本文提出了一些可能的优化方案,希望能够为数据预处理工作提供一些参考。

最简单的方法

如果您想使用开源的NLP包来解决手机号识别问题,可以考虑使用jieba或者thulac等库。这些库都提供了中文分词和关键词提取等功能,可以方便地对文本进行处理。

对于Maxcomputer安装依赖的问题,您可以尝试最简单的方法,即在git上找到一个开源的NLP包,并使用pip安装到Maxcomputer中。接下来,您可以使用Python脚本或Maxcomputer Studio中的pyudf功能来实现手机号识别。

我最初使用的是JioNLP,但不确定它与其他项目相比有何优劣。我只是搜索了一下,发现它的stars很高,因此就使用了它。

JioNLP包含许多小模块,其中有两个模块专门用于手机号识别。第一个模块名为phone_location,如果无法识别数据,则会返回None;否则,它将返回一个字典。

需要留意的是手机号和电话号是有区别的,电话号是不会返回operator的,实际使用的时候要留意,第二种则是extract_phone_number,传出的会是一个dict组成的数组,它会提取出文本中正确的手机号,如果你的一个文本中包含多个手机号,就需要留意是否考虑变换表结构。

简单的办法好处就是封装的好,交给任何一个稍微会点python的,for循环也能够实现一个还算可以的udf,不过要安装一个开源的包,你需要先介绍这个包的作用,来自哪里,然后写个文档,一套流程下来,如果包冲突,最后再不了了之,还不如直接扒代码,毕竟复制粘贴可是本能,这里就不展示使用源码编写的过程了。

jio.extract_phone_number

extract_phone_number是Extractor类中的一个方法,这个类是一个规则抽取器,其中定义了各种不同的需求抽取方法。针对手机号的抽取是常规的正则表达式抽取,使用了一个手机号的正则表达式、一个电话号码的正则表达式和一个基础函数。

首先,我们需要了解extract_base函数的使用方法。它接受三个参数:正则表达式对象、要抽取的文本和是否返回偏移量的开启选项。如果不指定开启选项,默认情况下不会返回偏移量,只会返回抽取出来的文本。

在数据进来后,extract_base函数会使用正则表达式的finditer方法返回一个所有匹配上的子串的迭代器对象。然后,我们可以使用列表推导式和条件表达式来处理每一个子串,并使用group方法获得第一个捕获组的值。偏移量则是通过span方法获得。

了解了基类的使用方法,我们可以轻松地实现extract_phone_number函数。首先,我们需要创建两个正则表达式对象,分别用于手机号和电话号码的匹配。接下来,在需要判定的文本前后都加上#号,这一段我并不是很清楚,只是怀疑和正在中采用的负向前瞻断言和正向前瞻断言有关,可能是为了优化空格等特殊字符。

然后是上面代码对应的两段正则:

如果没有接触过正则表达式,可以使用市面上的生成式AI来理解。基础正则表达式是一种模式匹配规则,用于在文本中查找特定的模式。捕获组是指正则表达式中用括号括起来的子表达式,它们可以被提取出来作为结果的一部分。前瞻断言和负前瞻断言是两种特殊的正则表达式,用于指定匹配的顺序和位置。

在使用过程中,需要注意考虑到一行文本可能包含多个联系方式,因此最终返回的结果是一个数组。

jio.phone_location

相比于extract_phone_number,phone_location的处理过程显然更为复杂。我们可以从数据进入的方式来解释数据的处理过程,而不是从代码的上下文结构来描述。

当数据进来时,在phone_location类的init方法中,会创建一个名为cell_phone_location_trie的cell_phone_location_trie对象,并将其初始化为none。第一次调用该方法时,会首先检查其值是否为none。如果为none,则开始加载词典。这个词典是一个名为phone_location的txt文件,其中存储了很多国内联系方式的开头以及后面的四位数字。

在phone_location文件中,联系方式的开头和后面的四位数字之间用换行符分隔。不同的中间位数则使用逗号分隔。实际上,-符号表示一个区间范围。代码会根据-将其转化为一个数组。为了避免出现0011在循环中被识别为11,我们使用了格式化字符串将其转化为四位数字。如果不足四位,则在前面补0。最后,将手机号前缀和所有手机号整理成一个新的集合并返回。

当然,我们还使用了startwith方法来判断是否需要识别该行。例如,开头的山东这一行会被忽略。开头第一行存储的是直观的省、市、区号和电话号码段。接下来,数据会根据这个被解析后的手机号生成三个词典类型。第一个phone_location_dict中的数据格式大概是这样的:{1340054****:“山东 济南”},而zip_code_location_dict则是{0531:"山东 济南"},area_code_location_dict的是{250000:"山东 济南"}。

当词典加载完成后,cell_phone_location的数据会被循环写入cell_phone_location_trie中。首先,我们将cell_phone_location_trie转化为一个树对象TrieTree,使用树的add方法将数据分为手机号和对应的地区loc传入。在添加数据时,我们会先清空两侧空白字符,然后判断其不是由特殊的空白字符组成。

接下来,我们需要使用init中定义的一个dict类型,以及对输入的手机号求长度并将其字母全部转为小写。然后,我们将手机号拆分成一个个字符,并进行一些判断。例如,当我们第一次输入"123"时,tree-init中的dict会是{1:{2:{3:{}}}}。而当我们再次输入"124"时,数据会变成{1:{2:{3:{},4:{}}}}。有些人可能会因为else中最下面的一个tree而感到困惑。实际上,这个tree是一个嵌套的新字典。如果自己运行代码并观察一下,可以发现它与上面的tree[char]不是同一个东西。

地区的loc会被作为cell_phone_location_trie['type']写入,之后会是和extract_phone_number一样的定义三个正则匹配对象,具体正则如下:

加载完电话号词典后,我们还会加载一个运营商词典。这个相对简单,只是为了通过手机号的前三位判断对应的通信运营商。加载方式依旧是采用树结构来add。

首先,我们使用正则表达式来判断手机号是否有符合的子串。如果有,就截取前七位。然后,我们使用相同的逻辑,用字符来依次匹配字段,返回手机号代表的省市。由于省市之间在txt中使用的空格拆分,所以这里使用空格作为split的拆分键。

对于电话号的处理逻辑思路大致相同,只不过在进行电话号的匹配规则上还有一个针对区号的规则。如果没有对应的区号,则会返回none。如果对电话号的限制没有这么高,可以调整此处规则,比如剔除对区号的判定。同时,电话号的区域判定是通过area_code_location_dict来判断,不需要使用树结构。此外,源代码中有一个单独的电话号方法和手机号匹配方法,实际不使用的时候可以剔除,减少代码的冗余。

实际使用

实际使用时,我采用了两种方式。一种是UDF,另一种是维表。UDF是为了单独通过类型确定联系方式是否是可使用的联系方式。维表则是为了后续考虑而编写,前者没有解析手机号,后者则解析出正确的手机号。

由于数据的问题,可能会有部分识别不精准的情况。源代码提供的txt词典是三年前的,实际上有十万分之一,甚至更低的概览会出现手机号不在词典的情况。不过我有一个思路,对于这些少一点的手机号,可以使用request直接百度,然后解析html看其是否是手机号。但实际开发时间有限,而且考虑到maxcomputer的白名单问题,我没有采用这种方法。

同时,识别不精确还包括可能正确的数据。比如数据为“dsfjasd13934720013fasdf”这样的数据,里面包含的手机号确实正确,但说其是手机号吗?这一点让人很头疼。

实际代码

fromodpsimportODPSfromodps.dfimportDataFrameimporthashlibfromdatetimeimportdatetime, timedeltaimportreimportjsonimportosimportsysimporttime##@resource_reference{"phone_location.txt"}##@resource_reference{"telecom_operator.txt"}sys.path.append(os.path.dirname('phone_location.txt'))
sys.path.append(os.path.dirname('telecom_operator.txt'))
# 插入表insert_table='wwwx_cdm_dev.dim_wwwx_ppd_phone_detail_di'no_clean_phone='old_phone'# 查询表名:列名table_name_col_name= {"wwwx_cdm.dwd_wwwx_ppd_order_master_df":"user_phone","wwwx_cdm.dim_wwwx_ppd_cw_user_df":"phone"}
# 手机号码CELL_PHONE_PATTERN=r'(?<=[^\d])(((\+86)?([- ])?)?((1[3-9][0-9]))([- ])?\d{4}([- ])?\d{4})(?=[^\d])'LANDLINE_PHONE_PATTERN=r'(?<=[^\d])(([\((])?0\d{2,3}[\)) —-]{1,2}\d{7,8}|\d{3,4}[ -]\d{3,4}[ -]\d{4})(?=[^\d])'# 该规则用于抽取与判定手机号的归属地,即抽取前三位、中间4位CELL_PHONE_CHECK_PATTERN=r'((1[3-9][0-9]))([- ])?\d{4}([- ])?\d{4}'LANDLINE_PHONE_CHECK_PATTERN=r'(([\((])?0\d{2,3}[\)) —-]{1,2}\d{7,8}|\d{3,4}[ -]\d{3,4}[ -]\d{4})'# 用分隔符,找到靠前的区号LANDLINE_PHONE_AREA_CODE_PATTERN=r'(0\d{2,3})[\)) —-]'# 拿到全部地区的手机号GRAND_DIR_PATH='phone_location.txt'# 拿到全国手机号开头映射TELE_DIR_PATH='telecom_operator.txt'defextract_base(pattern, text, with_offset=False):
""" 正则抽取器的基础函数    Args:        pattern(re.compile): 正则表达式对象        text(str): 字符串文本        with_offset(bool): 是否携带 offset (抽取内容字段在文本中的位置信息)    Returns:        list: 返回结果    """ifwith_offset:
results= [{'text': item.group(1),
'offset': (item.span()[0] -1, item.span()[1] -1)}
foriteminpattern.finditer(text)]
else:
results= [item.group(1) foriteminpattern.finditer(text)]
returnresultsdefextract_phone_number(text, detail=False):
"""从文本中抽取出电话号码    Args:        text(str): 字符串文本        detail(bool): 是否携带 offset (电话号码在文本中的位置信息)    Returns:        list: 电话号码列表    """cell_phone_pattern=re.compile(CELL_PHONE_PATTERN)
landline_phone_pattern=re.compile(LANDLINE_PHONE_PATTERN)
text=''.join(['#', text, '#'])
cell_results=extract_base(
cell_phone_pattern, text, with_offset=detail)
landline_results=extract_base(
landline_phone_pattern, text, with_offset=detail)
ifnotdetail:
returncell_results+landline_resultselse:
detail_results=list()
foritemincell_results:
item.update({'type': 'cell_phone'})
detail_results.append(item)
foriteminlandline_results:
item.update({'type': 'landline_phone'})
detail_results.append(item)
returndetail_resultsdefread_file_by_line(file_path, line_num=None,
skip_empty_line=True, strip=True,
auto_loads_json=True):
""" 读取一个文件的前 N 行,按列表返回,    文件中按行组织,要求 utf-8 格式编码的自然语言文本。    若每行元素为 json 格式可自动加载。    Args:        file_path(str): 文件路径        line_num(int): 读取文件中的行数,若不指定则全部按行读出        skip_empty_line(boolean): 是否跳过空行        strip: 将每一行的内容字符串做 strip() 操作        auto_loads_json(bool): 是否自动将每行使用 json 加载,默认是    Returns:        list: line_num 行的内容列表    Examples:        >>> file_path = '/path/to/stopwords.txt'        >>> print(jio.read_file_by_line(file_path, line_num=3))        # ['在', '然后', '还有']    """content_list=list()
count=0withopen(file_path, 'r', encoding='utf-8') asf:
line=f.readline()
whileTrue:
ifline=='':  # 整行全空,说明到文件底breakifline_numisnotNone:
ifcount>=line_num:
breakifline.strip() =='':
ifskip_empty_line:
count+=1line=f.readline()
else:
try:
ifauto_loads_json:
cur_obj=json.loads(line.strip())
content_list.append(cur_obj)
else:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
except:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
count+=1line=f.readline()
continueelse:
try:
ifauto_loads_json:
cur_obj=json.loads(line.strip())
content_list.append(cur_obj)
else:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
except:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
count+=1line=f.readline()
continuereturncontent_listdefphone_location_loader():
""" 加载电话号码地址与运营商解析词典 """content=read_file_by_line(os.path.join(
GRAND_DIR_PATH),
strip=False, auto_loads_json=False)
defreturn_all_num(line):
""" 返回所有的手机号码中间四位字符串 """front, info=line.strip().split('\t')
num_string_list=info.split(',')
result_list= []
fornum_stringinnum_string_list:
if'-'innum_string:
start_num, end_num=num_string.split('-')
foriinrange(int(start_num), int(end_num) +1):
result_list.append('{:0>4d}'.format(i))
else:
result_list.append(num_string)
result_list= [front+resforresinresult_list]
returnresult_listphone_location_dict= {}
cur_location=''zip_code_location_dict= {}
area_code_location_dict= {}
forlineincontent:
ifline.startswith('\t'):
res=return_all_num(line)
foriinres:
phone_location_dict.update({i: cur_location})
else:
cur_location, area_code, zip_code=line.strip().split('\t')
zip_code_location_dict.update({zip_code: cur_location})
area_code_location_dict.update({area_code: cur_location})
returnphone_location_dict, zip_code_location_dict, area_code_location_dictdeftelecom_operator_loader():
"""     加载通信运营商手机号码的匹配词典    """telecom_operator=read_file_by_line(os.path.join(TELE_DIR_PATH))
telecom_operator_dict=dict()
forlineintelecom_operator:
num, operator=line.strip().split(' ')
telecom_operator_dict.update({num: operator})
returntelecom_operator_dictdefget_one_phone(oldphone, phone):
"维表唯一键"md5_obj=hashlib.md5()
md5_obj.update((oldphone+phone).encode('utf-8'))
returnmd5_obj.hexdigest()
defget_data_sql(table_name,col_name,ds):
"拼接成没有清洗过的字符串"drop_sql="drop table if exists wwwx_cdm_dev.phone_tmp_di;"sql_text="create table wwwx_cdm_dev.phone_tmp_di as select distinct {} as tmp_phone from {} where ds={} and {} not in (select distinct {} from {} where ds<={} and {} is not null);".format(col_name,table_name,ds,col_name,no_clean_phone,insert_table,ds,no_clean_phone)
returndrop_sql,sql_text,"wwwx_cdm_dev.phone_tmp_di"defget_table_phone_n_data(table_name,ds,table_name_col_name):
"获得需要运行的数据"col_name=table_name_col_name[table_name]
drop_sql,create_sql,new_table_name=get_data_sql(table_name,col_name,ds)
try:
# 过滤掉的数据no_clean_master=DataFrame(odps.get_table(new_table_name))
clean_phone=no_clean_master[no_clean_master.tmp_phone.notnull()][['tmp_phone']].distinct()
result=clean_phone.execute()
forphont_iteminresult:
foritem_valinlist(phont_item):
yielditem_valexceptException:
print("数据转化异常")
defregex_phone(old_phone,phone_location_x):
"""    返回解析后的手机号的具体情况:    唯一键,原始手机号,正确手机号,省,市,手机号|电话号格式,服务商,是否是可识别手机号,是否清洗,是否是报错写入    如果无法解析,只有原始手机号有值,且是否可识别手机号为False    """defget_phone_data(old_phone, text_phone, phone_location_x):
"用解析出来的手机号,再解析手机号详情"text_phone=text_phone.replace(" ","")
phone_location=phone_location_x(text_phone)
ifphone_location['type'] =='cell_phone':
return [get_one_phone(old_phone, text_phone), old_phone, text_phone, phone_location['province'], phone_location['city'], phone_location['type'], phone_location['operator'], True, True, False]
elifphone_location['type'] =='unknown':
return [old_phone, old_phone, None, None, None, None, None, False, False, False]
else:
return [get_one_phone(old_phone, text_phone), old_phone, text_phone, phone_location['province'], phone_location['city'], phone_location['type'], None, True, True, False]
defget_phone_one_data(old_phone, phone_location_x):
"会有无法先解析手机号,再解析手机号详情,却可以直接解析手机号详情的情况"phone_location=phone_location_x(old_phone.replace(" ",""))
ifphone_location['type'] =='cell_phone':
return [old_phone, old_phone, old_phone, phone_location['province'], phone_location['city'], phone_location['type'], phone_location['operator'], True, False, False]
elifphone_location['type'] =='unknown':
return [old_phone, old_phone, None, None, None, None, None, False, False, False]
else:
return [old_phone, old_phone, old_phone, phone_location['province'], phone_location['city'], phone_location['type'], None, True, False, False]
phone_message=extract_phone_number(old_phone, detail=True)
iflen(phone_message) ==0:
get_main_phone_message=get_phone_one_data(
old_phone, phone_location_x)
iflen(get_main_phone_message) ==0:
yield [old_phone, old_phone, None, None, None, None, None, False, False, False]
else:
yieldget_main_phone_messageelse:
foriteminphone_message:
yieldget_phone_data(old_phone, item['text'], phone_location_x)
classTrieTree(object):
"""    Trie 树的基本方法,用途包括:    - 词典 NER 的前向最大匹配计算    - 繁简体词汇转换的前向最大匹配计算    """def__init__(self):
self.dict_trie=dict()
self.depth=0defadd_node(self, word, typing):
"""向 Trie 树添加节点。        Args:            word(str): 词典中的词汇            typing(str): 词汇类型        Returns: None        """word=word.strip()
ifwordnotin ['', '\t', ' ', '\r']:
tree=self.dict_triedepth=len(word)
word=word.lower()  # 将所有的字母全部转换成小写forcharinword:
ifcharintree:
tree=tree[char]
else:
tree[char] =dict()
tree=tree[char]
ifdepth>self.depth:
self.depth=depthif'type'intreeandtree['type'] !=typing:
print(
'`{}` belongs to both `{}` and `{}`.'.format(
word, tree['type'], typing))
else:
tree['type'] =typingdefbuild_trie_tree(self, dict_list, typing):
""" 创建 trie 树 """forwordindict_list:
self.add_node(word, typing)
defsearch(self, word):
""" 搜索给定 word 字符串中与词典匹配的 entity,        返回值 None 代表字符串中没有要找的实体,        如果返回字符串,则该字符串就是所要找的词汇的类型        """tree=self.dict_trieres=Nonestep=0# step 计数索引位置forcharinword:
ifcharintree:
tree=tree[char]
step+=1if'type'intree:
res= (step, tree['type'])
else:
breakifres:
returnresreturn1, NoneclassPhoneLocation(object):
""" 对于给定的电话号码,返回其归属地、区号、运营商等信息。    该方法与 jio.extract_phone_number 配合使用。    Args:        text(str): 电话号码文本。若输入为 jio.extract_phone_number 返回的结果,效果更佳。            注意,仅输入电话号码文本,如 "86-17309729105"、"13499013052"、"021 60128421" 等,            而 "81203432" 这样的电话号码则没有对应的归属地。            若输入 "343981217799212723" 这样的文本,会造成误识别,须首先从中识别电话号码,再进行            归属地、区号、运营商的识别    Returns:        dict: 该电话号码的类型,归属地,手机运营商    Examples:        # [{'text': '13288568202', 'offset': (5, 16), 'type': 'cell_phone'},           {'text': '(021)32830431', 'offset': (18, 31), 'type': 'landline_phone'}]        # {'number': '(021)32830431', 'province': '上海', 'city': '上海', 'type': 'landline_phone'}        # {'number': '13288568202', 'province': '广东', 'city': '揭阳',           'type': 'cell_phone', 'operator': '中国联通'}    """def__init__(self):
self.cell_phone_location_trie=Nonedef_prepare(self):
""" 加载词典 """cell_phone_location, zip_code_location, area_code_location=phone_location_loader()
self.zip_code_location=zip_code_locationself.area_code_location=area_code_locationself.cell_phone_location_trie=TrieTree()
fornum, locincell_phone_location.items():
self.cell_phone_location_trie.add_node(num, loc)
self.cell_phone_pattern=re.compile(CELL_PHONE_CHECK_PATTERN)
self.landline_phone_pattern=re.compile(LANDLINE_PHONE_CHECK_PATTERN)
self.landline_area_code_pattern=re.compile(
LANDLINE_PHONE_AREA_CODE_PATTERN)
# 运营商词典telecom_operator=telecom_operator_loader()
self.telecom_operator_trie=TrieTree()
fornum, locintelecom_operator.items():
self.telecom_operator_trie.add_node(num, loc)
def__call__(self, text):
""" 输入一段电话号码文本,返回其结果 """ifself.cell_phone_location_trieisNone:
self._prepare()
res=self.cell_phone_pattern.search(text)
ifresisnotNone:  # 匹配至手机号码cell_phone_number=res.group()
first_seven=cell_phone_number[:7]
_, location=self.cell_phone_location_trie.search(first_seven)
province, city=location.split(' ')
# print(province, city)_, operator=self.telecom_operator_trie.search(
cell_phone_number[:4])
return {'number': text, 'province': province, 'city': city,
'type': 'cell_phone', 'operator': operator}
res=self.landline_phone_pattern.search(text)
ifresisnotNone:  # 匹配至固话号码# 抽取固话号码的区号res=self.landline_area_code_pattern.search(text)
ifresisnotNone:
area_code=res.group(1)
province, city=self.area_code_location.get(
area_code, ' ').split(' ')
ifprovince=='':
province, city=None, Nonereturn {'number': text, 'province': province,
'city': city, 'type': 'landline_phone'}
else:
return {'number': text, 'province': None,
'city': None, 'type': 'landline_phone'}
return {'number': text, 'province': None,
'city': None, 'type': 'unknown'}
if__name__=="__main__":
print("开始执行:{}".format(datetime.now()))
ds=args['ds']
master_table=odps.get_table(insert_table)
# 如果分区存在,删除分区master_table.delete_partition('ds={}'.format(ds),if_exists=True)
foritem_tintable_name_col_name:
num=0drop_sql,create_sql,new_table_name=get_data_sql(item_t,table_name_col_name[item_t],ds)
instance1=odps.run_sql(drop_sql)
whilestr(instance1.status) in ['Status.RUNNING','Status.WAITING']:
passinstance2=odps.run_sql(create_sql)
whilestr(instance2.status) in ['Status.RUNNING','Status.WAITING']:
passdatan=get_table_phone_n_data(item_t,ds,table_name_col_name)
phone_location_x=PhoneLocation()
result_phone= []
fordataindatan:
try:
foritem_phoneinregex_phone(data,phone_location_x):
result_phone.append(item_phone)
exceptAttributeError:
result_phone.append(
                    [data, data, None, None, None, None, None, False, False, True])
iflen(result_phone)>134000:
odps.write_table(insert_table, result_phone,
partition='ds={}'.format(ds), create_partition=True)
result_phone.clear()
num=num+134000print("数据量有点大,目前跑了{}条了,当前时间为{}".format(num,datetime.now()))
odps.write_table(insert_table, result_phone,
partition='ds={}'.format(ds), create_partition=True)
print("表{}结束:{}".format(item_t,datetime.now()))
print("结束执行:{}".format(datetime.now()))

在DataWorks中使用PyODPS时,如果要引用资源,可以使用@resource来实现。之后再调用os就可以正常调用了。由于涉及到多表的一个notin操作,使用DataFrame效率没有runsql来的快。因此,我们直接上了SQL语句。由于它是异步执行的,所以我们需要通过判定运行状态来确定是否成功执行。逻辑主要在regex_phone里面。如果有extract_phone_number解析失败,我们会再次调用phone_location,因为后者的准确率是建立在词典和正则表达式上的,比单纯的extract_phone_number要高。

# coding: utf-8fromodps.udfimportannotatefromodps.distcacheimportget_cache_fileimportreimportjson# 手机号码CELL_PHONE_PATTERN=r'(?<=[^\d])(((\+86)?([- ])?)?((1[3-9][0-9]))([- ])?\d{4}([- ])?\d{4})(?=[^\d])'LANDLINE_PHONE_PATTERN=r'(?<=[^\d])(([\((])?0\d{2,3}[\)) —-]{1,2}\d{7,8}|\d{3,4}[ -]\d{3,4}[ -]\d{4})(?=[^\d])'# 该规则用于抽取与判定手机号的归属地,即抽取前三位、中间4位CELL_PHONE_CHECK_PATTERN=r'((1[3-9][0-9]))([- ])?\d{4}([- ])?\d{4}'LANDLINE_PHONE_CHECK_PATTERN=r'(([\((])?0\d{2,3}[\)) —-]{1,2}\d{7,8}|\d{3,4}[ -]\d{3,4}[ -]\d{4})'# 用分隔符,找到靠前的区号LANDLINE_PHONE_AREA_CODE_PATTERN=r'(0\d{2,3})[\)) —-]'defextract_base(pattern, text, with_offset=False):
""" 正则抽取器的基础函数    Args:        pattern(re.compile): 正则表达式对象        text(str): 字符串文本        with_offset(bool): 是否携带 offset (抽取内容字段在文本中的位置信息)    Returns:        list: 返回结果    """ifwith_offset:
results= [{'text': item.group(1),
'offset': (item.span()[0] -1, item.span()[1] -1)}
foriteminpattern.finditer(text)]
else:
results= [item.group(1) foriteminpattern.finditer(text)]
returnresultsdefextract_phone_number(text, detail=False):
"""从文本中抽取出电话号码    Args:        text(str): 字符串文本        detail(bool): 是否携带 offset (电话号码在文本中的位置信息)    Returns:        list: 电话号码列表    """cell_phone_pattern=re.compile(CELL_PHONE_PATTERN)
landline_phone_pattern=re.compile(LANDLINE_PHONE_PATTERN)
text=''.join(['#', text, '#'])
cell_results=extract_base(
cell_phone_pattern, text, with_offset=detail)
landline_results=extract_base(
landline_phone_pattern, text, with_offset=detail)
ifnotdetail:
returncell_results+landline_resultselse:
detail_results=list()
foritemincell_results:
item.update({'type': 'cell_phone'})
detail_results.append(item)
foriteminlandline_results:
item.update({'type': 'landline_phone'})
detail_results.append(item)
returndetail_resultsdefextract_phone_number(text, detail=False):
"""从文本中抽取出电话号码    Args:        text(str): 字符串文本        detail(bool): 是否携带 offset (电话号码在文本中的位置信息)    Returns:        list: 电话号码列表    """cell_phone_pattern=re.compile(CELL_PHONE_PATTERN)
landline_phone_pattern=re.compile(LANDLINE_PHONE_PATTERN)
text=''.join(['#', text, '#'])
cell_results=extract_base(
cell_phone_pattern, text, with_offset=detail)
landline_results=extract_base(
landline_phone_pattern, text, with_offset=detail)
ifnotdetail:
returncell_results+landline_resultselse:
detail_results=list()
foritemincell_results:
item.update({'type': 'cell_phone'})
detail_results.append(item)
foriteminlandline_results:
item.update({'type': 'landline_phone'})
detail_results.append(item)
returndetail_resultsdefread_file_by_line(file_path, line_num=None,
skip_empty_line=True, strip=True,
auto_loads_json=True):
""" 读取一个文件的前 N 行,按列表返回,    文件中按行组织,要求 utf-8 格式编码的自然语言文本。    若每行元素为 json 格式可自动加载。    Args:        file_path(str): 文件路径        line_num(int): 读取文件中的行数,若不指定则全部按行读出        skip_empty_line(boolean): 是否跳过空行        strip: 将每一行的内容字符串做 strip() 操作        auto_loads_json(bool): 是否自动将每行使用 json 加载,默认是    Returns:        list: line_num 行的内容列表        # ['在', '然后', '还有']    """content_list=list()
count=0withfile_pathasf:
line=f.readline()
whileTrue:
ifline=='':  # 整行全空,说明到文件底breakifline_numisnotNone:
ifcount>=line_num:
breakifline.strip() =='':
ifskip_empty_line:
count+=1line=f.readline()
else:
try:
ifauto_loads_json:
cur_obj=json.loads(line.strip())
content_list.append(cur_obj)
else:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
except:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
count+=1line=f.readline()
continueelse:
try:
ifauto_loads_json:
cur_obj=json.loads(line.strip())
content_list.append(cur_obj)
else:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
except:
ifstrip:
content_list.append(line.strip())
else:
content_list.append(line)
count+=1line=f.readline()
continuereturncontent_listdefphone_location_loader(GRAND_DIR_PATH):
""" 加载电话号码地址与运营商解析词典 """content=read_file_by_line(
GRAND_DIR_PATH,
strip=False, auto_loads_json=False)
defreturn_all_num(line):
""" 返回所有的手机号码中间四位字符串 """front, info=line.strip().split('\t')
num_string_list=info.split(',')
result_list= []
fornum_stringinnum_string_list:
if'-'innum_string:
start_num, end_num=num_string.split('-')
foriinrange(int(start_num), int(end_num) +1):
result_list.append('{:0>4d}'.format(i))
else:
result_list.append(num_string)
result_list= [front+resforresinresult_list]
returnresult_listphone_location_dict= {}
cur_location=''zip_code_location_dict= {}
area_code_location_dict= {}
forlineincontent:
ifline.startswith('\t'):
res=return_all_num(line)
foriinres:
phone_location_dict.update({i: cur_location})
else:
cur_location, area_code, zip_code=line.strip().split('\t')
zip_code_location_dict.update({zip_code: cur_location})
area_code_location_dict.update({area_code: cur_location})
returnphone_location_dict, zip_code_location_dict, area_code_location_dictdeftelecom_operator_loader(TELE_DIR_PATH):
"""    加载通信运营商手机号码的匹配词典    """telecom_operator=read_file_by_line(TELE_DIR_PATH)
telecom_operator_dict=dict()
forlineintelecom_operator:
num, operator=line.strip().split(' ')
telecom_operator_dict.update({num: operator})
returntelecom_operator_dictclassTrieTree(object):
"""    Trie 树的基本方法,用途包括:    - 词典 NER 的前向最大匹配计算    - 繁简体词汇转换的前向最大匹配计算    """def__init__(self):
self.dict_trie=dict()
self.depth=0defadd_node(self, word, typing):
"""向 Trie 树添加节点。        Args:            word(str): 词典中的词汇            typing(str): 词汇类型        Returns: None        """word=word.strip()
ifwordnotin ['', '\t', ' ', '\r']:
tree=self.dict_triedepth=len(word)
word=word.lower()  # 将所有的字母全部转换成小写forcharinword:
ifcharintree:
tree=tree[char]
else:
tree[char] =dict()
tree=tree[char]
ifdepth>self.depth:
self.depth=depthif'type'intreeandtree['type'] !=typing:
print(
'`{}` belongs to both `{}` and `{}`.'.format(
word, tree['type'], typing))
else:
tree['type'] =typingdefbuild_trie_tree(self, dict_list, typing):
""" 创建 trie 树 """forwordindict_list:
self.add_node(word, typing)
defsearch(self, word):
""" 搜索给定 word 字符串中与词典匹配的 entity,        返回值 None 代表字符串中没有要找的实体,        如果返回字符串,则该字符串就是所要找的词汇的类型        """tree=self.dict_trieres=Nonestep=0# step 计数索引位置forcharinword:
ifcharintree:
tree=tree[char]
step+=1if'type'intree:
res= (step, tree['type'])
else:
breakifres:
returnresreturn1, NoneclassPhoneLocation(object):
""" 对于给定的电话号码,返回其归属地、区号、运营商等信息。    该方法与 jio.extract_phone_number 配合使用。    Args:        text(str): 电话号码文本。若输入为 jio.extract_phone_number 返回的结果,效果更佳。            注意,仅输入电话号码文本,如 "86-17309729105"、"13499013052"、"021 60128421" 等,            而 "81203432" 这样的电话号码则没有对应的归属地。            若输入 "343981217799212723" 这样的文本,会造成误识别,须首先从中识别电话号码,再进行            归属地、区号、运营商的识别    Returns:        dict: 该电话号码的类型,归属地,手机运营商    Examples:        # [{'text': '13288568202', 'offset': (5, 16), 'type': 'cell_phone'},           {'text': '(021)32830431', 'offset': (18, 31), 'type': 'landline_phone'}]        # {'number': '(021)32830431', 'province': '上海', 'city': '上海', 'type': 'landline_phone'}        # {'number': '13288568202', 'province': '广东', 'city': '揭阳',           'type': 'cell_phone', 'operator': '中国联通'}    """def__init__(self, GRAND_DIR_PATH, TELE_DIR_PATH):
self.cell_phone_location_trie=Noneself.GRAND_DIR_PATH=GRAND_DIR_PATHself.TELE_DIR_PATH=TELE_DIR_PATHdef_prepare(self):
""" 加载词典 """cell_phone_location, zip_code_location, area_code_location=phone_location_loader(self.GRAND_DIR_PATH)
self.zip_code_location=zip_code_locationself.area_code_location=area_code_locationself.cell_phone_location_trie=TrieTree()
fornum, locincell_phone_location.items():
self.cell_phone_location_trie.add_node(num, loc)
self.cell_phone_pattern=re.compile(CELL_PHONE_CHECK_PATTERN)
self.landline_phone_pattern=re.compile(LANDLINE_PHONE_CHECK_PATTERN)
self.landline_area_code_pattern=re.compile(
LANDLINE_PHONE_AREA_CODE_PATTERN)
# 运营商词典telecom_operator=telecom_operator_loader(self.TELE_DIR_PATH)
self.telecom_operator_trie=TrieTree()
fornum, locintelecom_operator.items():
self.telecom_operator_trie.add_node(num, loc)
def__call__(self, text):
""" 输入一段电话号码文本,返回其结果 """ifself.cell_phone_location_trieisNone:
self._prepare()
res=self.cell_phone_pattern.search(text)
ifresisnotNone:  # 匹配至手机号码cell_phone_number=res.group()
first_seven=cell_phone_number[:7]
_, location=self.cell_phone_location_trie.search(first_seven)
province, city=location.split(' ')
# print(province, city)_, operator=self.telecom_operator_trie.search(
cell_phone_number[:4])
return {'number': text, 'province': province, 'city': city,
'type': 'cell_phone', 'operator': operator}
res=self.landline_phone_pattern.search(text)
ifresisnotNone:  # 匹配至固话号码# 抽取固话号码的区号res=self.landline_area_code_pattern.search(text)
ifresisnotNone:
area_code=res.group(1)
province, city=self.area_code_location.get(
area_code, ' ').split(' ')
ifprovince=='':
province, city=None, Nonereturn {'number': text, 'province': province,
'city': city, 'type': 'landline_phone'}
else:
return {'number': text, 'province': None,
'city': None, 'type': 'landline_phone'}
return {'number': text, 'province': None,
'city': None, 'type': 'unknown'}
@annotate("string->string")
classPhoneClean(object):
def__init__(self):
# 引用资源self.GRAND_DIR_PATH=get_cache_file('phone_location.txt')
self.TELE_DIR_PATH=get_cache_file('telecom_operator.txt')
self.phone_location_x=PhoneLocation(self.GRAND_DIR_PATH, self.TELE_DIR_PATH)
defevaluate(self, arg0):
ifarg0isNoneorlen(str(arg0)) ==0:
returnNoneelse:
try:
regex_phone=self.phone_location_xnew_text=str(arg0).replace(" ", '')
returnregex_phone(new_text)['type']
exceptException:
returnNone

在UDF这一段,需要留意资源的导入。最初在DataWorks中导入资源后,发现无法识别。最后是通过MaxCompute Studio的add方法来添加的资源。然后需要在init方法中使用get_cache_file将资源引入。如果放在外面是没有办法实现调用的。

相关实践学习
基于MaxCompute的热门话题分析
本实验围绕社交用户发布的文章做了详尽的分析,通过分析能得到用户群体年龄分布,性别分布,地理位置分布,以及热门话题的热度。
SaaS 模式云数据仓库必修课
本课程由阿里云开发者社区和阿里云大数据团队共同出品,是SaaS模式云原生数据仓库领导者MaxCompute核心课程。本课程由阿里云资深产品和技术专家们从概念到方法,从场景到实践,体系化的将阿里巴巴飞天大数据平台10多年的经过验证的方法与实践深入浅出的讲给开发者们。帮助大数据开发者快速了解并掌握SaaS模式的云原生的数据仓库,助力开发者学习了解先进的技术栈,并能在实际业务中敏捷的进行大数据分析,赋能企业业务。 通过本课程可以了解SaaS模式云原生数据仓库领导者MaxCompute核心功能及典型适用场景,可应用MaxCompute实现数仓搭建,快速进行大数据分析。适合大数据工程师、大数据分析师 大量数据需要处理、存储和管理,需要搭建数据仓库?学它! 没有足够人员和经验来运维大数据平台,不想自建IDC买机器,需要免运维的大数据平台?会SQL就等于会大数据?学它! 想知道大数据用得对不对,想用更少的钱得到持续演进的数仓能力?获得极致弹性的计算资源和更好的性能,以及持续保护数据安全的生产环境?学它! 想要获得灵活的分析能力,快速洞察数据规律特征?想要兼得数据湖的灵活性与数据仓库的成长性?学它! 出品人:阿里云大数据产品及研发团队专家 产品 MaxCompute 官网 https://www.aliyun.com/product/odps&nbsp;
目录
相关文章
|
1天前
|
API
查询城市手机号段免费API接口教程
此API用于查询指定城市的手机号段、服务商、区号、邮编等信息。支持POST或GET请求,需提供用户ID、KEY及城市名称等参数。返回数据包括状态码、信息提示、查询数量、最大页码、当前页码、省份、城市、区号、邮编、区划代码及数据集等。示例中提供的ID和KEY为公共测试用,建议使用个人ID和KEY以获得更高的调用频率。
|
1天前
|
API
手机号段查询城市免费API接口教程
此接口用于根据手机号段查询相关信息,包括城市、运营商、区号等。支持POST和GET请求,需提供用户ID、KEY及手机号段前7位作为参数。返回数据包含状态码、信息提示及详细归属信息。示例请求地址:https://cn.apihz.cn/api/ip/haoduan.php?id=88888888&key=88888888&numbers=1321993。接口免费,建议使用个人ID与KEY以独享调用频次。
|
5月前
|
DataWorks Oracle 关系型数据库
DataWorks产品使用合集之手动添加上了,但是同步过来的数据这个字段依然显示为空,是什么原因
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
|
3月前
|
SQL 数据采集 DataWorks
DataWorks产品使用合集之空间管理员怎么删除其他人创建的表
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
|
5月前
|
分布式计算 DataWorks 安全
DataWorks产品使用合集之如何删除已有表中的单个字段
DataWorks作为一站式的数据开发与治理平台,提供了从数据采集、清洗、开发、调度、服务化、质量监控到安全管理的全套解决方案,帮助企业构建高效、规范、安全的大数据处理体系。以下是对DataWorks产品使用合集的概述,涵盖数据处理的各个环节。
70 1
|
6月前
分享:2秒快速查询40万手机号码归属地,批量手机号码归属地查询可以导出excel表格,WPS表格查询手机号码归属地怎么操作,批量手机号码归属地批量查询软件,批量号码查询按省份和城市分类,按运移动号码电信号码联通号码分类整理
本文介绍了如何批量快速查询手机号码归属地并进行分类。首先,通过提供的百度网盘或腾讯云盘链接下载免费查询软件。其次,开启软件,启用复制粘贴功能,直接粘贴号码列表并选择高速查询。软件能在极短时间内(如1.76秒内)完成40多万个号码的查询,结果包括归属地、运营商、邮箱和区号,且数据准确。之后,可直接导出数据至表格,若数据超过100万,可按省份、城市及运营商分类导出。文章还附带了操作动画演示,展示全程流畅的处理大量手机号码归属地查询的过程。
316 0
分享:2秒快速查询40万手机号码归属地,批量手机号码归属地查询可以导出excel表格,WPS表格查询手机号码归属地怎么操作,批量手机号码归属地批量查询软件,批量号码查询按省份和城市分类,按运移动号码电信号码联通号码分类整理
|
6月前
|
分布式计算 运维 DataWorks
DataWorks常见问题之查出所有字段失败如何解决
DataWorks是阿里云提供的一站式大数据开发与管理平台,支持数据集成、数据开发、数据治理等功能;在本汇总中,我们梳理了DataWorks产品在使用过程中经常遇到的问题及解答,以助用户在数据处理和分析工作中提高效率,降低难度。
|
6月前
|
SQL 数据挖掘 数据处理
「SQL面试题库」 No_99 顾客的可信联系人数量
「SQL面试题库」 No_99 顾客的可信联系人数量
|
存储 SQL 分布式计算
用户/帖子/好友/订单中心如何进行数据库水平切分
用户/帖子/好友/订单中心如何进行数据库水平切分
|
数据库
LeetCode(数据库)- 好友申请l:总体通过率
LeetCode(数据库)- 好友申请l:总体通过率
95 0