python和正则表达式抽取文本化的信息

本文介绍了如何借助正则表达式和Python从非结构化文本中抽取结构化信息,以高中班主任统计学生毕业去向为例,详细解析了正则表达式的匹配方法和Python代码实现,包括分组、匹配定位符和“或”关系的使用,最终将数据转换为表格并导出。通过这个实例,读者可以学习到正则表达式在处理大量文本数据时的高效性和实用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

很多人的日常工作,都是要和大量的文本打交道的。例如学者需要阅读大量的文献材料,从中找到灵感、数据与论据;学生需要阅读很多教科书和论文,然后写自己的报告或者做幻灯;财经分析师,需要从大量的新闻报道中,找到行业的发展趋势和目标企业动态的蛛丝马迹。

但不是所有的文本处理,都那么新鲜而有趣。有一项重要但繁琐的工作,就是从大量的文本当中抽取结构化的信息。

然而,结构化的信息不一定就在那里,静候你来使用。很多时候,它蕴藏在以往生成的非结构化文本中。

你可能早已习惯,人工阅读文本信息,把关键点摘取出来,然后把它们拷贝粘贴放到一个表格中。从原理上讲,这样做无可厚非。但是实际操作里面,效率太低,而且太麻烦。

大部分人,是不愿意从事这种简单重复的枯燥工作的。一遍遍机械重复鼠标划定文本范围,⌃Ctrl+C、切换到表格文档、找准输入位置,再 ⌃Ctrl+V……这种事儿做得太多,对你的肩肘关节,甚至是身心健康,都有可能造成不利影响。

想不想尝试用一种更简单的自动化方式,替你快速完成这些烦人的操作步骤呢?读过本文后,希望你能找到答案。样例

这里,我们举一个极端简化的中文文本抽取信息例子。之所以这样做,是为了避免你在解读数据上花费太多时间。我更希望,你能够聚焦于方法,从而掌握新知。

假设一个高中班主任,高考后让班长统计一下学生们的毕业去向。班长很认真地进行了调查,然后做了如下汇报:

张华考上了北京大学
李萍进了中等技术学校
韩梅梅进了百货公司
……

为了让你对样例足够熟悉,甚至有共鸣,这里我从 1998 年版的新华字典中,「借鉴」了部分内容。够贴心吧?

现实生活中,一个班大概不会只有 3 个人,因此你可以想象这是一个长长的句子列表。但其实班主任有个隐含的意思没有表达出来,即:

我想要一张表格

所以,看到这一长串的句子,你可以想象他的表情。班长估计也很难堪:

想要表格你早说啊!

这时候,假设你是班长,怎么办?信息都在文本里面。但如果需要转换成表格,就得一个个信息点去寻找和处理。其实,对于四五十人的班级来说,手动操作也不是什么太难的事情。但是设想一下,如果你需要处理的数据量,是这个例子的十倍、百倍甚至千万倍呢?继续坚持手动处理?这不仅麻烦,而且不现实。

我们需要找到一种简单的方法,帮助我们自动抽取相应的信息。此处我们使用的方法,是正则表达式

正则

「正则表达式」这个名字,初听起来好像很玄妙。实际上,它是从英文 「regular expression」翻译过来的。如果译成白话,那就是 「有规律的表述形式」。

这,听起来,是不是就更加接地气了?但是,给你补一下 「假行家 101」课程:

说别人听得懂的话,你能唬得住谁?

约定俗成,咱们继续沿用 「正则表达式」,来称呼它好了。从创生之日起,它就给文本处理带来了高效率。但是,用它的主要人群,却不是时常跟文字打交道的作家、编辑、学者、文员,而是……程序员!

程序员写的代码,是文本;程序员处理的数据,很多也是文本格式。其中便有很多显著的规律可循。

正是靠着正则表达式这种独门秘籍,许多别人做起来,需要昏天黑地一整周的任务,程序员可以半小时搞定,然后喝着咖啡等下班。

即便到了泛人工智能的今天,正则表达式依然有许多令你意想不到的应用。例如人机对话系统。你可能看了新闻报道,总以为人机对话都是靠着知识图谱或者深度学习搞出来的。不能说这里面,没有上述炫酷技术的参与。但它们充其量,只占其中一部分,或许还只是一小部分。

生产实践里面,大量的对话规则后面,并不是让你倍感神奇深奥的神经网络,而是一堆正则表达式。你可能会担心,这样高端的应用技术,自己能掌握吗?

