08. 反向引用


上一篇博客学习了使用子表达式进行分组, 对模式的重复进行控制. 现在, 我们来学习子表达式的另一种重要用途 – 反向引用 (backreference).

相信如果是第一次听到这个名词, 你一定会和我一样充满困惑, 什么是反向引用?
这里从微软官方文档中引用一段

Backreferences provide a convenient way to identify a repeated character or substring within a string. For example, if the input string contains multiple occurrences of an arbitrary substring, you can match the first occurrence with a capturing group, and then use a backreference to match subsequent occurrences of the substring.

简单点来说, 反向引用是用来匹配重复的. 我们还是通过书中的例子来学习, 对其有一个直观认识, 不然的话太难以理解了.

匹配 HTML 标签

HTML 程序员使用标题标签 (<h1><h6>, 以及配对的结束标签) 来定义和排版 Web 页面里的标题文字. 假设你现在需要把某个 Web 页面里的所有标题文字全部查找出来, 不管是几级标题.
文本

<body>
<h1>Welcome to my homepage</h1>
Content is divided into two sections:<br>
<h2>SQL</h2>
Information about SQL.
<h2>RegEx</h2>
Information about Regular Expressions.
</body>

代码

import re

# 测试文本
text = """
<body>
<h1>Welcome to my homepage</h1>
Content is divided into two sections:<br>
<h2>SQL</h2>
Information about SQL.
<h2>RegEx</h2>
Information about Regular Expressions.
</body>
"""

# 正则表达式
REGEXP = r'<[hH]1>.*?</[hH]1>'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    for r in rs:
        print(r)
else:
    print("No match!")

结果
在这里插入图片描述
这样看起来它是可以正常工作的, 如果我们想要匹配 h1h6, 则只需要改为: <[hH][1-6]>.*?</[hH][1-6]
在这里插入图片描述

注意: 这里使用的是 .*? 懒惰型而不是 .* 贪婪型. 否则, 可能会从第一个 h1 匹配到最后一个 h2. 这里只是可能, 因为元字符 . 通常无法匹配换行符, 而这里每一个标题都各自占据了一行.

但是, 上面这样就没有问题了吗? 我们来看下一个例子:
一个错误的例子

<body>
<h1>Welcome to my homepage</h1>
Content is divided into two sections:<br>
<h2>SQL</h2>
Information about SQL.
<h2>RegEx</h2>
Information about Regular Expressions.
<h2>This is not valid HTML</h3>
</body>

代码

import re

# 测试文本
text = """
<body>
<h1>Welcome to my homepage</h1>
Content is divided into two sections:<br>
<h2>SQL</h2>
Information about SQL.
<h2>RegEx</h2>
Information about Regular Expressions.
<h2>This is not valid HTML</h3>
</body>
"""

# 正则表达式
REGEXP = r'<[hH][1]>.*?</[hH][1]>'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    for r in rs:
        print(r)
else:
    print("No match!")

结果
在这里插入图片描述
这里会匹配到一个不符合规范的标题: <h2>This is not valid HTML</h3>. 这个问题在于匹配的第二部分 (用来匹配结束标签的那部分) 对匹配的第一部分 (用来匹配开始标签的那部分) 一无所知. 这就需要使用反向引用了.

匹配重复出现的单词

待会再去解决上面那个问题, 现在先来看一个简单的问题: 假设你有一段文本, 你想把这段文本里所有连续重复出现的单词 (打字错误, 同一个单词输了两遍) 找出来. 显然, 在搜索某个单词的第二次出现时, 这个单词必须是已知的. 反向引用允许正则表达式模式引用之前匹配的结果.

文本

This is a block of of text,
several words here are are
repeated, and and they
should not be. 

代码

import re

# 测试文本
text = """
This is a block of of text,
several words here are are
repeated, and and they
should not be. 
"""

# 正则表达式
REGEXP = r'[ ]+(\w+)[ ]+\1'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.finditer(text)
if rs:
    for r in rs:
        print(r.group())
else:
    print("No match!")

