首页
/ calibre 正则表达式完全指南:从入门到精通(附带源码级原理解析)

calibre 正则表达式完全指南:从入门到精通(附带源码级原理解析)

2026-09-11 17:30:35作者:温艾琴Wonderful

正则表达式(Regular Expression,简称 regexp)是 calibre 中一项贯穿电子书内容处理与元数据管理的核心工具。本指南基于 calibre 官方文档《All about using regular expressions in calibre》整理并深度扩充,覆盖转换时「搜索与替换」、文件名元数据识别、批量编辑元数据、编辑器与阅读器搜索等全部使用场景,从最基础的字面匹配、字符集、量词,一直讲到分组、反向引用、标志位以及 (*SKIP)(*FAIL)\K、环视、递归等高级技巧。读完本文,你将能够亲手编写并验证自己的正则表达式,安全高效地完成电子书清理、元数据整理等实战任务。

一、先读这段:勇气与警告

这一章不可避免会有些技术性——毕竟正则表达式本就是用来做技术活的技术工具。文中会使用一些术语和概念,它们初看可能复杂费解,但请放心,每个新概念都会尽力解释清楚。

请不要被术语吓退。 正则表达式看起来像一串神秘莫测的“乱码”,但实际上它们并不复杂。即便是正则高手,阅读复杂表达式时也会头疼,但编写它并不难——关键在于一步一步地构造表达式。慢慢来,跟着一步步走进这个“兔子洞”。

二、calibre 中可以在哪些地方使用正则表达式?

calibre 在多个功能模块中内置了正则表达式支持,官方教程 列出的主要场景包括:

使用场景 入口位置 主要用途
转换选项中的「搜索与替换」 转换(Conversion)对话框 在转换过程中批量修改电子书文本与结构
导入设置中的文件名元数据识别 设置 → 添加书籍(Adding books) 从文件名提取书名、作者等元数据
批量编辑元数据中的搜索与替换 选中多本书 → 批量元数据编辑 跨字段批量修正元数据
电子书编辑器的搜索与替换 编辑书籍(Edit book)工具 在 HTML 源码层面精确替换
书库搜索 图书列表搜索栏 用正则进行复杂检索
电子书阅读器内部搜索 E-book viewer 搜索面板 在阅读内容中搜索

其中转换「搜索与替换」的底层实现在 src/calibre/gui2/convert/search_and_replace.py,它定义了 SearchAndReplaceWidget 组件,包含"搜索正则表达式"与"替换文本"两列输入,并提供添加、删除、上下移动、加载/保存 .csr 规则文件等功能。

三、正则表达式到底是什么?

一个正则表达式是描述一组字符串的方式。单个正则表达式可以匹配多种不同的字符串——这正是它强大的原因:它是描述海量变体的一种简洁表达。

关于"字符串"的说明:这里沿用编程语言中的含义,即一个或多个字符的序列,包括文字、数字、标点以及所谓"空白字符"(换行、制表符等)。请注意,通常大写与小写字符被视为不同字符,例如 aA 是两个不同的字符。

在 calibre 中,搜索栏里的正则默认不区分大小写,但转换选项中的正则默认区分大小写。有一种方法可以让所有正则都忽略大小写,稍后会讲到标志位(flags)时再说明。

最核心的概念

一个字符串本身就是匹配它自身的正则表达式。

如果要用正则匹配 "Hello, World!",那么正则就是 Hello, World!——就是这么简单。但注意,它只能精确匹配 "Hello, World!",不能匹配 "Hello, wOrld!""hello, world!" 之类的变体。

四、字符集(Sets):匹配一组字符中的任意一个

还记得正则可以匹配多个字符串吗?现在进入正题。假设要转换的电子书有一个烦人的页脚"Page 5 of 423",页码从 1 到 423,难道要写 423 个表达式?当然不用。

字符集(也叫字符类)允许你定义一个待匹配字符的集合:把想放入集合的字符放进方括号 [] 即可。例如:

  • [abc] 匹配字符 "a"、"b" 或 "c" 中的任意一个
  • [a-z] 匹配任意小写字母
  • [a-zA-Z] 匹配任意大小写字母

字符集永远只匹配集合中的一个字符。 字符集支持范围(range)语法。于是:

  • 表达式 Page [0-9] of 423 能匹配前 9 页
  • 表达式 Page [0-9][0-9] of 423 能匹配所有两位数页码
  • 第三个表达式自然是 Page [0-9][0-9][0-9] of 423,覆盖三位数页码

常用字符集速查