答案是:当然!

正则表达式,并不难学。尤其是当你把它和 Python 结合到一起,那简直就是效率神器了。我们这就来看看,正则表达式怎么帮我们识别出样例文本里面 「人名」和 「去向」信息。

试练

请你开启一个浏览器,键入这个 网址

你会看见如下界面:

它可是一个正则表达式实验的利器。我教 INFO 5731 课程时,学生们就是在掌握了这个工具以后,迅速玩儿转了正则表达式。

这么好的工具,一定要价不菲吧?不,它是免费的。你放心大胆使用就好了。

我们首先把左侧的编程语言,从默认的 PHP ,调整为 Python。之后,把需要进行处理的文本,贴到中间空白的大文本框里面。

img

下面我们来尝试进行 「匹配」。什么叫做匹配呢?就是你写一个表达式,电脑便拿着鸡毛当令箭,在每一行文本上,都认认真真地找有没有符合该表达式的文本段落。如有,则会高亮显示出来。

这里我们观察一下,发现每个句子里面,人员去向前面,都有一个 「了」字。好,我们就在中部上方小文本框里,把 「了」字输入进去。可以看到,三句话里面的 「了」,全都亮了。

img

这就是你接触到的第一种匹配方式 —— 按照字符原本的意思来查找一致的内容。因为样例文本的规律性,我们可以把 「了」当成一个定位符,它后面,到句子结束位置,是 「去向」信息。咱们需要找的一半结构化信息,不就是这个 「去向」吗?

我们尝试匹配 「去向」。怎么匹配呢?这次每一行的字儿都不一样啊?没关系,正则表达式强大之处,此时就显示出来了。你可以用一个点号,也就是 .,表示任意字符。字母、数字、标点…… 甚至是中文,也能涵盖在内。

然后我们继续想想看,去向信息这里,会有几个字呢?不好说。例子里面这简单的三句话,就有 「4 个字」或者 「6 个字」两种情况。所以,我们无法指定去向信息里面字符的长度。但这也没关系,我们只需要用一个星号(*),就可以代表出现次数,从 0 到无穷大都可以匹配。当然,实际情况中,是不会真出现无穷大的。

我们在刚才输入的基础上,加上 .*,结果就成了这个样子:

img

不错嘛!不过似乎去向信息和 「了」字儿都是一样颜色的高亮。那不就混到了一起吗?我们可不想这样。

怎么办?请你在 .* 的两侧,尝试加入一对小括号(注意,不要用中文全角符号)试试看。

img

你会发现,这次 「了」依然用蓝色表示,而后面的去向信息,已经变成了绿色。这一对小括号,很重要,它叫做 「分组」,是提取信息的基本单位。

我们的任务已经解决了一半了,是吧?下面我们来尝试把人名一并抽取出来。我们来找人名的锚定位置。细细观察,你不难发现,每个人名的后面,都有个动词跟着。升学的同学,用的是 「考」字,而就业的同学,用的是 「进」字。

我们先尝试一下 「考」字。这里我们尝试直接把 「考」字放在 「了」字以前。但是你会发现,什么匹配结果也没有。

img

为什么?回看数据,你会发现,人家用的原词是 「考上了」。当然这里我们可以输入 「上」字。不过你要考虑一下更为通用的情况。好比说,「考取了」怎么办?「考入了」呢?更好的方式,是继续使用我们刚才学会的 「大招」,在 「考」和 「了」之间,插入一个 .*

这时候,你的正则表达式的样子是 考.*了(.*)

img

看,第一行的信息成功匹配了吧?但是,那后面还有两行没有匹配,怎么办?

我们依样画葫芦,就会发现,使用进.*了(.*) 就能正确匹配后两行:

img

问题来了:匹配第一行的,匹配不了后两行,反之亦然。这不好。我们希望写的表达式,能够更通用

怎么办?我们看看正则表达式当中 「或」关系的表示。这里,我们可以把两个字符用竖线隔开,旁边用中括号括起来,代表两者任一出现,都算匹配成功。

也就是,把正则表达式,写成这样:[考|进].*了(.*)

img

太棒了,三行的内容都已经匹配成功。这里,动词词组,和代表时态的 「了」作为中间锚定信息,我们可以放心大胆,把之前的人名信息,提取出来了。

也就是这样写:(.*)[考|进].*了(.*)

img

