편집기에서의 찾아 바꾸기 함수 모드¶
편집기의 찾아 바꾸기 도구는 *함수 모드*를 지원합니다. 이 모드에서는 정규 표현식(calibre에서 정규 표현식 사용하기 참조)을 임의로 강력한 Python 함수와 결합하여 다양한 고급 텍스트 처리를 수행할 수 있습니다.
표준 regexp 모드에서는 검색할 정규 표현식과 발견된 모든 일치 항목을 대체하는 데 사용할 템플릿을 모두 지정합니다. 함수 모드에서는 고정 템플릿 대신 `Python 프로그래밍 언어 <https://docs.python.org>`_로 임의의 함수를 지정합니다. 이를 통해 단순한 템플릿으로는 불가능한 많은 작업을 수행할 수 있습니다.
함수 모드를 사용하는 기술과 구문은 점점 더 복잡한 작업을 수행하는 함수를 만드는 방법을 보여주는 예제를 통해 설명됩니다.
문서의 머리글 대소문자 자동 수정¶
여기에서는 편집기의 내장 함수 중 하나를 활용하여 머리글 태그 안의 모든 텍스트 대소문자를 제목 대소문자로 자동 변경합니다:
Find expression: <([Hh][1-6])[^>]*>.+?</\1>
함수로 제목 대소문자 텍스트 (태그 무시) 내장 함수를 선택하기만 하면 됩니다. 이렇게 하면 ``<h1>some titLE</h1>``과 같은 제목이 ``<h1>Some Title</h1>``로 변경됩니다. 머리글 태그 안에 다른 HTML 태그가 있어도 작동합니다.
첫 번째 사용자 지정 함수 - 하이픈 스마트 변환¶
함수 모드의 진정한 힘은 임의의 방식으로 텍스트를 처리하는 자신만의 함수를 만들 수 있다는 것입니다. 편집기의 스마트 문장 부호 도구는 개별 하이픈을 그대로 두므로, 이 함수를 사용하여 하이픈을 엄선으로 대체할 수 있습니다.
새 함수를 만들려면 만들기/편집하기 버튼을 클릭하여 새 함수를 만들고 아래의 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 &
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``를 사용하는 것입니다. 이는 편집기 자체가 책의 텍스트 철자 검사에 사용하는 사전을 나타냅니다. 이 함수는 하이픈으로 분리된 단어를 찾아 하이픈을 제거하고 사전이 합성어를 인식하는지 확인합니다. 인식하면 원래 단어가 하이픈 없는 합성어로 대체됩니다.
이 기술의 한 가지 제한 사항은 기본적으로 ``dictionaries.recognized()``가 책의 주 언어를 사용하므로 단일 언어 책에서만 작동한다는 점입니다.
섹션 자동 번호 매기기¶
이제 약간 다른 것을 살펴보겠습니다. HTML 파일에 여러 섹션이 있고 각 섹션에 <h2>Some text</h2>`와 같은 :code:`<h2> 태그의 머리글이 있다고 가정합니다. 이러한 머리글에 연속적인 섹션 번호를 자동으로 매기는 사용자 지정 함수를 만들어 :code:`<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>)
커서를 파일 상단에 놓고 :guilabel:`모두 바꾸기`를 클릭하세요.
이 함수는 replace()``의 또 다른 유용한 추가 인수인 ``number 인수를 사용합니다. :guilabel:`모두 바꾸기`를 수행할 때 number는 각 연속 일치 항목에 대해 자동으로 증가합니다.
또 다른 새로운 기능은 ``replace.file_order``의 사용입니다. 이를 ``’spine’``으로 설정하면 여러 HTML 파일에서 이 검색이 실행될 때 파일이 책에 나타나는 순서대로 처리됩니다. 자세한 내용은 :ref:`file_order_replace_all`을 참조하세요.
목차 자동 생성¶
마지막으로 좀 더 도전적인 것을 시도해 보겠습니다. 책에 <h1 id="someid">Some Text</h1>``처럼 보이는 ``h1 및 h2 태그의 머리글이 있다고 가정합니다. 이러한 머리글을 기반으로 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=['"]([^'"]+)['"][^<>]*>([^<>]+)
:guilabel:`모든 텍스트 파일`에서 검색을 실행하면 검색이 끝날 때 “함수의 디버그 출력” 창이 팝업되며, 여기에 :file:`toc.html`에 붙여넣을 준비가 된 HTML 목차가 포함됩니다.
위 함수는 주석이 많이 달려 있어 따라하기 쉬울 것입니다. 핵심적인 새로운 기능은 replace() 함수의 또 다른 유용한 추가 인수인 data 객체의 사용입니다. data 객체는 단일 모두 바꾸기 작업 동안 ``replace()``의 모든 연속 호출 사이에 지속되는 Python *딕셔너리*입니다.
또 다른 새로운 기능은 call_after_last_match``의 사용입니다. ``replace() 함수에서 이를 ``True``로 설정하면 편집기가 모든 일치 항목을 찾은 후 ``replace()``를 한 번 더 호출합니다. 이 추가 호출에서 match 객체는 ``None``이 됩니다.
이것은 함수 모드의 강력함을 보여주기 위한 데모일 뿐입니다. 실제로 책의 머리글에서 목차를 생성해야 한다면 :guilabel:`도구->목차`의 전용 목차 도구를 사용하는 것이 더 좋습니다.
함수 모드의 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 인수는 현재 일치 항목의 번호입니다. :guilabel:`모두 바꾸기`를 실행하면 각 연속 일치 항목에 대해 ``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 dictionary``입니다. :guilabel:`모두 바꾸기`를 실행하면 각 연속 일치 항목에 대해 동일한 ``dictionary``가 data로 ``replace()``가 호출됩니다. 따라서 :guilabel:`모두 바꾸기` 작업 동안 ``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']
...
함수 객체를 사용하여 영속적인 데이터를 저장하고 다른 함수에서 재사용할 수도 있습니다. 예를 들어, :guilabel:`모두 바꾸기`와 함께 실행될 때 일부 데이터를 수집하는 하나의 함수와 나중에 실행될 때 해당 데이터를 사용하는 another 함수를 만들 수 있습니다. 다음 두 함수를 고려하세요:
# 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 두 값을 받으며, 각각 책에 나타나는 순서대로 또는 역순으로 여러 파일을 처리하도록 합니다.
마지막 일치 항목을 찾은 후 함수를 한 번 더 호출하기¶
때때로 위의 목차 자동 생성 예시처럼 마지막 일치 항목을 찾은 후 함수를 한 번 더 호출하는 것이 유용합니다. 함수에서 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 전자책 편집기 포럼 <https://www.mobileread.com/forums/showthread.php?t=237181>`_에서 확인할 수 있습니다.