集合 含义
[0-9] 单个数字
[a-z] 单个小写字母
[A-Z] 单个大写字母
[a-zA-Z] 单个字母
[a-zA-Z0-9] 单个字母或数字
[^a] 除 "a" 之外的任意字符(补集)

大小写敏感性提醒:启用"搜索不区分大小写"设置时,大小写集合可能同时匹配大小写。这类设置在 calibre 的 偏好设置 → 搜索 中,以及 E-book viewer 和 Edit book 的搜索面板上都有。

转义简写(Shorthand)

简写 等价于 含义
\d [0-9] 一个数字
\w [a-zA-Z0-9_] 一个"单词"字符
\s 任意空白字符 空格、制表符、换行、换页、回车、不换行空格等
\D [^0-9] 任意非数字字符
\W 任意非单词字符 大写形式即补集
\S 任意非空白字符 大写形式即补集

"空白字符"(Whitespace)指所有不会被打印出来的字符,包括空格、制表符、换行符、换页符、回车符、不换行空格等。

补集(Complementing)

在集合的第一个位置^,即可定义"除集合内字符之外的任意字符":[^a] 匹配除 "a" 外的任意字符。转义简写同样可以用大写字母取补:\D 等价于 [^0-9]

结合这一点回头看前文的例子:<p[^>]*> 中的集合匹配的是"除右尖括号之外的任意字符",于是该表达式能精确匹配开标签。

五、量词(Quantifiers):重复前一个元素

字符集一次只能匹配一个字符,但有了量词,一切不同了。+?* 这三个特殊字符重复其前面的单个元素(元素指单个字符、字符集、转义序列或分组——总之是正则中的任意单一实体),它们统称通配符或量词:

量词 含义
? 前一个元素出现 0 或 1
* 前一个元素出现 0 或更多
+ 前一个元素出现 1 或更多

