Функциональный режим для поиска и замены в редакторе

Инструмент Search & replace в редакторе поддерживает функциональный режим. В этом режиме вы можете комбинировать регулярные выражения (см. Всё об использования регулярных выражений в calibre) с произвольно мощными функциями Python для выполнения всех видов расширенной обработки текста.

В стандартном режиме regexp для поиска и замены вы указываете как регулярное выражение для поиска, так и шаблон, который используется для замены всех найденных совпадений. В функциональном режиме вместо использования фиксированного шаблона вы указываете произвольную функцию на языке программирования Python. Это позволяет вам делать много вещей, которые невозможны с простыми шаблонами.

Методы использования функционального режима и синтаксиса будут описаны с помощью примеров, показывающих, как создавать функции для выполнения более сложных задач.

Функциональный режим

Автоматическое исправление регистра заголовков в документе

Здесь мы будем использовать одну из встроенных функций в редакторе, чтобы автоматически изменять регистр всего текста внутри тегов заголовка на регистр заголовка:

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

Для функции просто выберите встроенную функцию Заголовок (без тегов). Заголовки будут изменены следующим образом: <h1>some TITLE</h1> на <h1>Some Title</h1>. Это будет работать, даже если внутри заголовочных тегов есть другие HTML-теги.

Ваша первая пользовательская функция - умные дефисы

Настоящая сила режима функций заключается в возможности создавать свои собственные функции для произвольной обработки текста. Инструмент Smarten Punctuation в редакторе оставляет индивидуальные дефисы в отдельности, поэтому вы можете использовать эту функцию, чтобы заменить их на тире.

Чтобы создать новую функцию, просто нажмите кнопку Create/edit, чтобы создать новую функцию и скопировать код Python снизу.

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

Каждая пользовательская функция Search & replace должна иметь уникальное имя и состоять из функции Python с именем replace, которая принимает все аргументы, показанные выше. На данный момент мы не будем беспокоиться о различных аргументах функции 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> теге, который выглядит следующим образом :code: <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-файлам, файлы обрабатываются в том порядке, в котором они отображаются в книга. Смотрите Выберите порядок файлов при работе с несколькими файлами HTML для деталей.

Автоматическое создание оглавления

Наконец, давайте попробуем что-нибудь более амбициозное. Предположим, что ваша книга имеет заголовки в тегах 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'

И использовать это с выражением поиска:

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

Запустите поиск по Все текстовые файлы и в конце поиска появится окно Отладка вывода из вашей функции, которое будет содержать оглавление HTML, готовое для вставки в toc.html.

Вышеприведенная функция сильно прокомментирована, поэтому ей должно быть легко следовать. Ключевой новой возможностью является использование другого полезного дополнительного аргумента для функции replace() объекта data. Объект data - это Python dict, который сохраняется между всеми последовательными вызовами ``replace()``во время одной операции Replace All.

Еще одной новой возможностью является использование call_after_last_match - установка этого параметра в `` True`` в функции ` replace()`` означает, что редактор будет вызывать replace() один раз после всех найденных совпадений. Для этого дополнительного вызова объектом сопоставления будет None.

Это была просто демонстрация мощи функционального режима: если вам действительно нужно сгенерировать оглавление из заголовков в вашей книге, вам лучше использовать специальный инструмент оглавления в :guilabel:`Инструменты->Оглавление.

API для function mode

Все функции function mode должны быть функциями Python с именем replace, со следующей сигнатурой:

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

Когда выполняется поиск/замена, для каждого найденного совпадения вызывается функция replace(), она должна возвращать строку замены для этого совпадения. Если никакие замены не должны быть сделаны, она должна вернуть match.group(), которая является исходной строкой. Различные аргументы функции replace() описаны ниже.

Аргумент match

Аргумент match представляет текущее найденное совпадение. Это объект Python Match. Его наиболее полезным методом является group(), который можно использовать для получения сопоставленного текста, соответствующего отдельным группам захвата в регулярном выражении поиска.

Аргумент number

Аргумент number - это номер текущего совпадения. Когда вы запускаете Replace All, каждое последующее совпадение будет вызывать``replace()`` с возрастающим числом. Первое совпадение имеет номер 1.

Аргумент file_name

Это имя файла, в котором было найдено текущее совпадение. При поиске внутри помеченного текста file_name пусто. file_name находится в канонической форме, пути относительно корня книги, используя / в качестве разделителя пути.

Аргумент metadata

Это представляет метаданные текущей книги, такие как название, авторы, язык и т. д. Это объект класса calibre.ebooks.metadata.book.base.Metadata. Полезные атрибуты включают title, authors (список авторов) и language (код языка).

Аргумент dictionaries

Это представляет собой словари, используемые для проверки орфографии текущей книги. Его наиболее полезным методом является dictionaries.recognized(word), который будет возвращать True, если переданное слово распознается словарём для языка текущей книги.

Аргумент data

Это простой Python dict. Когда вы запускаете Replace all, каждое последующее совпадение будет вызывать``replace()`` с тем же dict, что и для данных. Таким образом, вы можете использовать его для хранения произвольных данных между вызовами replace() во время операции Replace all.

Аргумент 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']
    ...

Вы также можете использовать объект функций для хранения постоянных данных, которые могут быть повторно использованы другими функциями. Например, у вас может быть одна функция, которая при запуске с Replace All собирает некоторые данные, и другая функция, которая использует их при последующем запуске. Рассмотрим следующие две функции:

# 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']
    ...

Отладка ваших функций

Вы можете отлаживать созданные вами функции, используя стандартную функцию print() из Python. Результат печати будет отображаться во всплывающем окне после завершения поиска/замены. Вы видели пример использования print() для вывода всего оглавления выше.

Выберите порядок файлов при работе с несколькими файлами HTML

Когда вы запускаете Заменить все для нескольких файлов HTML, порядок, в котором файлы обрабатываются, зависит от того, какие файлы вы открыли для редактирования. Вы можете заставить поиск обрабатывать файлы в том порядке, в котором они отображаются, установив атрибут file_order в вашей функции, например так:

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

replace.file_order = 'spine'

file_order принимает два значения, spine и spine-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.