エディタの検索と置換用の関数モード

エディタの 検索 & 置換 ツールは 関数モード をサポートしています。このモードでは、正規表現 (calibre で正規表現を利用するにあたって重要なこと を参照) と任意の強力な Python 関数を組み合わせて利用することで、あらゆる種類の高度なテキスト処理を行えます。

検索と置換のための 正規表現 モードでは、検索のための正規表現とあわせて見つかったマッチをすべて置換するために使用するテンプレートの両方を指定します。関数モードでは、固定のテンプレートを使用する代わりに、 Python プログラミング言語 の任意の関数を指定します。これにより単純なテンプレートでは不可能だった多くのことを実行できるようになります。

関数モードを使用するテクニックと構文を、例を用いて説明します。どのように関数を作成するかを、実行するタスクを少しずつ複雑にしながら説明します。

関数モード

ドキュメントの見出しの大文字と小文字を自動修正

ここではエディタのビルトイン関数を利用して、見出しタグ内のすべてのテキストの大文字と小文字を自動的にタイトルケースに変更します:

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

関数にはビルトイン関数 Title-case text (ignore tags) を選択するだけです。これは <h1>some TITLE</h1> のようなタイトルを <h1>Some Title</h1> に変更します。これは見出しタグの内側に別のタグがあっても動作します。

最初のカスタム関数 - ハイフンをスマート化

関数モードの真の力は、テキストを任意の方法で処理する独自の関数を作成できる点にあります。エディタの Smarten Punctuation ツールは個々のハイフンをそのままにしておくため、このツールを使用して em-dash に置換できます。

新しい関数を作成するには、作成/編集 ボタンをクリックして新しい関数を作成し、下の 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() は、マッチしたテキストをすべて返します。そのテキスト内のハイフンを em-dash に置換すればよいのですが、その際にまずダブルハイフンを置換してから、次にシングルハイフンを置換します。

この関数を次の検索正規表現とともに使用してください:

>[^<>]+<

そしてこれはすべてのハイフンを em-dash に置換しますが、置換するのは実際のテキストだけで、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>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 を使用しています。すべて置換 を実行するとき、number はマッチが成功するたびに自動的に数が増やされます。

もうひとつの新しい機能は、replace.file_order の利用です。これを 'spine' に設定すると、この関数を複数の HTML ファイルに対して実行したときに、本に表示される順番でファイルを処理します。詳しくは 複数の HTML ファイルに実行する際にファイル順を選択 を参照してください。

目次の自動生成

最後に、もう少し大がかりなことを試してみましょう。本の中に h1h2 タグの見出しがあり、<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 に貼り付けて利用できます。

The function above is heavily commented, so it should be easy to follow. The key new feature is the use of another useful extra argument to the replace() function, the data object. The data object is a Python dictionary that persists between all successive invocations of replace() during a single Replace All operation.

もうひとつの新機能は 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 クラスのオブジェクトです。便利な属性には、title, authors (著者のリスト) および language (言語コード) があります。

引数 dictionaries

現在の本のスペルチェックに使われた辞書のコレクションを表します。その最も便利なメソッドは dictionaries.recognized(word)` で、渡された単語が現在の本の言語用の辞書で認識さたら True` を返します。

引数 data

This a simple Python dictionary. When you run Replace all, every successive match will cause replace() to be called with the same dictionary as data. You can thus use it to store arbitrary data between invocations of replace() during a Replace all operation.

引数 functions

引数 functions を使うと他のすべてのユーザ定義関数にアクセスできます。コードの再利用に役立ちます。ユーティリティ関数を 1 か所で定義して、他のすべての関数からそれを再利用することができます。たとえば 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 オブジェクトを使って、他の関数から再利用できる永続データを格納することもできます。たとえば すべて置換 で実行されたときにデータを収集し、後で別の関数が実行されたときにそれを使用するといったことが可能です。次の 2 つの関数のようなものを考えてみてください:

# 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 の出力は検索/置換が完了した後にポップアップウィンドウに表示されます。print() の使用例は、上で目次全体を出力する説明にあります。

複数の HTML ファイルに実行する際にファイル順を選択

すべて置換 を複数の HTML に対して実行するとき、ファイルが処理される順番は編集のために開いているファイルによって異なります。次のように、関数に file_order 属性を設定すると、検索がファイルを設定された順に処理するよう強制できます。

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

replace.file_order = 'spine'

file_orderspinespine-reverse という 2 つの値を受け取り、検索が複数ファイルを処理するのに本の中に現れた順で、それぞれ降順または昇順に処理するようにします。

最後のマッチが見つかった後にもう一度関数を呼び出させる

上で説明した目次の自動生成の例のように、最後のマッチが見つかった後に関数をもう一度呼び出させると便利な場合があります。これを行うには、次のように、関数の 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 電子書籍エディタフォーラム にあります。