基于Python的公司名分词比对

本文最后更新于 2024年12月7日 晚上

从事审计行业, 对于搞审计的人来说,sql可以说是大部分人都必会的知识了,join等语法应该都很熟悉。但面对被审计单位的许多非结构化数据,或者说从不同系统导出的数据,例如“重庆市渝北区丁义珍诊所”这样的机构名称,其又没有记录公司的税务识别号;或是基于手工记录的Excel文档、审计组下发回填上报的数据,没经过数据库的约束性校验,那么公司名就很有可能不是标准的,当我们想比对不同表间的公司名称时, 传统的sql语句就难以匹配到。像oracle等数据库提供的相似度函数UTL_MATCH.EDIT_DISTANCE_SIMILARITYUTL_MATCH.JARO_WINKLER_SIMILARITY,仅基于一个相似度分数难以衡量分数大小,即便尝试不同的相似度分数,也难以对找到合适的值,所以本篇基于一次W项目提供一个解决思路

companynameparser


github项目地址:company name parser[1]
作者主页: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[2]

安装

  • 全自动安装:pip install -U companynameparser
  • 半自动安装:
1
2
3
git clone https://github.com/shibing624/companynameparser.git
cd 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 分词器词性标注的解释[3]

标签含义标签含义标签含义标签含义
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 os
from typing import List
import pandas as pd
import companynameparser as cp
import jieba
from pathlib import Path

# 定义常量
class 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表中同一个诊所机构在分词秩序上,即便是多个品牌词的情况,第一个都是相同的(从结果上看也基本符合),又如,万一有关键词重名,但一个是“中医诊所”,一个是“西医诊所”的情况也未考虑,但案例确为真实且有成效的,仅用于审计疑点的初筛已足够,就没有继续往下追。
如对你审计工作有一点帮助或启发,就足够了。如有不足,请多包含指正。


参考

  1. 中文公司名称分词工具,支持公司名称中的地名,品牌名(主词),行业词,公司名后缀提取,companynameparser
  2. “结巴”中文分词:做最好的 Python 中文分词组件
  3. 词性标注,tokenizer参数可指定内部使用的 jieba.Tokenizer分词器使用示例

基于Python的公司名分词比对
https://inkcodes.com/2024/12/07/基于Python的公司名分词比对/
作者
Specialhua
发布于
2024年12月7日
更新于
2024年12月7日
许可协议