编辑器中搜索和替换功能

编辑器中的“搜索和替换”工具支持*功能模式*。 在此模式下,您可以将正则表达式(请参阅“regexp”)与任意强大的 Python 函数结合起来,以执行各种高级文本处理。

在搜索和替换的标准 regexp 模式中,您指定要搜索的正则表达式以及用于替换所有找到的匹配项的模板。 在函数模式下,您不使用固定模板,而是在“Python 编程语言 <https://docs.python.org>”中指定任意函数。 这使您可以完成许多使用简单模板无法完成的事情。

将通过示例描述使用函数模式和语法的技术,向您展示如何创建函数来执行逐渐复杂的任务。

功能模式

自动修复文档中标题的大小写

在这里,我们将利用编辑器中的内置函数之一自动将标题标签内所有文本的大小写更改为标题大小写:

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

For the function, simply choose the Title-case text (ignore tags) builtin function. The will change titles that look like: <h1>some titLE</h1> to <h1>Some Title</h1>. It will work even if there are other HTML tags inside the heading tags.

您的第一个自定义函数 - 智能连字符

函数模式的真正强大之处在于能够创建自己的函数以任意方式处理文本。 编辑器中的 Smarten 标点符号工具会保留单个连字符,因此您可以使用此功能将它们替换为破折号。

要创建新函数,只需单击“创建/编辑”按钮即可创建新函数并复制下面的 Python 代码。

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    return match.group().replace('--', '—').replace('-', '—')

每个“搜索和替换”自定义函数都必须有一个唯一的名称,并由一个名为“replace”的 Python 函数组成,该函数接受上面显示的所有参数。 目前,我们不必担心“replace()”函数的所有不同参数。 只需关注“match”参数即可。 它表示运行搜索和替换时的匹配。 其完整文档位于“此处 <https://docs.python.org/library/re.html#match-objects>”_。 match.group() 只是返回所有匹配的文本,我们所做的就是用破折号替换该文本中的连字符,首先替换双连字符,然后替换单连字符。

将此函数与查找正则表达式一起使用:

>[^<>]+<

它将用破折号替换所有 连字符,但仅限于实际文本中,而不是 HTML 标记定义内。

功能模式的力量 - 使用拼写词典来修复错误连字符的单词

通常,通过扫描印刷书籍创建的电子书包含错误连字符的单词,即在印刷页面上的行尾处分开的单词。 我们将编写一个简单的函数来自动查找并修复这些单词。

import regex
from calibre import replace_entities
from calibre import prepare_string_for_xml

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):

    def replace_word(wmatch):
        # Try to remove the hyphen and replace the words if the resulting
        # hyphen free word is recognized by the dictionary
        without_hyphen = wmatch.group(1) + wmatch.group(2)
        if dictionaries.recognized(without_hyphen):
            return without_hyphen
        return wmatch.group()

    # Search for words split by a hyphen
    text = replace_entities(match.group()[1:-1])  # Handle HTML entities like &amp;
    corrected = regex.sub(r'(\w+)\s*-\s*(\w+)', replace_word, text, flags=regex.VERSION1 | regex.UNICODE)
    return '>%s<' % prepare_string_for_xml(corrected)  # Put back required entities

使用与之前相同的查找表达式来使用此函数,即:

>[^<>]+<

它会神奇地修复书中所有错误连字符的单词。 主要技巧是使用替换函数的有用的额外参数之一“字典”。 这是指编辑器本身用来对书中文本进行拼写检查的词典。 该函数的作用是查找由连字符分隔的单词,删除连字符并检查字典是否识别该复合词,如果是,则将原始单词替换为无连字符的复合词。

请注意,此技术的一个限制是它仅适用于单语言书籍,因为默认情况下,“dictionaries.recognized()”使用书籍的主要语言。

自动编号章节

