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

У інструменті пошуку із заміною редактора передбачено підтримку функціонального режиму. У цьому режимі ви можете поєднувати формальні вирази (див. Все про використання формальних виразів в calibre) із довільними потужними функціями Python для виконання усіх типів розширеної обробки тексту.

У стандартному режимі формальних виразів для пошуку з заміною ви вказуєте формальний вираз для пошуку і шаблон, який буде використано для заміни усіх відповідників. У функціональному режимі, замість використання фіксованого шаблона, ви вказуєте довільну функцію, яку написано мовою програмування Python. Це надає вам змогу виконувати багато завдань, які неможливо виконати за допомогою простих шаблонів.

Методики використання функціонального режиму та синтаксис буде описано у форматі прикладів, які продемонструють вам, як створити функції, які виконуватимуть усе більш складні завдання.

Режим функцій

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

Тут ми підсилимо одну із вбудованих функцій редактора, реалізувавши автоматичну заміну регістру символів усього тексту у теґах заголовків на регістр заголовків:

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

Для цієї функції просто виберіть вбудовану функцію Title-case text (ignore tags). Ця функція змінює заголовки ось так: з <h1>якийсь ЗАГОЛОВОК</h1> виходить <h1>Якийсь Заголовок</h1>. Вона працюватиме, навіть якщо у теґах заголовка місяться інші теґи HTML.

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

Справжня потужність функціонального режиму полягає у можливості створення власних функцій для довільної обробки тексту. Інструмент кмітливої пунктуації у редакторі не чіпає окремих дефісів, отже ви можете скористатися цією функцією для заміни їх на тире.

Щоб створити нову функцію, просто натисніть кнопку Створити/Редагувати для створення нової функції і скопіюйте код Python, наведений нижче.

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

Кожна нетипова функція пошуку з заміною повинна мати неповторну назву і складатися з функції python із назвою replace, яка приймає усі аргументи, вказані вище. Для початку, пропустимо усі різні аргументи функції replace(). Зосередимо нашу увагу на аргументі match. Він представляє відповідник рядка пошуку під час пошуку з заміною. Повну документацію щодо нього можна знайти тут. 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

Скористайтеся цією функцією із тим самим виразом для пошуку, що і раніше, а саме:

>[^<>]+<

І вона чарівним чином виправить усі слова із помилковими дефісами у тексті книги. Основним трюком тут є використання одного з корисних аргументів функції replace, а саме аргументу dictionaries. Цей аргумент посилається на словники, які редактор використовує для перевірки правопису тексту книги. Ця функція шукає слова, які відокремлено дефісом, вилучає дефіс і перевіряє, чи розпізнається словником з’єднане слово. Якщо слово розпізнається, початкові слова буде замінено об’єднаним словом без дефіса.

Зауважте, що одним із обмежень цієї методики є те, що вона працюватиме лише для одномовних книг, оскільки, типово, dictionaries.recognized() використовує основну мову книги.

Автоматична нумерація розділів

Тепер ми розглянемо дещо інший приклад. Припустімо, що у вашому файлі HTML багато розділів, кожен з яких має заголовок у тезі <h2>, який виглядає так: <h2>Якийсь текст</h2>. Ви можете створити нетипову функцію, яка автоматично нумеруватиме такі заголовки послідовними номерами розділів, щоб виходило ось таке: <h2>1. Якийсь текст</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="якийсь_ідентифікатор">Якийсь Текст</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, який зберігається протягом усіх послідовних викликів replace() під час виконання однієї дії Замінити всі.

Ще однією новою можливістю є використання call_after_last_match — встановлення для цієї змінної значення True у функції replace() означає, що редактор викликатиме replace() додатковий раз після знайдення усіх відповідників. Для цього додаткового виклику об’єктом-відповідником буде None.

Тут наведено лише приклади потужності функціонального режиму. Якщо вам дійсно потрібно створити таблицю змісту на основі заголовків у вашій книзі, вам варто скористатися спеціалізованим інструментом для створення таблиць змісту, а саме, пунктом меню Інструменти  →  Зміст.

Програмний інтерфейс функціонального режиму

Усі функції режиму функцій мають бути функціями Python із назвою replace, з таким підписом:

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

Під час виконання дії із пошуку з заміною для кожного знайденого відповідника викликатиметься функція replace() і має бути повернуто замінений рядок для кожного відповідника. Якщо замін не виконуватиметься, має бути повернуто match.group(), яким є початковий рядок. Документацію з різноманітних аргументів функції replace() наведено нижче.

Аргумент «match»

Аргумент match представляє поточний знайдений відповідник. Це об’єкт Match Python. Його найкориснішим методом є group(), метод, яким можна скористатися для отримання відповідного тексту для окремих груп захоплення під час пошуку за формальним виразом.

Аргумент «number»

Аргумент number є номером поточного відповідника. Коли ви наказуєте програмі Замінити усе, виявлення кожного наступного відповідника призводитиме до того, що replace() викликатиметься зі збільшеним числом. Перший відповідник матиме номер 1.

Аргумент «file_name»

Це назва файла, у якому було знайдено поточний відповідник. Якщо пошук виконується у позначеному фрагменті тексту, значення file_name є порожнім. Значення file_name записується у канонічній формі, як шлях відносно кореневої теки книги. з використанням роздільника елементів шляху /.

Аргумент «metadata»

Є метаданими поточної книги, зокрема даними щодо назви, авторів, мови тощо. Є об’єктом класу calibre.ebooks.metadata.book.base.Metadata. Серед особливо корисних атрибутів, title (назва або заголовок книги), authors (список авторів) та language (код мови).

Аргумент «dictionaries»

Збірка словників, які використовуються для перевірки правопису у поточній книзі. Найкориснішим методом є dictionaries.recognized(слово). Цей метод повертає 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']
    ...

Ви також можете скористатися об’єктом functions для зберігання даних, які може бути повторно використано іншими функціями. Наприклад, ви можете створити функцію, яка під час запуску за допомогою кнопки Замінити всі збирає певні дані, і іншу функцію, яка використовує ці зібрані дані під час запуску. Розгляньмо такі дві функції:

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