エディタの検索と置換用の関数モード¶
エディタの 検索 & 置換 ツールは 関数モード をサポートしています。このモードでは、正規表現 (calibre で正規表現を利用するにあたって重要なこと を参照) と任意の強力な Python 関数を組み合わせて利用することで、あらゆる種類の高度なテキスト処理を行えます。
検索と置換のための 正規表現 モードでは、検索のための正規表現とあわせて見つかったマッチをすべて置換するために使用するテンプレートの両方を指定します。関数モードでは、固定のテンプレートを使用する代わりに、 Python プログラミング言語 の任意の関数を指定します。これにより単純なテンプレートでは不可能だった多くのことを実行できるようになります。
関数モードを使用するテクニックと構文を、例を用いて説明します。どのように関数を作成するかを、実行するタスクを少しずつ複雑にしながら説明します。
ドキュメントの見出しの大文字と小文字を自動修正¶
ここではエディタのビルトイン関数を利用して、見出しタグ内のすべてのテキストの大文字と小文字を自動的にタイトルケースに変更します:
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 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 &
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 ファイルに実行する際にファイル順を選択 を参照してください。
目次の自動生成¶
最後に、もう少し大がかりなことを試してみましょう。本の中に 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
に貼り付けて利用できます。
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_order
は spine
と spine-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 電子書籍エディタフォーラム にあります。