结果
在这里插入图片描述
分析
上面这个例子的原理是: [ ]+ 匹配一个或多个空格, \w+ 匹配一个或多个字母数字字符, [ ]+ 匹配结尾的空格. 注意, \w+ 是出现在括号里的, 所以它是一个子表达式. 该子表达式并不是用来进行重复匹配的, 这里也没什么要重复匹配的. 它只是对模式分组, 将其表示出来以备后用. 模式最后一部分是 \1, 这是对前面那个子表达式的反向引用, \1 匹配的内容与第一个分组匹配内容一样. 因此, 如果 (\w+) 匹配的是单词 of, 那么 \1 也匹配单词 of; 如果 (\w+) 匹配的是单词 and, 那么 \1 也匹配单词 and.

注意: 术语 “反向引用” 指的是这些实体引用的是先前的子表达式.
\1 到底是什么意思? 它匹配模式中所使用的第一个子表达式, \2 匹配第二个子表达式, \3 匹配第三个, 以此类推. 所以, 在上面那个例子中, [ ]+(\w+)[ ]+\1 匹配连续两次重复出现的单词.

注意: 在不同的正则表达式实现中, 反向引用的语法差异不小.

提示: 可以把反向引用想象成变量.

所以对于匹配 HTML 标签这个问题, 我们可以这样来做:

<([hH][1-6])>.*</\1>

这样, 它就可以排除掉错外的标题标签了.
在这里插入图片描述
或者这样写: <(?P<title>[hH][1-6])>.*</(?P=title)>, 同样也是可以匹配到正确结果的.

捕获组 Capturing Group

这里来提一下捕获组的概念, 希望可以帮助你理解反向引用.

模式的一部分可以用括号括起来 (…)。这被称为“捕获组(capturing group)”。
这有两个影响:

  1. 它允许将匹配的一部分作为结果数组中的单独项。
  2. 如果我们将量词放在括号后,则它将括号视为一个整体。

子表达式其实就是一个捕获组, 所以前面提到了分组的概念.
捕获组又分为: 数字捕获组和命名捕获组. 前面提到的 \1 就是数字捕获组. 命名捕获就是给某个子表达式起一个唯一的名称, 随用用该名称 (而不是相对位置) 来引用这个子表达式.
所以上一个例子中的数字捕获组可以改成如下的命名捕获组: [ ]+(?P<word>\w+)[ ]+(?P=word)

注意: 反向引用只能用来引用括号里的子表达式.
提示: 反向引用匹配通常从 1 开始计数 (\1, \2 等). 在许多正则表达式实现里, 第 0 个匹配 (\0) 可以用来代表整个表达式.

正则表达式实现替换操作

前面学习的内容, 都是关于如何使用正则表达式进行搜索的, 但是实际上正则表达式还可以进行替换! 这也是一个非常强大给功能, 但是注意并不是所有的实现都是支持它的. 例如, grep, SQL 它们的正则实现就不支持替换操作.

然后, 我们看一下书里的示例, 匹配电子邮件的地址, 并将其转换为可点击的连接. 在 HTML 文档里, 你需要使用 <a href="mailto:user@address.com">user@address.com</a> 这样的语法来创建一个可点击的电子邮件地址. 下面我们来看如何使用反向引用来实现这个目的 !

注意: 我们这里使用的是 Python, 所以代码中的实现和书中不一样.

文本

Hello, ben@forta.com is my email address.
Hello, ben@forta.com1 is my email address.
Hello, ben@forta.com2 is my email address.
Hello, ben@forta.com3 is my email address.

代码

import re

# 测试文本
text = """
Hello, ben@forta.com is my email address.
Hello, ben@forta1.com is my email address.
Hello, ben@forta2.com is my email address.
Hello, ben@forta3.com is my email address.
"""

# 正则表达式
REGEXP = r'(\w+[\w\.]*@[\w\.]+\.\w+)'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

# 替换
new_text = pattern.sub(
    lambda e: f'<a href="mailto:{e.group(1)}">{e.group(1)}</a>', text)
