查看原文
其他

Python 实战 | 使用正则表达式从文本中提取指标

python实战 数据Seminar 2023-10-24



Python教学专栏,旨在为初学者提供系统、全面的Python编程学习体验。通过逐步讲解Python基础语言和编程逻辑,结合实操案例,让小白也能轻松搞懂Python!
>>>点击此处查看往期Python教学内容

本文目录

一、引言

二、原理概念

三、实现过程

四、总结

本文共 6471个字,阅读大约需要17分钟,欢迎指正!

Part1引言

正则表达式(Regular Expression)是一种强大的字符串处理工具,常用于字符串的匹配、查找和替换操作。通过定义特定的字符串格式,我们可以快速准确地从文本中提取所需信息,或对文本进行格式化和校验。正则表达式的语法灵活,可以处理各种复杂的字符串操作。之前的文章中介绍了正则表达式的基础知识、主要语法、进阶用法以及使用 Python 的 re 模块中正则函数的用法,本文将给出一个具体的工作场景来展示正则表达式在数据处理中的应用


💡  Python 教学 | “小白”友好型正则表达式教学(一) 

      Python 教学 | “小白”友好型正则表达式教学(二) 

      Python 教学 | “小白”友好型正则表达式教学(三)

本文处理的原始数据是行政处罚文书数据,将数据用 Excel 打开如下图所示。该数据包含了 9 个字段,分别存储“行政处罚 id”、“主体分类”、“处罚种类”、“标题”、“处罚对象分类”、“执法级别”、“执法地域”、“处罚日期”和“文书全文”的内容,我们的目的是从字段“文书全文”中将处罚的金额提取出来

本教程基于 pandas 2.0.0 版本书写

本文中所有 Python 代码均在集成开发环境 Visual Studio Code (VScode) 中使用交互式开发环境 Jupyter Notebook 中编写,本文分享的代码请使用 Jupyter Notebook 打开。


💡后台回复关键词“20230908”即可获取本文所有演示代码以及演示用的数据。

Part2原理概述

本文的原始数据是通过爬虫获取的。在网页中,行政处罚文书数据以表格的形式呈现,经过爬虫获取整个表格之后,内容将以 HTML 的格式存储在字段“文书全文”中。

网页中的表格
HTML 格式文本(部分)

我们处理的思路是:首先在字段“文书全文”的 HTML 文本中粗略的定位罚款金额所在的位置,如上图的例子中的“0.1(万元)”附近。由于各个文书中关于处罚金额的表述不一致,因此就需要编写一个灵活的正则表达式来实现,这里先展示一下提取的结果,相关过程会在下文继续介绍。需要注意的是,这一步的目标只是粗略的定位处罚金额所在位置,并没有准确地提取具体的金额,比如上图例子中提取的字符串是“:0.1(万元)”。

下图所示为粗略提取金额的结果(示例):

# [1,000.00]
# [2000]
# [:0.1(万元]
# []
# [:0.03(万元]
# [3,076.65]
# [1,000.00]
# [1000.00]
# [20]
# [2000]
# [5200元]
# [10000.00]
# [人民币5000]
# [人民币叁佰元]
# [:38192]
# [:0.1(万元]

可见,初步提取的结果中除了包含处罚金额外,还包含了其他不需要的字符,比如英文逗号,、中文左括号、货币单位、货币种类人民币等等。

接下来我们需要将这些多余的字符去除,使结果中只包含罚款金额的具体数值和单位,也就是只保留阿拉伯数字、小数点、中文数字和数值单位,这一步也是通过正则表达式来完成的,处理的结果如下图所示。需要注意的是,之前的结果以列表的形式存储的,而这一步的结果是以字符串的形式存储。

# 1000.00
# 2000
# 0.1万
# 0
# 0.03万
# 3076.65
# 1000.00
# 1000.00
# 20
# 2000
# 5200
# 10000.00
# 5000
# 叁佰
# 38192
# 0.1万

