calibre 正则表达式完全指南:从入门到精通(附带源码级原理解析)
正则表达式(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 规则文件等功能。
三、正则表达式到底是什么?
一个正则表达式是描述一组字符串的方式。单个正则表达式可以匹配多种不同的字符串——这正是它强大的原因:它是描述海量变体的一种简洁表达。
关于"字符串"的说明:这里沿用编程语言中的含义,即一个或多个字符的序列,包括文字、数字、标点以及所谓"空白字符"(换行、制表符等)。请注意,通常大写与小写字符被视为不同字符,例如
a与A是两个不同的字符。在 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)**功能:
- 点击向导按钮,预览 calibre 在转换过程中"看到"的内容
- 滚动到想删除的字符串处,选中并复制
- 粘贴到窗口顶部的正则字段中
- 如果有可变部分(如页码),用字符集和量词覆盖;别忘了转义可能存在的特殊字符
- 点击 Test 按钮,calibre 会高亮显示使用该正则时会替换的部分
- 确认无误后点 OK 并开始转换
在 src/calibre/gui2/convert/search_and_replace.py 的 SearchAndReplaceWidget 中,可以看到搜索框提供了实时的文档更新连接(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.mobiScience 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 - Dune、Dune 2 - Dune Messiah……想把 Dune 提取到系列(series)字段:
- 在标题字段搜索
(.*?) \d+ - .*,替换为\1,结果写入 series 字段——\1就是对第一个分组的引用,替换的是系列字段 - 再在标题字段搜索
.*? -,替换为空字符串"",标题字段本身——元数据就清爽了
除了整体替换字段,还可以选择**追加(append)或前置(prepend)**到字段内容,例如想在书名前加上系列信息,也可以做到。这个界面上还有一个「Case sensitive」复选框,因此在批量编辑中不必用标志位来切换大小写行为。
十四、写在最后:测试为王
这篇简明介绍到此告一段落。希望已经让你入门,并能自行继续学习——好的起点是 Python 的 re 模块文档(calibre 实际使用的正则库是 Python 的 regex 库,它相对标准库支持若干有用的增强特性,例如 \K、递归、原子分组与分支重置等,这也是上文中 (*SKIP)(*FAIL)、(?R) 等高级语法可用的原因)。
最后一句忠告:正则功能强大,但也极易出错。calibre 提供了非常好的测试手段来验证表达式是否符合预期——请善用它们。多测试、多检查、逐步构造,尽量避免"搬起石头砸自己的脚"。
十五、更多资源
- 完整正则语法快速参考表:manual/regexp_quick_reference.rst
- 转换「搜索与替换」界面实现:src/calibre/gui2/convert/search_and_replace.py
- 正则编译与缓存实现:src/calibre/ebooks/conversion/search_replace.py
- 文件名元数据提取默认模式:src/calibre/ebooks/metadata/meta.py
致谢
感谢 ldolse、kovidgoyal、chaley、dwanthny、kacir、Starson17、Orpheu 等社区成员为本教程提供的技巧、更正与帮助。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.22 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python400
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python48367
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go20843
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java34351