print(new_text)

结果
在这里插入图片描述
替换操作需要使用两个正则表达式: 一个用来指定搜索模式, 另一个用来指定替换模式.

我们也可以使用命名捕获组:
代码

import re

# 测试文本
text = """
Hello, ben@forta.com is my email address.
Hello, ben@forta1.com is my email address.
Hello, ben@forta2.com is my email address.
Hello, ben@forta3.com is my email address.
"""

# 正则表达式
REGEXP = r'(?P<email>\w+[\w\.]*@[\w\.]+\.\w+)'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.findall(text)
if rs:
    print(rs)
else:
    print("No match!")

# 替换
new_text = pattern.sub(
    lambda e: f'<a href="mailto:{e.group("email")}">{e.group(1)}</a>', text)
print(new_text)

结果
在这里插入图片描述

注意: 注意, 这里是可以同时使用命名捕获和数字捕获的, 但是如果不使用命名捕获的话, 只能使用数字捕获. 去掉 ?P<email> 就会报下面这个错误了.
在这里插入图片描述

网易单词例句解析

最后贡献一个有趣的示例吧, 前段时间, 我准备制作一个学习英语的工具, 我有一份单词列表, 然后我准备给它们配上一个中英文例句. 然后我写了一个抓取网易有道词典数据的 demo. 不过, 后来废弃了, 因为感觉这样速度太慢, 而且网易会进行封禁, 并且这些数据是有版权问题的. 不过这里可以贴一下当时用于取出数据的正则表达式用法:

单词例句格式
网易有道词典单词例句格式如下, 去除了空格, 单词来源.

1.
While centralization ensures that all students are equipped with roughly the same resources and perform at roughly the same level, it also discourages experimentation. 
虽然集中化可以确保所有学生拥有大致相同的资源,达到大致相同的表现,但它也对实验不利。
2.
Centralization is an important growing trend for corporate. 
集中化管理是企业重要的发展趋势。
3.
The latter contains the three centralization adoption phases. 
接下来的部分包含了三个集中式采用阶段。

获取了这样一个文本之后, 接下来的工作就是取出每一个例句的序号, 例句 和 翻译了. 如果是你, 你会怎么做呢? 我当时还没有学习正则表达式, 所以我刚开始是每三行作为一个处理单元, 用常用的字符串操作来分别取出 序号, 例句 和 翻译. 后来学习了正则之后, 我就改进了方法, 我们直接上代码吧!

代码

import re
import json

# 测试文本
text = """
1.
While centralization ensures that all students are equipped with roughly the same resources and perform at roughly the same level, it also discourages experimentation. 
虽然集中化可以确保所有学生拥有大致相同的资源,达到大致相同的表现,但它也对实验不利。
2.
Centralization is an important growing trend for corporate. 
集中化管理是企业重要的发展趋势。
3.
The latter contains the three centralization adoption phases. 
接下来的部分包含了三个集中式采用阶段。
"""

# 正则表达式
REGEXP = r'(?P<no>\d+?)\.\n(?P<original>.+?)\n(?P<translate>.+?)\n'

# 编译
pattern = re.compile(REGEXP)

# 匹配
rs = pattern.finditer(text)
for r in rs:
    print(r.groupdict())
    print(json.dumps(r.groupdict(), ensure_ascii=False, indent=4))
    print("===========================================")

结果
在这里插入图片描述
总结
使用正则表达式之后, 整个实现就变得非常简洁和优雅了. 而且是一步到位的转换, 直接从文本转成了 Dict 对象, 省去了操作字符串, 创建 Dict 然后再赋值的步骤了.

总结

反向引用在文本匹配和替换中, 非常有用, 是一个很强大的功能. 这里只是一个简单的介绍, 先对它有一个初步的理解. 这个系列, 也快结束了, 预计还会再写几篇博客吧, 如果感兴趣的话, 可以接着往下看. 不过, 近来工作也蛮累的, 很多时候都心有余而力不足了. 哈哈.

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值