最后,我们需要将这些数字转换为阿拉伯数字,并将数据存储为浮点型,这里可以使用cn2an库完成,最终处理的结果如下:

# 1000.00
# 2000.00
# 1000.00
# 0.00
# 300.00
# 3076.65
# 1000.00
# 1000.00
# 20.00
# 2000.00
# 5200.00
# 10000.00
# 5000.00
# 300.00
# 38192.00
# 1000.00

Part3实现过程

现在我们已经了解了提取罚款金额的思路,下面来看一下如何用 Python 来实现上述过程。首先我们需要导入相关的库并且读取使用的数据,并且由于我们在过程中使用了cn2an库中的函数cn2an()来进行阿拉伯数字的转换,所以第一次使用之前需要先安装 cn2an 库。

# %pip 魔术命令,管理当前运行的 Kernel 对应的 Python 虚拟环境中的包
%pip install cn2an

import pandas as pd
import re
from cn2an import cn2an  # 用于中文数字转换
import numpy as np

# 导入数据
test = pd.read_csv(".\\DATA\\原始数据1.csv")

# 缺失值用空字符串填充
test.fillna('', inplace=True)

# 只保留处罚种类字段中包含“罚款”的样本
test = test[test['chufazhonglei'].str.contains('罚款')].reset_index(drop=True)

我们先使用名为“原始数据1.csv”的文件中的数据做探索性的尝试,这是全部原始数据的部分样例。使用正则表达式处理字符串的过程其实可以看作是在找规律,当样本量很大时,先用一个小的子数据集做探索是很有必要的。实际上,本文示例的原始数据非常庞大,正常情况下是无法一次性将所有数据都处理好的,需要多轮迭代处理,同时在每次处理过程中更换正则表达式,尽可能处理更多数据

将数据导入后,首先需要将其中的缺失值用空字符串填充,并且,由于存在罚款金额样本的字段“处罚种类”中一定包含“罚款”,所以我们可以先将字段“处罚种类”中不包含“罚款”的样本剔除掉。代码如下:

def find_cash(text):
    # 编译正则表达式
    pattern = re.compile(r'(?<=罚款)人民币.*元(?!以上|以下)|'
                         r'(?<=罚款).*元(?!以上|以下)|'
                         r'(?<=罚款):?:?[0-9,\.万]+|'
                         r'(?<=罚款)人民币\d*万*|'
                         r'(?<=处以).+罚款|'
                         r'(?<=处).*的罚款')
    # 匹配字符串
    cash = re.findall(pattern, text)
    return cash

# 创建'RE'字段,用于储存初步匹配结果
test['RE'] = test['quanwen'].apply(find_cash)

现在我们需要在行政处罚全文中粗略地提取处罚金额。观察原始数据可以发现,罚款金额都出现在“罚款”二字的附近,当罚款金额位于“罚款”二字之后时,表述方法有以下三种:

  • 罚款人民币xx元——这里的xx可能是阿拉伯数字或者中文数字,可能没有“人民币”
  • 罚款:xx——这里的xx是阿拉伯数字,可能带有单位“万”
  • 罚款人民币xx万——这里的xx是阿拉伯数字

当罚款金额在“罚款”二字之前时,表述方法有以下两种:

  • 处以xx罚款
  • 处xx的罚款——这两种情况下xx中都可能包含“人民币”

根据上述规律,我们可以编写如下正则表达式:

'(?<=罚款)人民币.元(?!以上|以下)|(?<=罚款).元(?!以上|以下)|(?<=罚款):?:?[0-9,.万]+|(?<=罚款)人民币\d万|(?<=处以).+罚款|(?<=处).*的罚款'

由于文本长度的原因,这个表达式不便于在程序中阅读,因此我们将这个代码拆成了六行,每行的正则表达式都以r开头表示原始字符串,然后使用r'内容'的形式将每行的正则表达式连接起来。拆分之后的正则表达式可读性提高了很多,可以看到这个正则表达式可以用|分成六个部分,不同部分之间是“或”的关系。

