查看原文
其他

特别推荐 | “正则表达式”在工业企业数据库匹配中的运用(一)

Dyson 数据Seminar 2021-06-03




导读


近些年来,随着微观数据可得性的增加和研究的进一步深入,经济学界在数据的使用方式上由对单一数据的潜心挖掘逐渐向融合多源数据过渡!
“中国工业企业数据库”是国内学术研究使用最多的微观数据库之一,利用合并数据进行多样化的企业经济行为分析是工业企业数据库利用的应用趋势。比如,基于中国知网,通过全文关键词检索发现,近五年(2015年-2019年9月),融合工业企业数据与海关进出口数据发表的文章共有403篇,其中CSSCI期刊比例高达92.56%;融合工业企业数据与境外投资企业(机构)名录发表的文章有22篇左右,其中CSSCI期刊比例为77.27%。
由于各个口子的数据,在工业企业的识别上或是没有唯一代码,亦或唯一代码不相同,这使得我们通常需要利用企业名称进行数据库之间的融合。然而,工业企业名称这一字段本身并不“干净”,举例而言——

1.“T T K 家庭电器有限公司”、“3 M 中国有限公司”等含有数字、字母型企业名,中英文符号混乱不一致;

2.还有,武汉旭东食品有限公司的企业名曾出现过“武汉旭东食品公司(2002、2008-2010)”、“武汉旭东食品有限责任公司(2003)”、“武汉市东西湖区旭东食品公司(2004)”和“武汉东西湖旭东食品有限公司(2005-2006)”以及“武汉东西湖旭东食品有限责任公司(2007)”等几种类型;

3.另有“广西横县闵桂钢厂(2003)”与“横县闵桂钢厂(2000-2002、2004-2009)”,地区冠名问题,等等。


可见,企业名称这一字段本身的数据质量很有问题,不进行事先的预处理就用企业名称进行合并,可能会带来各种问题。
事实上,哪怕是在工业企业库内部,因为各种可能原因,同一家企业在不同年份上企业名称都可能存在不能匹配的情况。
因此,对企业名称进行文本预处理,是数据库融合之前并不可少的步骤。
一般企业名称命名规则为:地区冠名+企业取名+行业属性+企业类型。其中,前缀“地区冠名”和后缀“企业类型”两个部分命名信息比较固定,无法用来对企业进行关键性识别,必要时应模式化处理;主干部分“企业取名+行业属性”变化很大,真实反映企业ID属性,主要用来企业身份识别。
有时,工业企业数据库中企业名称文本信息并不规范,主要原因有:

1、部分企业名称中包含标点符号、数字和字母,而同一企业名称可能由于中英文标点符号、数字和字母不一致会引起样本企业匹配不上。比如,中文符号“【”与英文符号“[”不一致,中英文数字、字母不一致问题等。


2、标点符号统一后,利用企业全称进行精确匹配后,部分无法匹配的企业,可能由于企业名称包含地区冠名信息丢失,如“威海ABC有限公司”写成“威海ABC有限公司”;或是企业类型发生变更后不一致,像“威海市ABC有限公司”变更“威海市ABC股份有限公司”等诸如此类现象。这时,需要用到企业名称主体进行模糊匹配,尽可能充分提升样本企业识别率。




正则表达式介绍


当然,面对上述问题,您不可能把所有的企业名称导入到EXCEL中,利用替换功能逐一实现。如果真是那样,我想您或许是个“闲人”!
其实,面对百万级的企业数据,可以利用正则表达式来对企业名称中的干扰信息进行技术预处理。
正则表达式(Regular Expression,代码简写regex、regexp或RE),又称规则表达式,是计算机科学的一个概念,它通常被用来检索、替换那些符合某个模式(规则)的文本。
正则表达式的用途非常广泛,凡是涉及到文本处理的算法,都离不开正则表达式。我们一般用正则表达式做如下操作:

1、字符串的提取,即从一段文本中提取一个或多个符合正则表达式所代表的文本规则的字符串。(在Python的re库中,有re.search,re.findall等函数可以实现。)


2、字符串的替换,即替换掉一段文本中的一个或多个符合正则表达式所代表的文本规则的字符串。(在Python的re库中,由re.sub函数来执行。)


3、字符串的分割,即将一段文本中的一个或多个以符合正则表达式所代表的文本规则的字符串作为“分隔符”,分成多个小字符串,(在Python的re库中,由re.split函数来执行。)

正则表达式虽然看起来也就只有这么几个功能,但是与其他算法和业务逻辑结合起来,所带来的惊喜只能用“层出不穷”来形容。(大家如果感兴趣的话,我今后可以再写一些正则表达式的使用案例,这里不再赘述。)接下来,我会简单的以字符串的提取为例讲讲正则表达式的常规使用。


正则表达式的元字符



说了这么多,“文本规则”是什么意思呢?
例如,我们想拿出一段文本里的所有数字,“数字”就是一个文本规则!一个数字的话,在正则表达式里用元字符“\d”来表示,我们可以用Python来试一下。
In [1]: str0 = """今天去理发,洗剪吹68,烫发和染发668。我就做了个洗剪吹,结账的时候发现居然收我668!!我不服,1米95的经理走出来对我耐心地解释:你看哈,刚才洗头的时候,是不是感觉水很烫??"""
In [2]: re.findall(r'\d', str0) # 加上r模式是为了防止Python中的转义字符与正则表达式中的元字符Out[2]: ['6', '8', '6', '6', '8', '6', '6', '8', '1', '9', '5']

左右滑动查看更多

“\d”就是正则表达式的元字符。但是并不能满足我们的需求,这么散乱的数字没有任何意义。于是我们调整一下思路,我们要的不是“数字”,而是所有“由纯数字组成的文本”,那么就可以这么实现。
In [3]: re.findall('\d+', str0)Out[3]: ['68', '668', '668', '1', '95']
左右滑动查看更多

这就引出了另外一个元字符“+”,“+”在正则表达式里代表的是“匹配前面的子表达式一次或多次”。

在“\d+”中,子表达式就是“\d”,一个“\d”是匹配一个数字,“\d+”则就是匹配一个或者多个数字。这很好理解。
更多的,我们并不满足以上结果,因为最后两个数字1和95被分开了。那么我们可以再做这样的改进。
In [4]: re.findall('[\d米]+', str0)Out[4]: ['68', '668', '668', '1米95']

左右滑动查看更多

[]方括号代表字符集合。匹配所包含的任意一个字符。“[\d米]”即匹配一个“\d”或者一个“米”字符,再带上加“+”,就可以匹配一个或者多个了。
以上就是我平时写正则表达式的思路,一般情况下,复杂的文本规则是很难一步写到位的,通过不断的尝试不断的提升自己正则表达式的精度才能达到目的。
常用的元字符有以下几种:

来源于菜鸟教程

其实这些元字符还可以进一步分类,限于篇幅我没法介绍很多,大家可以通过这个网址学到更过元字符的用法。

https://www.runoob.com/regexp/regexp-metachar.html




正则表达式的运算符优先级


与普通的算术计算类似,正则表达式也有自己的运算规则。下表从最高到最低说明了各种正则表达式运算符的优先级顺序:

来源于菜鸟教程
转义符不用多说,如果“\d”可能不表示数字,天知道出了BUG会有多难找。这里我想重点说说两个“或”逻辑的误区。我们直接从代码来看。
In [1]: re_str1 = r'[(ABC)(XYZ)]'
In [2]: re_str2 = r'(ABC|XYZ)'
In [3]: str0 = 'ABCXYZ'
In [4]: re.findall(re_str1, str0)Out[4]: ['A', 'B', 'C', 'X', 'Y', 'Z']
In [5]: re.findall(re_str2, str0)Out[5]: ['ABC', 'XYZ']

左右滑动查看更多

方括号“[]”与竖线“|”都有“或”逻辑在里面,但是在方括号内,圆括号“()”是形同虚设的,即r'[(ABC)(XYZ)]'等同于r'[ABCXYZ]'。这主要是因为方括号“[]”只能匹配一个字符。之前例子中的“\d”中,转义符的优先级高于方括号“[]”。所以程序处理完“\d”后,已经将其视为一个字符了。这是一个初学者比较容易犯的错误(例如当年的我),还请大家引起重视。




正则表达式的分组标签


我这边想简单介绍一下正则表达式的分组,分组在Python中有着极其重要的应用。我们练习正则表达式的时候,可能不会去考虑她。但是在一些特殊情况下会有出其不意的效果。我们从代码来看问题。
In [1]: str0 = """经过《喜羊羊与灰太狼》全集统计,灰太狼一共被红太狼的平底锅砸过9544次,被喜羊羊捉弄过2347次,被食人鱼追过769次,被电过1755次,捉羊想过2788个办法,奔波过19658次,足迹能绕地球954圈,至今一只羊也没吃到,他并没有放弃,"""
In [2]: re.findall(r'[\d次个圈]+', str0)Out[2]: ['9544次', '2347次', '769次', '1755次', '2788个', '19658次', '954圈']

左右滑动查看更多

按照我们之前的方法,如果要统计灰太狼被虐的经历,re.findall就有点捉襟见肘了。因为我们根本无法根据结果来判断哪个数字对应哪个统计值。被“平底锅砸过9544次”显然要比“被喜羊羊捉弄过2347次”来的疼。所以要拿出我们所关心的数据,我们就需要给每个数据打上标签。
In [3]: re_str = '(?P<平底锅伺候>(?<=平底锅砸过)\d+次)'
In [4]: re.search(re_str, str0).groupdict()Out[4]: {'平底锅伺候': '9544次'}
In [5]: re_str = '(?P<平底锅伺候>(?<=平底锅砸过)\d+次).*(?P<食人鱼伺候>(?<=食人鱼追过)\d+次)'
In [6]: re.search(re_str, str0).groupdict()Out[6]: {'平底锅伺候': '9544次', '食人鱼伺候': '769次'} In [7]: re_str = '平底锅砸过(?P<平底锅伺候>\d+次).*食人鱼追过(?P<食人鱼伺候>\d+次)'
In [8]: re.search(re_str, str0).groupdict()Out[8]: {'平底锅伺候': '9544次', '食人鱼伺候': '769次'}
左右滑动查看更多

图一
  1. (?P<key>pattern)是正则表达式中的标签语法。圆括号()在正则表达式里代表分组。对应于Python中SRE_Match对象(即re.search返回的对象)下的groups方法,返回的是一个列表。?P<key>加进去以后,就给这个分组打上标签了。

  2. (?<=平底锅砸过)用到了向后查找lookbehind这个语法,意思就是匹配“平底锅砸过”后面的字符串,不包括“平底锅砸过”这几个字。可惜的是,由于Python的re库的引擎原因,这样的语法中的字符串长度必须是固定长度的。例如,(?<=平底锅\d)是可以的,四个字符;(?<=平底锅\d\w)是可以的,五个字符;(?<=平底锅\d+)就不行了,因为不能确定“+”代表几个字符,在Python中运行的话,会直接报错。但是这个缺陷可以由分组的方式完美避开,参考上面代码中的最后一个正则表达式。

细心的同学可能会发现,上面代码中的最后两个正则表达式,关键词的顺序必须是固定的,即“平底锅”不能出现在“食人鱼”后面。这个是可以通过优化正则表达式来解决的,也是我喜欢用分组标签的一个重要原因,聪明的你知道如何修改吗?在留言区留下你的答案吧!




如何学习正则表达式


如果你的网上搜正则表达式的教程,你会发现身边有各种各样快速入门教程。我当时学正则表达式的时候,也就花了几个小时把这本100页书上的代码都试了一遍,当时觉得不过如此。

图二

然而真正在项目里需要用的时候,你会发现自己被啪啪啪的疯狂打脸。你会发现自己的代码只能在自己脑子里run出正确的结果。这就是我不想在这里过多的介绍正则表达式的基础语法的原因。网上的快速入门要不要看,要!但那个只是帮自己克服面对这种“乱七八糟”的代码的恐惧而已。真正的入门,应该是从我们在实际案例中使用正则表达式开始。
最后,讲了这么多,其实大家可以发现,写正则表达式就是一个找规律的过程。本期,我们主要介绍了正则表达式基本用法的两个案例,下期将继续为您介绍应用正则表达式对工业企业数据库企业名称进行预处理,敬请期待。








►往期推荐

回复【Python】👉简单有用易上手

回复【学术前沿】👉机器学习丨大数据

回复【数据资源】👉公开数据

回复【可视化】👉你心心念念的数据呈现

回复【老姚专栏】👉老姚趣谈值得一看


►一周热文

工具技巧丨如何解决机器学习中数据不平衡问题

特别推荐丨老姚专栏:宏观数据VS微观问题:谨防生态学谬误

工具&技巧丨能够融合Stata、Python和R的神器——Jupyter Notebook

数据呈现丨22个!史上最全Python数据可视化库大合集

工具&技巧 | 经济学圈特供 小刘帮你画专业社会网络图(二)





数据Seminar

这里是大数据、分析技术与学术研究的三叉路口


作者:Dyson(刘颖波)审阅、修订:杨奇明、简华(何年华)、江东(刘良东)编辑:青酱






    欢迎扫描👇二维码添加关注    


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存