举例说明:

  • a? 匹配空字符串或 "a"
  • a* 匹配 ""、 "a"、"aa" 或任意连续多个 "a"
  • a+ 匹配 "a"、"aa" 或任意连续多个 "a"(注意:不匹配空字符串
  • [0-9]+ 匹配任意整数

于是回到页码例子上:一个表达式 Page [0-9]+ of 423 就足以匹配书中的所有页码了。

贪婪行为(Greedy Behaviour)与懒惰量词

注意:这些量词默认会尽可能多地匹配文本,这就是"贪婪行为"。当你想匹配一个标签时问题就来了。例如字符串 "<p class="calibre2">Title here</p>",你想匹配开标签(第一对尖括号之间的部分)。你可能会觉得 <p.*> 能匹配这个开标签,但实际上它匹配了整行字符串

(这里 . 是另一个特殊字符,它匹配除换行外的任意字符,因此 .* 基本能匹配你能想到的任何单行文本。)

解决办法是使用 <p.*?>——在量词 * 后面加 ? 使其非贪婪,这样表达式就只匹配第一个开标签。

还有一种办法:<p[^>]*> 也能匹配同一个开标签,原因参见上文补集部分。请记住:同一个匹配目标,往往有不止一种写法。

量词完整速查表

量词 前驱表达式的出现次数
? 0 或 1 次(等价于 {0,1}
+ 1 次或更多(等价于 {1,}
* 0、1 或更多次(等价于 {0,}
{n} 恰好 n 次
{min,max} 介于最小值与最大值之间(含端点)
{min,} 从最小值到无穷
{,max} 从 0 到最大值

性能警告:默认情况下正则引擎是贪婪的,常带来意外;在量词后加 ? 可使其懒惰(lazy)。尽量避免在同一表达式中使用两个懒惰量词,结果可能难以预测。更要警惕量词的嵌套,例如 (a*)* 这种模式会使处理时间呈指数级增长。

六、转义(Escaping):匹配字面特殊字符

想匹配一个点号或问号怎么办?很简单:在任何特殊字符前加一个反斜杠 \,它就会被解释为字面字符,不再具有特殊含义。反斜杠加单个字符这一组合称为转义序列(escape sequence),这一操作称为转义(escaping),一个转义序列被视为一个单一元素。

当然,有些转义序列不止是转义特殊字符,例如 \t 表示制表符。本文讨论中出现的任何"有某种功能"的字符都应视为特殊字符,想要匹配字面字符就必须转义。

特殊字符总表

需要转义的 12 个元字符(在它们前面加 \ 才能恢复为普通字符):

^ . [ ] $ ( ) * + ? | \

另外 7 个元字符不需要(但也可以)加反斜杠:

{ } ! < > = :
  • 在字符类内部(方括号 [] 内),特殊字符会失去特殊地位;但右方括号 ] 和连字符 - 在类中仍有特殊地位。类外的连字符只是普通字面字符,右方括号仍是元字符。
  • 斜杠 / 和井号 # 不是元字符,无需转义。
  • 在某些外部工具(如 regex101.com 的 Python 引擎)中,双引号是特殊分隔符必须转义,但在 calibre 编辑器中没有这一限制。

常用特殊字符转义

表示 字符
\t 制表符
\n 换行符
\x20 (可断行的)空格
\xa0 不换行空格

七、分组与或运算(Grouping & Alternation)

假设转换的书中奇数页印着 "Title"、偶数页印着 "Author",打印效果不错,但在电子书里很烦人。你可以用普通括号把整个表达式分组,并用 |(竖线字符)匹配它左边或右边的表达式

  • 先把奇偶页的表达式分组,得到 (Title)(Author) 两个表达式
  • 再用竖线合并:(Title|Author) 会在奇数页匹配 "Title",偶数页匹配 "Author"

当然,不用分组括号也能使用竖线:Title|Author 同样匹配 "Title" 或 "Author"。注意量词只重复前面的单一元素,而竖线的选择范围是它之前和之后的整个表达式。所以想只匹配 "Calibre" 和 "calibre" 中的大小写 c,必须用 (c|C)alibre 来保证只有 "c" 参与选择;如果写成 c|Calibre,匹配的将是字符串 "c" 或 "Calibre",并非所愿。一句话:拿不准时,就把竖线和分组一起用。

排除模式(Exclusion)——高级技巧

快速参考(manual/regexp_quick_reference.rst)中还给出了两种"匹配但不包括"的实用技巧:

方法 1:(*SKIP)(*FAIL)

pattern_to_exclude(*SKIP)(*FAIL)|pattern_to_select

例如 "Blabla"(*SKIP)(*FAIL)|Blabla 会在字符串 Blabla"Blabla or Blabla" 中选中 Blabla,但不会选中 "Blabla" 里被引号括起来的那处。

方法 2:\K

pattern_to_exclude\K|(pattern_to_select)

例如 "Blabla"\K|(Blabla),效果同上。\K 会将选择起点重置到它所在的位置。某些正则引擎(非 calibre)不允许变长后顾(lookbehind),而 \K 能绕过这一限制,实现变长"正向后顾"的等效效果。

八、分组进阶:反向引用(Backreferences)

分组还有个非常酷的用途:对之前匹配到的分组做引用。分组从 1 开始编号,用转义的数字引用,即第 5 个分组写作 \5。因此,如果对字符串 "Test Test" 搜索 ([^ ]+) \1,就能匹配整个字符串——\1 引用的是第一个分组刚匹配到的 "Test"。

在替换文本中同样可以引用分组:\n 表示按阅读顺序编号的第 n 个捕获分组(从 1 开始)。

分组类型完整参考

表示 含义
(expression) 捕获分组:保存选中内容,可在搜索/替换模式中用 \n 召回
(?:expression) 非捕获分组:不保存选中内容
(?>expression) 原子分组:表达式一旦满足,引擎即通过;若后续模式失败,不会回溯尝试该表达式内的其他组合。原子分组不捕获
`(? expression)`
(?<name>expression) 命名分组:搜索模式中用 (?P=name) 召回,替换模式中用 \g<name> 召回。两个不同分组可以同名

九、标志位(Flags):不区分大小写与点号匹配一切

之前提到过"让所有正则不区分大小写"的办法——标志位(flags)。在表达式里用特殊结构 (?flags) 来包含标志,例如忽略大小写的标志是 i,写作 (?i)。于是 (?i)test 可以匹配 "Test"、 "tEst"、"TEst" 及任何大小写变体。

另一个实用的标志是 s:让点号 . 匹配包括换行符在内的任意字符。多个标志可以放在同一结构中:(?is) 表示忽略大小写且点号匹配一切,标志顺序无关紧要,(?si) 与之等价。

模式(Modes)速查

模式 作用
(?i) 忽略大小写
(?s) 点号 . 也匹配换行符
(?m) 使 ^$ 锚点匹配行的开头和结尾,而非整个字符串的开头和结尾

十、环视(Lookarounds):零宽断言

环视用于在不消费字符的前提下做位置断言,它们是零长度、不捕获的,并且属于原子分组:断言一旦满足引擎即通过;若后续模式失败,不会在环视内部回溯尝试其他组合。

环视 含义
?= 正向前瞻(放在待选内容之后)
?! 负向前瞻(放在待选内容之后)
?<= 正向后顾(放在待选内容之前)
?<! 负向后顾(放在待选内容之前)

负向前瞻示例:

(?![^<>{}]*[>}])

放在模式末尾,可防止选中的内容位于文件内嵌的标签或样式中。

实践提示:在字符串中寻找多个匹配时,每次匹配尝试的起始位置处,后顾可以检查当前位置之前的字符。例如对字符串 123,模式 (?<=\d)\d(数字前还有一个数字)理论上应选中 2 和 3;而 \d\K\d 只能选中 2,因为第一次选中后起始位置紧挨着 3 之前,剩余字符不足再次匹配。同理 \d(\d) 只捕获 2。在 calibre 的正则引擎实践中,正向后顾的表现与此一致——只选中 2,与理论相反。

环视内部可以放分组,但捕获通常用处不大;若确有需要,务必小心后顾中的量词——与"无回溯"相伴的贪婪性可能产生出人意料的捕获结果。因此,当正向后顾的捕获分组中含有量词(甚至多个)时,优先使用 \K 而非正向后顾。 此外,尽量"锚定"环视,以减少引擎所需的匹配步骤数。

十一、递归(Recursion):处理嵌套结构

表示 含义
(?R) 整个模式的递归
(?1) 编号捕获分组(这里是分组 1)的模式的递归

递归就是"调用自己"。它非常适合平衡匹配,例如可能内嵌引号字符串的带引号字符串:处理一对双引号间的字符串时,若遇到新的双引号字符串开头,就递归调用自身。通用模板:

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

选择一对双引号之间、且不因内嵌字符串而提前停止的字符串:

“((?>[^“”]+|(?R))*[^“”]+)”

该模板同样可用于修改可嵌套的成对标签,如 <div> 标签。

十二、锚点(Anchors):匹配位置而非字符

锚点匹配的是字符串中的逻辑位置而非字符。文本处理中最常用的锚点有:

  • \b:单词边界,即空白到非空白字符的过渡。例如用 \bsurd 匹配 the surd 而不匹配 absurd
  • ^:行的开头(默认即多行模式)
  • $:行的结尾(默认即多行模式)
  • \K:将选择起点重置到其在模式中的位置

字符类完整参考

表示
[a-z] 小写字母(不含带重音字符和连字)
[a-z0-9] 小写字母 a-z 或数字 0-9
[A-Za-z-] 大小写字母或连字符。连字符放进类时必须放在开头或结尾,以免与范围连字符混淆
[^0-9] 除数字外的任意字符。^ 放在类开头表示排除(补集类)
[[a-z]--[aeiouy]] 小写辅音字母。类可以包含类,-- 排除其后的内容
[\w--[\d_]] 所有字母(含带重音的外文字符)。缩写类可以在类内使用
\d 数字(同 [0-9]
\D 任意非数字字符(同 [^0-9]
\w 字母数字字符(含带重音字符和连字)
\W 任意非"单词"字符
\s 空格、不换行空格、制表符、回车
\S 任意非空白字符
. 除换行外的任意字符。用 "dot all" 复选框或 (?s) 修饰符可包含换行

综合示例:<[^<>]+> 用于选择 HTML 标签。

十三、实战:在 calibre 中使用正则表达式

场景一:转换选项中的「搜索与替换」

转换设置中的「搜索与替换」非常实用:输入一个描述待替换字符串的正则表达式,转换过程中就会执行替换。亮点在于**向导(wizard)**功能:

  1. 点击向导按钮,预览 calibre 在转换过程中"看到"的内容
  2. 滚动到想删除的字符串处,选中并复制
  3. 粘贴到窗口顶部的正则字段中
  4. 如果有可变部分(如页码),用字符集和量词覆盖;别忘了转义可能存在的特殊字符
  5. 点击 Test 按钮,calibre 会高亮显示使用该正则时会替换的部分
  6. 确认无误后点 OK 并开始转换

src/calibre/gui2/convert/search_and_replace.pySearchAndReplaceWidget 中,可以看到搜索框提供了实时的文档更新连接(doc_update 信号),规则以"搜索正则表达式 / 替换文本"两列表格管理,支持保存/加载 .csr 定义文件。

真实案例:转换源文件中可能夹带这种垃圾标签:

Maybe, but the cops feel like you do, Anita. What's one more dead vampire?
New laws don't change that. </p>
<p class="calibre4"> <b class="calibre2">Generated by ABC Amber LIT Conv
<a href="http://www.processtext.com/abclit.html" class="calibre3">erter,
http://www.processtext.com/abclit.html</a></b></p>
<p class="calibre4"> It had only been two years since Addison v. Clark.

要清理掉其中一些标签:开标签用 <b.*?>,闭标签用 </b>,于是可用 <b.*?>.*?</b> 删除两个标签之间的所有内容。但这样会误删 <b> 标签包围的一切正文(<b> 渲染为粗体)。更稳妥的做法是把被包围字符串的开头也纳入表达式:

<b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b>

\s 加量词替代显式空格,以覆盖字符串可能出现的各种空白变体。务必用 Test 检查 calibre 将删除的内容——只检查一处匹配可能遗漏文本中其他位置的不匹配情况。另外,即使意外删多了或删少了标签,calibre 也会在删除后尝试修复受损的代码。

从源码看,转换搜索与替换的正则编译由 src/calibre/ebooks/conversion/search_replace.py 中的 compile_regular_expression() 完成,使用 Python 的 regex 库,默认标志为 regex.VERSION1 | regex.WORD | regex.FULLCASE | regex.MULTILINE | regex.UNICODE,并带缓存机制。

场景二:从文件名提取元数据(添加书籍)

在"添加书籍"设置中,可以用正则从文件名提取元数据,特殊之处在于可以使用元数据字段名:(?P<title>) 表示将字符串这一部分用作书名。允许的字段名会列在窗口中,旁边还有一个测试字段。

示例:要批量导入如下命名的文件:

  • Classical Texts: The Divine Comedy by Dante Alighieri.mobi
  • Science Fiction epics: The Foundation Trilogy by Isaac Asimov.epub

calibre 默认的文件名元数据提取表达式是 (?P<title>.+) - (?P<author>[^_]+),对这种命名方案无能为力。适用于此场景的正则:

[a-zA-Z]+: (?P<title>.+) by (?P<author>.+)

注意:字段名分组内,需要用表达式描述该字段实际匹配的内容。另外,使用测试字段时必须给测试文件名加上文件扩展名,否则即便表达式正确也得不到任何匹配。

该默认模式在 src/calibre/ebooks/metadata/meta.py 中可见((?P<title>.+) - (?P<author>[^_]+)),它由 calibre 的元数据自动识别逻辑使用,与 regex 库配合(regex.UNICODE | regex.VERSION1 | regex.FULLCASE 等标志)。

场景三:批量编辑元数据中的搜索与替换

最后一个场景是元数据字段中的正则「搜索与替换」:在书库中选中多本书,使用批量元数据编辑进入。使用此功能时务必极其小心,它可能对你的书库造成严重后果(Very Bad Things)! 请先用测试字段反复验证表达式符合预期,并且只勾选真正想修改的书籍!

在正则搜索模式下,你可以在一个字段中搜索、替换文本,甚至可以把结果写入另一个字段。经典案例:书库中有 Frank Herbert 的沙丘系列,书名形如 Dune 1 - DuneDune 2 - Dune Messiah……想把 Dune 提取到系列(series)字段:

  1. 在标题字段搜索 (.*?) \d+ - .*,替换为 \1结果写入 series 字段——\1 就是对第一个分组的引用,替换的是系列字段
  2. 再在标题字段搜索 .*? -,替换为空字符串 "",标题字段本身——元数据就清爽了

除了整体替换字段,还可以选择**追加(append)或前置(prepend)**到字段内容,例如想在书名前加上系列信息,也可以做到。这个界面上还有一个「Case sensitive」复选框,因此在批量编辑中不必用标志位来切换大小写行为。

十四、写在最后:测试为王

这篇简明介绍到此告一段落。希望已经让你入门,并能自行继续学习——好的起点是 Python 的 re 模块文档(calibre 实际使用的正则库是 Python 的 regex 库,它相对标准库支持若干有用的增强特性,例如 \K、递归、原子分组与分支重置等,这也是上文中 (*SKIP)(*FAIL)(?R) 等高级语法可用的原因)。

最后一句忠告:正则功能强大,但也极易出错。calibre 提供了非常好的测试手段来验证表达式是否符合预期——请善用它们。多测试、多检查、逐步构造,尽量避免"搬起石头砸自己的脚"。

十五、更多资源

致谢

感谢 ldolse、kovidgoyal、chaley、dwanthny、kacir、Starson17、Orpheu 等社区成员为本教程提供的技巧、更正与帮助。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
936
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
4.02 K
1.03 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
400
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.07 K
538