(r'(?<=罚款)人民币.*元(?!以上|以下)|'
 r'(?<=罚款).*元(?!以上|以下)|'
 r'(?<=罚款):?:?[0-9,\.万]+|'
 r'(?<=罚款)人民币\d*万*|'
 r'(?<=处以).+罚款|'
 r'(?<=处).*的罚款')

现在我们来分行解读这个正则表达式。每一行开头的(?<=罚款)表示肯定式向后断言,也就是说,待匹配的内容必须出现在“罚款”二字之后,但实际匹配到的结果中不包含“罚款”二字,(?<=处以)(?<=处)也是同样的含义。前两行的正则表达式最后的(?!以上|以下)表示否定式向前断言,即待匹配的内容不能出现在“以上”或“以下”之前。有关断言的用法可以参考往期文章Python 教学 | “小白”友好型正则表达式教学(三)

之所以设置否定式向前断言的条件,是因为文书中可能会引用相关的法律条文,比如:“根据《xx法》,应处以罚款人民币xx元以下”。这里的金额并非实际的处罚金额,所以不应该提取。

其他位置使用了量词元字符*?+,分别表示匹配任意次、匹配 0 或 1 次、匹配一次或一次以上;[]中的内容表示匹配字符串区间中的一个字符;转义符\的作用是将元字符转义为普通字符,或者表示特殊字符,比如\.代表匹配字符串“.”,\d代表匹配一个 0 - 9 之间的数字字符。有关量词元字符的用法可以参考文章Python 教学 | “小白”友好型正则表达式教学(二);转义符的用法可以参考文章Python 教学 | “小白”友好型正则表达式教学(一)

至此,已经完成了对罚款金额的粗略提取,现在我们需要保留提取内容中的数字和单位,然后将其他无关的内容去除,代码如下:

def keep_specific_chars(RE_list):
    # 未匹配到罚款金额
    if len(RE_list) == 0:
        return "0"
    else:
        string = RE_list[0]  # 将列表中的字符串取出
        pattern = re.compile(r'[0-9〇一二两三四五六七八九十百千万零壹贰叁肆伍陆柒捌玖拾佰仟萬\.]*')
        results = re.findall(pattern, string)
        return ''.join(results)  # 将识别到的数字连接起来

# 将'RE'字段中无关的字符删除,只保留数字
test['RE'] = test['RE'].apply(keep_specific_chars)

由于处罚金额的表述方式不同,所以金额中可能会包括阿拉伯数字、小写中文数字、大写中文数字和小数点,将这些可能的描述用正则表达式概括为:'[0-9〇一二两三四五六七八九十百千万零壹贰叁肆伍陆柒捌玖拾佰仟萬\.]*'

阿拉伯数字的转换需要使用函数cn2an(inputs, mode),该函数的作用是将中文字符转换为阿拉伯数字,函数中有两个参数,含义如下:

  • inputs:待转换的中文数字
  • mode:三种模式,分别为 strict 严格,normal 正常,smart 智能

mode='strict'模式(默认)下,只有严格符合数字拼写的才可以进行转换;
mode='normal'模式下可以将类似“一二三”的数字转换;
mode='smart'模式下可以将混合拼写的数字转换。示例如下:

# strict 模式
print(cn2an('五百二十', mode='strict'))
# 输出 520

# normal 模式
print(cn2an('五二零', mode='normal'))
# 输出 520

# smart 模式
print(cn2an('5百贰拾', mode='smart'))
# 输出 520

最后我们用 cn2an 库中的函数cn2an()将中文数字转换为阿拉伯数字,代码如下:

def trans(string):
    try:
        # 将中文数字转换为阿拉伯数字,并储存为浮点数类型
        return float(cn2an(string, mode = 'smart'))
    except:
        # 删除无法识别的部分
        return np.nan

