从事审计行业, 对于搞审计的人来说,sql可以说是大部分人都必会的知识了,join
等语法应该都很熟悉。但面对被审计单位的许多非结构化数据,或者说从不同系统导出的数据,例如“重庆市渝北区丁义珍诊所”这样的机构名称,其又没有记录公司的税务识别号;或是基于手工记录的Excel文档、审计组下发回填上报的数据,没经过数据库的约束性校验,那么公司名就很有可能不是标准的,当我们想比对不同表间的公司名称时, 传统的sql语句就难以匹配到。像oracle等数据库提供的相似度函数UTL_MATCH.EDIT_DISTANCE_SIMILARITY
、UTL_MATCH.JARO_WINKLER_SIMILARITY
,仅基于一个相似度分数 难以衡量分数大小,即便尝试不同的相似度分数,也难以对找到合适的值,所以本篇基于一次W项目提供一个解决思路
companynameparser github项目地址:company name parser 作者主页:Ming Xu (徐明) extract company name brand. 中文公司名称分词工具,支持公司名称中的地名,品牌名(主词),行业词,公司名后缀提取。
示例 input:
1 2 3 4 5 6 7 8 9 10 11 import companynameparser company_strs = [ "武汉海明智业电子商务有限公司" , "泉州益念食品有限公司" , "常州途畅互联网科技有限公司合肥分公司" , "昆明享亚教育信息咨询有限公司" , ]for name in company_strs: r = companynameparser.parse(name) print (r)
output:
1 2 3 4 {'place' : '武汉' , 'brand' : '海明智业' , 'trade' : '电子商务' , 'suffix' : '有限公司' , 'symbol' : '' } {'place' : '泉州' , 'brand' : '益念' , 'trade' : '食品' , 'suffix' : '有限公司' , 'symbol' : '' } {'place' : '常州,合肥' , 'brand' : '途畅' , 'trade' : '互联网科技' , 'suffix' : '有限公司,分公司' , 'symbol' : '' } {'place' : '昆明' , 'brand' : '享亚' , 'trade' : '教育信息咨询' , 'suffix' : '有限公司' , 'symbol' : '' }
基于 该项目基于中文分词jieba
安装 全自动安装:pip install -U companynameparser
半自动安装: 1 2 3 git clone https://github.com/shibing624/companynameparser.gitcd companynameparser python setup.py install
通过以上两种方法的任何一种完成安装都可以。如果不想安装,可以下载github源码包,安装依赖requirements.txt 再使用。
离线安装:使用网络被隔离的环境,可以在github下载companynameparser和jieba的包后,甩到本机Python环境的site-packages
当中 项目场景 数据结构 审计手里有两张表,A表为机构信息表,从卫健部门系统导出,★标识的为关注的重点,表结构为:
序号 数据项 类型及长度 字段说明 1 当前状态 String “变更”、“注册”、“诊所备案”等 2 机构名称 String 导出机构名称★ 3 床位数 Int 本例中不重要 4 牙椅数 Int 本例中不重要
B表为医废运输信息表,从企业取得,部分表结构如下:
序号 数据项 类型及长度 字段说明 1 序号 String “变更”、“注册”、“诊所备案”等 2 联单编号 String 主键 3 联单类型 Int 如“医废转移联单”等 4 联单状态 Int “已办结”、“未办结”等 5 产废单位 String 机构名称★ 6 运输单位 String 运输医废的单位 7 转移量(kg) float 数量 … …… …… ……
To do 关注诊所类 机构,如A表中(名称为虚构),机构名称为“重庆市渝北区丁义珍诊所”,B表中,机构名称为“重庆渝北解放路丁义珍西医诊所”,两者实为同一机构,现需要查找在A表机构名称中有(诊所均需向卫健部门登记),而在B表产废单位中没有(诊所未将医废交由医废企业处理)的疑点
实现 1.自定义用户词典 由于jieba所使用的中文分词词典(位于jiaba包内dict.txt )缺失部分地点、人名,例如重庆市“永川区”在jieba分词词典中仍为“永川市”,又如“大足区”、“合川区”也未在词典中,再加之场景下,部分个独企业诊所多数以“人名”来命名,部分人名无法正确分词,因此需要添加自定义词典以保证分词准确。
注意,自定义用户词典,应当根据程序运行的结果不断调整,例如某些地名无法被正确分词至“place”分组,那么应当更新用户自定义词典
这是一个示例的用户自定义词典: 格式为{词} {词频} {词性}
1 2 3 4 5 6 7 8 9 永川 3 ns 永川区 9 ns 合川 3 ns 合川区 9 ns 大足 3 ns 大足区 9 ns 史建宁 3 nr 康太平 3 nr 刘新建 3 nr
以下是jieba.Tokenizer
分词器词性标注的解释。
标签 含义 标签 含义 标签 含义 标签 含义 n 普通名词 f 方位名词 s 处所名词 t 时间 nr 人名 ns 地名 nt 机构名 nw 作品名 nz 其他专名 v 普通动词 vd 动副词 vn 名动词 a 形容词 ad 副形词 an 名形词 d 副词 m 数量词 q 量词 r 代词 p 介词 c 连词 u 助词 xc 其他虚词 w 标点符号 PER 人名 LOC 地名 ORG 机构名 TIME 时间
2.修改companynameparser/data中的行业、后缀 由于companynameparser主要是针对公司名称,如“武汉海明智业电子商务有限公司”
这样的公司名来分词,对医疗机构来说,如“诊所”这样的词默认还是会被分到brand
里,我们不想让其作为干扰项,那么就修改/data/trade.txt
,在末尾添加“诊所”。 对于诊所前的“修饰”,如中医诊所、西医诊所、中西医诊所,修改data/suffix.txt
,在末尾添加如下内容:
1 2 3 4 5 6 7 8 9 10 中医 西医 中西医 结合 (综合) 口腔诊所 儿科 妇科 医学美容 眼科
python实现 由于使用了作者提供的包,建议在构建项目时,创建CITATION
文件,并输入以下内容:
1 2 3 4 5 6 @software{companynameparser, author = {Xu Ming}, title = {companynameparser: Company Name parser Tool}, year = {2021}, url = {https://github.com/shibing624/companynameparser}, }
以下是python实现,常量部分仅供阅读参考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 import osfrom typing import List import pandas as pdimport companynameparser as cpimport jiebafrom pathlib import Pathclass Paths : CONCAT_PATH = Path('/tmp/医废联单' ) STD_CLINIC_FILE = Path('/tmp/导出机构信息.xlsx' ) CUSTOM_DICT_PATH = Path('/tmp/custom_dict.txt' ) RESULT_FILE = Path('/tmp/未交医废企业处置诊所疑点表.xlsx' )class ColumnNames : UNIT_NAME_A = "机构名称" UNIT_NAME_B = "产废单位" FILTER_WORD = "诊所" UNION_NAME = "简化名" def init_jieba (): """初始化结巴分词用户词典""" if not Paths.CUSTOM_DICT_PATH.exists(): raise FileNotFoundError(f"用户词典文件不存在: {Paths.CUSTOM_DICT_PATH} " ) jieba.load_userdict(str (Paths.CUSTOM_DICT_PATH))def get_filelist (path: Path ) -> List [Path]: """ 获取传入路径内含子文件夹所有文件的绝对路径 Args: path: 指定路径 Returns: 包含所有文件绝对路径的列表 """ if not path.exists(): raise FileNotFoundError(f"指定路径不存在: {path} " ) return [Path(os.path.join(root, file)) for root, _, files in os.walk(path) for file in files]def get_brand (column_name: str , row: pd.Series ) -> str : """ 获取DataFrame某一列根据companynameparser分词的结果 Args: column_name: 列名 row: DataFrame的行数据 Returns: 分词后的品牌名 """ try : listbrand = cp.parse(row[column_name])['brand' ].split("," ) return listbrand[0 ] if listbrand else '' except Exception as e: print (f"处理行数据时出错: {row[column_name]} , 错误: {e} " ) return '' def get_dup_table (unit_name: str , df: pd.DataFrame ) -> pd.DataFrame: """ 获得单位名称与提取到的brand去重后的DataFrame Args: unit_name: 单位名称 df: 要去重的DataFrame Returns: 去重后的DataFrame """ return df[[unit_name, ColumnNames.UNION_NAME]].drop_duplicates()def main (): """主函数入口""" try : init_jieba() dfs = [] for file in get_filelist(Paths.CONCAT_PATH): if file.suffix == '.xls' : try : df = pd.read_excel(file, engine='xlrd' , skiprows=1 ) dfs.append(df) except Exception as e: print (f"读取文件失败 {file} : {e} " ) if not dfs: raise ValueError("没有找到可用的数据文件" ) df_all = pd.concat(dfs) df_filtered = df_all[df_all[ColumnNames.UNIT_NAME_B].str .contains( ColumnNames.FILTER_WORD, na=False )].copy() df_filtered[ColumnNames.UNION_NAME] = df_filtered.apply( lambda row: get_brand(ColumnNames.UNIT_NAME_B, row), axis=1 ) result_df_b = get_dup_table(ColumnNames.UNIT_NAME_B, df_filtered) df_std = pd.read_excel(Paths.STD_CLINIC_FILE) df_std_filtered = df_std[df_std[ColumnNames.UNIT_NAME_A].str .contains( ColumnNames.FILTER_WORD, na=False )].copy() df_std_filtered[ColumnNames.UNION_NAME] = df_std_filtered.apply( lambda row: get_brand(ColumnNames.UNIT_NAME_A, row), axis=1 ) result_df_a = get_dup_table(ColumnNames.UNIT_NAME_A, df_std_filtered) df_merged = pd.merge(result_df_a, result_df_b, on=ColumnNames.UNION_NAME, how='left' ) unmatched = df_merged[df_merged[ColumnNames.UNIT_NAME_B].isna()] unmatched.to_excel(Paths.RESULT_FILE, index=False ) except Exception as e: print (f"程序执行出错: {e} " )if __name__ == "__main__" : main()
结果示例 以下为部分结果示例:
机构名称 简化名 产废单位 重庆渝北解放路丁义珍西医诊所 丁义珍 重庆市渝北区丁义珍诊所 重庆市大足区丁真中西医结合诊所 丁真 大足丁真中西医结合诊所人民路分店 重庆市合川区宝宝巴士眼科诊所 宝宝巴士 重庆市北碚区张三口腔诊所 张三
后记 仅提供一个思路, 且有很多不完善的地方,例如,未对brand
分词结果为多个的情况继续细分,默认A表B表中同一个诊所机构在分词秩序上,即便是多个品牌词的情况,第一个都是相同的(从结果上看也基本符合),又如,万一有关键词重名,但一个是“中医诊所”,一个是“西医诊所”的情况也未考虑,但案例确为真实且有成效的,仅用于审计疑点的初筛已足够,就没有继续往下追。 如对你审计工作有一点帮助或启发,就足够了。如有不足,请多包含指正。