大多数文本文件编辑工具在查找替换时都可以使用正则表达式,但无法像在使用Python的re库时那样使用正则表达式函数完成替换。作为经常使用Calibre编辑EPUB书籍的用户,有一次,在制作Anki笔记导入文件时,我突发奇想,何不把文本文件导入Calibre电子书编辑器,不就可以在处理文本文件时使用正则表达式函数了吗?立即付诸行动:
双击Calibre套件中的"ebook-edit.exe",然后:

试着编写了两个正则表达式函数。第一个用于检查题目的编号与行号是否一致,从而判断是否成功让每一题占据了一行(处理后的文件每个字段均用西文双引号包裹,字段之间使用西文逗号分隔):
python
# 查找栏填写(?s)^.+,光标定位在文档开头或者先按Ctrl+A,点击查找,确保匹配了全文,再按替换
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
import re
full_text = match.group(0) # 取得文件全部文本
# 按换行切,拿到真实行列表
lines = full_text.splitlines()
err = ''
for lineno, line in enumerate(lines, start=1):
if line.startswith('"'):
line = line[1:] # 如果行首已经添加了引号则剔除
serial = re.search(r'^\d+', line).group(0)
if serial != str(lineno):
err = f"行{lineno}出错: {line[:8]},可搜索{lineno}进行修改"
break #只要某一行序号与行号对不上,后面的就全部对不上,因此,找到第一个对不上的就可以终止并更正了
if err == '':
print('行号全部正确')
else:
print(err)
return full_text #实际上没有替换任何字符
第二个正则表达式函数用于检查处理后的文件每行的字段数量是否与Anki模板的字段数量完全一致(新版Anki在文本文件的字段数量与模板中的字段数量不一致时会丢弃多余字段或者用空字符串补充缺失字段而不是报错,做这个检查可以避免出现非预期的导入结果):
python
# 查找栏填写^.+$,点击替换所有
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
import csv
from io import StringIO
sep = ',' # 字段分隔符
fields_count = 5 # 预期字段数
preview_chars = 8 # 出错信息中截取的行首字符数量
# 持久化保存行号及对应行的出错信息,无错误则信息为None
if 'errs' not in data:
data['errs']=[]
data['errs'].append({'lineno': 0, 'msg': None})
if match is None :
error_list = []
for err_info in data['errs']:
if err_info['msg'] is not None:
error_list.append(f"行{err_info['lineno']}出错:{err_info['msg']}")
if error_list:
print("\n".join(error_list))
else:
print('恭喜,所有行字段数量全部正确!')
return
else:
line = match.group(0)
lineno = data['errs'][-1]['lineno'] + 1
reader = csv.reader(StringIO(line), delimiter=sep)
for r in reader:
if len(r) != fields_count:
msg = f"{line[:preview_chars]}...,预期字段数量{fields_count},实际字段数量{len(r)}"
data['errs'].append({'lineno': lineno, 'msg': msg})
else:
data['errs'].append({'lineno': lineno, 'msg': None})
return line
# 开启全部匹配结束后额外调用一次 replace(match=None),输出相关结果
replace.call_after_last_match = True
当然,第二个函数还有像第一个那样一次性获取整个文档文本后再处理的写法(看起来代码更精炼):
python
# 查找栏填写(?s)^.+,光标定位在文档开头或者先按Ctrl+A,点击查找,确保匹配了全文,再按替换
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
import csv
from io import StringIO
if match is None :return
full_text = match.group(0)
# 按换行切,拿到真实行列表
lines = full_text.splitlines()
sep = ',' # 字段分隔符
fields_count = 5 # 预期字段数
preview_chars = 8
err = []
for lineno, line in enumerate(lines, start=1):
r = next(csv.reader([line], delimiter=sep))
if len(r) != fields_count:
err.append(f"行{lineno}: {line[:8]}...,预期字段数量{fields_count},实际字段数量{len(r)}")
if len(err) > 0:
print(f"❌ 共发现{len(err)}行字段数量错误:")
print("\n".join(err))
else:
print("✔ 恭喜!所有笔记字段数量均正确!")
return full_text
第一个函数也有每次读取一行进行处理的写法,这里就省略了。一次获取文档全部内容的写法在操作上要确保先用查找匹配到整个文件范围,再点击替换,每次读取一行的写法则无需这个前提条件,直接点击"替换所有"按钮即可。
关于Calibre编辑器中正则表达式函数更详细的知识参阅:Calibre编辑器中的函数模式搜索替换技术详解-CSDN博客