# 将'RE'字段中的数字转换为阿拉伯数字
test['RE'] = test['RE'].apply(trans)

目前为止,我们已经实现了处理一份数据的代码。进一步地,如果我们有很多份待处理的数据,就可以将上述过程写成自定义函数,应用于每一份数据,最后合并即可。代码如下:

# 提取罚款内容
def find_cash(text):
    # 编译正则表达式
    pattern = re.compile(r'(?<=罚款)人民币.*元(?!以上|以下)|'
                         r'(?<=罚款).*元(?!以上|以下)|'
                         r'(?<=罚款):?:?[0-9,\.万]+|'
                         r'(?<=罚款)人民币\d*万*|'
                         r'(?<=处以).+罚款|'
                         r'(?<=处).*的罚款')
    # 匹配字符串
    cash = re.findall(pattern, text)
    return cash

# 提取罚款内容中的数字部分
def keep_specific_chars(RE_list):
    # 未匹配到罚款金额
    if len(RE_list) == 0:
        return "0"
    else:
        string = RE_list[0]  # 将列表中的字符串取出
        pattern = re.compile(r'[0-9〇一二两三四五六七八九十百千万零壹贰叁肆伍陆柒捌玖拾佰仟萬\.]*')
        results = re.findall(pattern, string)
        return ''.join(results)  # 将识别到的数字连接起来

# 将数字内容转换为阿拉伯数字,并转换为数值型
def trans(string):
    try:
        # 将中文数字转换为阿拉伯数字,并储存为浮点数类型
        return float(cn2an(string, mode = 'smart'))
    except:
        # 删除无法识别的部分
        return np.nan

# 提取处罚内容中的处罚金额
def keep_cash(data):
    # 筛选处罚种类包括“罚款”的样本
    data = data[data['chufazhonglei'].str.contains('罚款')].reset_index(drop=True)

    # 初步提取罚款金额
    data['chufajine'] = data['quanwen'].apply(find_cash)
    # 仅保留数字
    data['chufajine'] = data['chufajine'].apply(keep_specific_chars)
    # 将中文数字转换为阿拉伯数字
    data['chufajine'] = data['chufajine'].apply(trans)
    return data

自定义函数keep_cash的作用就是提取一份文件中的罚款金额(参数data接收传入的文件),现在我们只需要循环遍历 DATA 目录下的每一个 csv 文件,在提取罚款金额后,通过函数pd.concat()合并数据。

import glob

# 查找 DATA 目录下的 csv 文件
files = glob.glob('.\\DATA\\*.csv')
# 创建一个空数据框,用于储存合并后的数据
All_data = pd.DataFrame()

# 将 csv 文件逐一导入并处理
for file in files:
    # 导入数据
    dat = pd.read_csv(file)
    # 提取罚款金额
    dat = keep_cash(dat)
    # 将提取后的数据合并
    All_data = pd.concat([All_data, dat], axis=0, ignore_index=True)

批量处理后的最终数据如下图所示,其中加粗的一列(字段 'chufajine')就是提取的罚款金额。

Part4总结

正则表达式是一个强大而灵活的字符串处理工具,能够实现高效的模式匹配和数据提取。通过本文的具体应用实例,我们更加深入了解了正则表达式的语法和使用技巧。掌握正则表达式的应用技巧对于从事文本数据处理工作的人来说非常重要。希望本文能帮助读者更好地理解和掌握正则表达式,提升工作效率。



星标⭐我们不迷路!想要文章及时到,文末“在看”少不了!

点击搜索你感兴趣的内容吧

往期推荐

2023国家社科: 1014项经管类项目! 哪所高校立项最多?超全解读来了!

Python 教学 | 一文搞懂面向对象中的“类和实例”

数据可视化 | 没错!在 Python 中也能像 ggplot2 一样绘图





数据Seminar




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


文 | 《社科领域大数据治理实务手册》


    欢迎扫描👇二维码添加关注    
点击下方“阅读全文”了解更多

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

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