现在我们会看到一些不同的东西。 假设您的 HTML 文件有许多部分,每个部分都有一个位于“<h2>”标记中的标题,看起来像“<h2>Some text</h2>”。 您可以创建一个自定义函数,自动使用连续的节号对这些标题进行编号,使它们看起来像“<h2> 1. Some text </h2>”。

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    section_number = '%d. ' % number
    return match.group(1) + section_number + match.group(2)

# Ensure that when running over multiple files, the files are processed
# in the order in which they appear in the book
replace.file_order = 'spine'

将其与查找表达式一起使用:

(?s)(<h2[^<>]*>)(.+?</h2>)

将光标置于文件顶部,然后单击“全部替换”。

该函数使用了另一个有用的额外参数“replace()”:“number”参数。 当执行“全部替换”时,每个连续匹配的数字都会自动增加。

另一个新功能是使用“replace.file_order”——将其设置为“'spine”意味着如果在多个 HTML 文件上运行此搜索,则文件将按照它们出现的顺序进行处理。 这本书。 有关 详细信息,请参阅“file_order_replace_all”。

自动创建目录

最后,让我们尝试一些更雄心勃勃的事情。 假设您的书的“h1”和“h2”标签中的标题看起来像“<h1 id="someid"> Some Text </h1>”。 我们将根据这些标题自动生成 HTML 目录。 创建以下自定义函数:

from calibre import replace_entities
from calibre.ebooks.oeb.polish.toc import TOC, toc_to_html
from calibre.gui2.tweak_book import current_container
from calibre.ebooks.oeb.base import xml2str

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    if match is None:
        # All matches found, output the resulting Table of Contents.
        # The argument metadata is the metadata of the book being edited
        if 'toc' in data:
            toc = data['toc']
            root = TOC()
            for (file_name, tag_name, anchor, text) in toc:
                parent = root.children[-1] if tag_name == 'h2' and root.children else root
                parent.add(text, file_name, anchor)
            toc = toc_to_html(root, current_container(), 'toc.html', 'Table of Contents for ' + metadata.title, metadata.language)
            print(xml2str(toc))
        else:
            print('No headings to build ToC from found')
    else:
        # Add an entry corresponding to this match to the Table of Contents
        if 'toc' not in data:
            # The entries are stored in the data object, which will persist
            # for all invocations of this function during a 'Replace All' operation
            data['toc'] = []
        tag_name, anchor, text = match.group(1), replace_entities(match.group(2)), replace_entities(match.group(3))
        data['toc'].append((file_name, tag_name, anchor, text))
        return match.group()  # We don't want to make any actual changes, so return the original matched text

# Ensure that we are called once after the last match is found so we can
# output the ToC
replace.call_after_last_match = True
# Ensure that when running over multiple files, this function is called,
# the files are processed in the order in which they appear in the book
replace.file_order = 'spine'

并将其与 find 表达式一起使用:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

在“所有文本文件”上运行搜索,在搜索结束时,将弹出一个窗口,其中显示“调试函数的输出”,其中包含 HTML 目录,可以粘贴到“toc.html”中。

上面的函数有很多注释,所以应该很容易理解。 关键的新功能是对“replace()”函数使用另一个有用的额外参数,即“data”对象。 “data” 对象是一个 Python 字典,在单个“全部替换”操作期间,在所有连续调用“replace()”之间持续存在。

另一个新功能是使用“call_after_last_match”——在“replace()”函数上将其设置为“True”意味着编辑器将额外调用一次“replace()” 已找到匹配项。 对于这个额外的调用,匹配对象将为“None”。

这只是一个演示,向您展示功能模式的强大功能,如果您确实需要根据书中的标题生成目录,您最好使用“工具”->“目录”中的专用目录工具 。

功能模式的API

所有函数模式函数必须是名为replace的Python函数,具有以下签名:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    return a_string

当运行查找/替换时,对于找到的每个匹配项,将调用“replace()”函数,它必须返回该匹配项的替换字符串。 如果不进行替换,则应返回原始字符串“match.group()”。 下面记录了“replace()”函数的各种参数。

“match” 参数