注意此时,人名分组是绿色,去向分组是红色的。我们成功提取了两组信息!庆祝一下!

可是,如果你给班主任看这里的结果,估计他不会满意。

表格,我要表格!

别着急,该 Python 出场了。下面我们尝试在 Python 把数据正式提取出来。

代码

首先,读入 Python 正则表达式包:

import re

然后,我们把数据准备好。注意为了演示代码的通用性,我这里在最后加了一行文字,区别于之前的文字规律,看看我们的代码能否正确处理它:

data = """张华考上了北京大学
李萍进了中等技术学校
韩梅梅进了百货公司
他们都有光明的前途"""

然后,该写正则表达式了。你真的需要自己手动来写吗?当然不必。

强大的 regex101 网站,已经帮助我们准备好了:

img

请你点击上图中红色圈出的按钮,网站会为你准备好一个初始代码的模板,可以匹配你需要的模式:

img

你不需要完全照搬代码。其中有这样一句,是很重要的,拷贝过来,贴到 Colab Notebook 就好:

regex = r"(.*)[考|进].*了(.*)"

以上就是你的正则表达式,在 Python 里面应有的样子。

我们准备一个空列表,用来接收数据:

mylist = []

接着,写一个循环:

for line in data.split('\n'):
  mysearch = re.search(regex, line)
  if mysearch:
    name = mysearch.group(1)
    dest = mysearch.group(2)
    mylist.append((name, dest))

我给你解释一下这个循环里面,各条语句的含义:

  • data.split('\n') 把文本数据按行来拆分开。这样我们就可以针对每一行,来获取数据;
  • mysearch = re.search(regex, line) 这一句尝试匹配模式到该行内容;
  • if mysearch 这个判断语句,是让程序分辨一下,该行是否有我们要找的模式。例如最后一行文字,里面并没有咱们前面分析的文字模式。遇到这样的行,直接跳过;
  • name = mysearch.group(1) 是说匹配的第一组内容,也就是 regex101 网站里绿色代表的人名分组存到 name 变量里。下一句依次类推。注意 group 对应你正则表达式里面小括号出现的顺序,从 1 开始计数;
  • mylist.append((name, dest)) 把该行抽取到的信息,存入到咱们之前定义的空列表里面。

注意,如果不加 mysearch = re.search(regex, line) 这一句,程序会对每一行都尝试匹配并且抽取分组内容,那么结果就会报这样的错误:

img

所以你看,用正则表达式抽取信息时,不能蛮干。

此时,我们查看一下 mylist 这个列表里面的内容:

mylist

结果为:

[('张华', '北京大学'), ('李萍', '中等技术学校'), ('韩梅梅', '百货公司')]

不错,一个不多,一个不少,恰好是我们需要的。

我们要把它导出成为表格。方法有很多,但是最简便顺手的,是用 Pandas 数据分析软件包:

import pandas as pd

只需要利用 pd.DataFrame 函数,我们就能把上面列表和元组(tuple)组成的一个二维结构,变成数据框。

df = pd.DataFrame(mylist)
df.columns = ['姓名', '去向']

注意,这里我们还非常细心地修改了表头。

看看你的劳动成果吧:

df

img

有了数据框,转换成为 Excel ,就是一行代码的事情了:

df.to_excel("dest.xlsx", index=False)

进入 Files 标签页,刷新并且查看一下当前目录下的内容:

img

这个 dest.xlsx 就是输出的结果了。下载之后我们可以用 Excel 打开查看。

img

任务完成!你可以把结果提交给班主任,看他满意的笑容了。

小结

这篇教程里面,咱们谈了如何利用文本字符规律,借助 Python 和正则表达式,来提取结构化信息。

希望你已经掌握了以下本领:

  • 了解正则表达式的功用;
  • 用 regex101 网站尝试正则表达式匹配,并且生成初步的代码;
  • 用 Python 批量提取信息,并且根据需求导出结构化数据为指定格式。

再次强调一下,对于这么简单的样例,使用上述方法,绝对是大炮轰蚊子。然而,如果你需要处理的数据是海量的,这个方法给你节省下来的时间,会非常可观。希望你能够举一反三,在自己的工作中灵活运用它。

