从事审计行业, 对于搞审计的人来说,sql可以说是大部分人都必会的知识了,join等语法应该都很熟悉。但面对被审计单位的许多非结构化数据,或者说从不同系统导出的数据,例如“重庆市渝北区丁义珍诊所”这样的机构名称,其又没有记录公司的税务识别号;或是基于手工记录的Excel文档、审计组下发回填上报的数据,没经过数据库的约束性校验,那么公司名就很有可能不是标准的,当我们想比对不同表间的公司名称时, 传统的sql语句就难以匹配到。像oracle等数据库提供的相似度函数UTL_MATCH.EDIT_DISTANCE_SIMILARITY、UTL_MATCH.JARO_WINKLER_SIMILARITY,仅基于一个相似度分数难以衡量分数大小,即便尝试不同的相似度分数,也难以对找到合适的值,所以本篇基于一次W项目提供一个解决思路
companynameparser
--- github项目地址:company name parser[^1] 作者主页:Ming Xu (徐明) extract company name brand. 中文公司名称分词工具,支持公司名称中的地名,品牌名(主词),行业词,公司名后缀提取。
示例
input:
import companynameparser
company_strs = [
"武汉海明智业电子商务有限公司",
"泉州益念食品有限公司",
"常州途畅互联网科技有限公司合肥分公司",
"昆明享亚教育信息咨询有限公司",
]
for name in company_strs:
r = companynameparser.parse(name)
print(r)
output:
{'place': '武汉', 'brand': '海明智业', 'trade': '电子商务', 'suffix': '有限公司', 'symbol': ''}
{'place': '泉州', 'brand': '益念', 'trade': '食品', 'suffix': '有限公司', 'symbol': ''}
{'place': '常州,合肥', 'brand': '途畅', 'trade': '互联网科技', 'suffix': '有限公司,分公司', 'symbol': ''}
{'place': '昆明', 'brand': '享亚', 'trade': '教育信息咨询', 'suffix': '有限公司', 'symbol': ''}
基于
安装
- 全自动安装:
pip install -U companynameparser - 半自动安装:
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分词词典中仍为“永川市”,又如“大足区”、“合川区”也未在词典中,再加之场景下,部分个独企业诊所多数以“人名”来命名,部分人名无法正确分词,因此需要添加自定义词典以保证分词准确。
::: info
注意,自定义用户词典,应当根据程序运行的结果不断调整,例如某些地名无法被正确分词至“place”分组,那么应当更新用户自定义词典
:::
这是一个示例的用户自定义词典:
格式为 {词} {词频} {词性}
永川 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,在末尾添加如下内容:
::: info
应当根据程序运行的结果不断调整
:::
中医
西医
中西医
结合
(综合)
口腔诊所
儿科
妇科
医学美容
眼科
python实现
由于使用了作者提供的包,建议在构建项目时,创建CITATION文件,并输入以下内容:
@software{companynameparser,
author = {Xu Ming},
title = {companynameparser: Company Name parser Tool},
year = {2021},
url = {https://github.com/shibing624/companynameparser},
}
以下是python实现,常量部分仅供阅读参考
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分词器使用示例