“match”参数代表当前找到的匹配。 它是一个`Python Match 对象<https://docs.python.org/library/re.html#match-objects>`_。 它最有用的方法是“group()”,它可用于获取与搜索正则表达式中的各个捕获组相对应的匹配文本。

number 参数

number 参数是当前匹配的编号。 当您运行“全部替换”时,每个连续的匹配都会导致以递增的数字调用“replace()”。 第一场比赛的号码为 1。

“file_name”参数

这是找到当前匹配项的文件的文件名。 在标记文本内搜索时,file_name 为空。 “file_name” 是规范形式,是相对于本书根目录的路径,使用“/” 作为路径分隔符。

“metadata” 参数

这代表当前书籍的元数据,例如标题、作者、语言等。它是`calibre.ebooks.metadata.book.base.Metadata` 类的对象。 有用的属性包括“标题”、“作者”(作者列表)和“语言”(语言代码)。

“dictionaries” 参数

这代表用于对当前书籍进行拼写检查的词典集合。 它最有用的方法是“dictionaries.recognized(word)”,如果传入的单词被当前书籍语言的字典识别,它将返回“True”。

“data” 参数

这是一个简单的 Python“字典”。 当您运行“全部替换”时,每个连续的匹配都会导致使用与数据相同的“字典”来调用“replace()”。 因此,您可以使用它在“全部替换”操作期间调用“replace()”之间存储任意数据。

“functions” 参数

functions 参数使您可以访问所有其他用户定义的函数。 这对于代码重用很有用。 您可以在一处定义实用函数,然后在所有其他函数中重复使用它们。 例如,假设您创建一个函数名称“My Function”,如下所示:

def utility():
   # do something

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

然后,在另一个函数中,您可以像这样访问“utility()”函数:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    utility = functions['My Function']['utility']
    ...

您还可以使用函数对象来存储持久数据,这些数据可以被其他函数重用。 例如,您可以拥有一个函数,当使用“全部替换”运行时会收集一些数据,而另一个函数则在随后运行时使用这些数据。 考虑以下两个函数:

# Function One
persistent_data = {}

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...
    persistent_data['something'] = 'some data'

# Function Two
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    persistent_data = functions['Function One']['persistent_data']
    ...

调试你的函数

您可以使用 Python 中的标准“print()”函数来调试您创建的函数。 查找/替换完成后,打印的输出将显示在弹出窗口中。 您在上面看到了使用“print()”输出整个目录的示例。

在多个 HTML 文件上运行时选择文件顺序

当您对多个 HTML 文件运行“全部替换”时,文件的处理顺序取决于您打开进行编辑的文件。 您可以通过在函数上设置“file_order”属性来强制搜索按文件出现的顺序处理文件,如下所示:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

replace.file_order = 'spine'

file_order 接受两个值,spinespine-reverse ,这两个值会导致搜索按照文件在书中出现的顺序,分别向前或向后,处理多个文件。

在找到最后一个匹配项后额外调用您的函数

有时,如上面的自动生成目录示例所示,在找到最后一个匹配项后额外调用函数会很有用。 您可以通过在函数上设置“call_after_last_match”属性来做到这一点,如下所示:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

replace.call_after_last_match = True

将函数的输出附加到标记的文本

在标记文本上运行搜索和替换时,有时将文本附加到标记文本的末尾很有用。 您可以通过在函数上设置“append_final_output_to_marked”属性来做到这一点(请注意,您还需要设置“call_after_last_match”),如下所示:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...
    return 'some text to append'

replace.call_after_last_match = True
replace.append_final_output_to_marked = True

对标记文本执行搜索时禁止显示结果对话框

您还可以通过在函数上设置“suppress_result_dialog”属性来抑制结果对话框(这会减慢在许多文本块上重复应用搜索/替换的速度),如下所示:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

replace.suppress_result_dialog = True

更多示例

由 calibre 用户贡献的更多有用示例可以在 calibre 电子书编辑器论坛 中找到。