(.*)[考|进].*了(.*) 方括号中不需要使用 | ,因为方括号本身的作用就是任选其中的一个字符,[考|进] 会匹配 考、|、进 三个中的任意一个。
### Java 实现快速序(LeetCode 题目) 快速序是一种基于分治法的高效算法,其核心思想是通过选取一个基准值(pivot),将数组划分为两部分:一部分小于基准值,另一部分大于基准值。接着递归地对这两部分分别进行快速序,最终得到有序数组。 以下是基于 LeetCode 的 Java 快速序实现: #### 基本实现 ```java class QuickSort { public static void quickSort(int[] arr, int low, int high) { if (low < high) { // 找到分区点 int pi = partition(arr, low, high); // 对左侧子数组进行快速序 quickSort(arr, low, pi - 1); // 对右侧子数组进行快速序 quickSort(arr, pi + 1, high); } } private static int partition(int[] arr, int low, int high) { // 选择最后一个元素作为基准值 int pivot = arr[high]; int i = (low - 1); // 小于基准值的部分的索引 for (int j = low; j < high; j++) { // 如果当前元素小于或等于基准值 if (arr[j] <= pivot) { i++; // 交换 arr[i] 和 arr[j] int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } // 交换 arr[i+1] 和 arr[high] (即基准值) int temp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = temp; return i + 1; } public static void main(String[] args) { int[] arr = {10, 7, 8, 9, 1, 5}; int n = arr.length; System.out.println("原始数组:"); printArray(arr); quickSort(arr, 0, n - 1); System.out.println("快速序后的数组:"); printArray(arr); } private static void printArray(int[] arr) { for (int value : arr) { System.out.print(value + " "); } System.out.println(); } } ``` 上述代码实现了标准的快速序逻辑[^4]。它通过 `partition` 方法找到基准值的位置,并将其两侧的子数组递归序。 --- #### 关键点析 1. **基准值的选择** 在上面的实现中,选择了数组中的最后一个元素作为基准值。这种策略简单易懂,但在某些情况下可能导致性能下降(如输入数组已经接近有序)。可以通过随机化基准值来优化性能。 2. **时间复杂度分析** 快速序的时间复杂度取决于划分是否均衡。理想情况下,每次划分都能将数组均匀分成两半,则时间复杂度为 \(O(n \log n)\)[^4]。然而,在最坏的情况下(例如每次都选到了最小或最大值作为基准值),时间复杂度会退化至 \(O(n^2)\)[^4]。 3. **空间复杂度** 快速序的空间复杂度主要由递归调用栈决定。在平均情况下,递归深度为 \(O(\log n)\),因此空间复杂度也为 \(O(\log n)\)[^4]。 --- #### 结合 LeetCode 题目的应用实例 以下是一个典型的 LeetCode 题目——第 215 号问题:“Kth Largest Element in an Array”,该题目要求找出无序数组中的第 k 大元素。可以利用快速序的思想决此问题。 ```java import java.util.Random; public class KthLargestElement { public int findKthLargest(int[] nums, int k) { return select(nums, 0, nums.length - 1, nums.length - k); } private int select(int[] nums, int left, int right, int kSmallest) { if (left == right) { return nums[left]; } int pivotIndex = new Random().nextInt(right - left + 1) + left; pivotIndex = partition(nums, left, right, pivotIndex); if (kSmallest == pivotIndex) { return nums[kSmallest]; } else if (kSmallest < pivotIndex) { return select(nums, left, pivotIndex - 1, kSmallest); } else { return select(nums, pivotIndex + 1, right, kSmallest); } } private int partition(int[] nums, int left, int right, int pivotIndex) { int pivotValue = nums[pivotIndex]; swap(nums, pivotIndex, right); int storeIndex = left; for (int i = left; i < right; i++) { if (nums[i] < pivotValue) { swap(nums, i, storeIndex++); } } swap(nums, storeIndex, right); return storeIndex; } private void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } public static void main(String[] args) { KthLargestElement solution = new KthLargestElement(); int[] nums = {3, 2, 1, 5, 6, 4}; int k = 2; System.out.println(solution.findKthLargest(nums, k)); // 输出应为 5 } } ``` 这段代码展示了如何结合快速序的核心思想决问题,同时引入了随机化的基准值选择方法以提高鲁棒性。 --- ### 总结 快速序作为一种高效的算法,广泛应用于各种场景。无论是基础的数组序还是更复杂的实际问题求,都可以借助快速序的强大能力完成任务。以上提供了两种典型的应用案例,分别是通用的快速序以及特定条件下的变种实现。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虚坏叔叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值