Functie modus voor zoek & vervangen in de editor

The Search & replace tool in the editor supports a function mode. In this mode, you can combine regular expressions (see Alles over het gebruik van reguliere expressies in calibre) with arbitrarily powerful Python functions to do all sorts of advanced text processing.

In de standaard regexp modus voor zoeken en vervangen, geeft u zowel een reguliere expressie op om naar te zoeken als een sjabloon, om alle gevonden overeenkomsten te vervangen. In functiemodus geeft u in plaats van een vast sjabloon een willekeurige functie op in de Python programmeertaal. Hiermee kunt u veel dingen doen die niet mogelijk zijn met eenvoudige sjablonen.

Technieken voor het gebruik van de functie modus en de syntaxis worden beschreven aan de hand van voorbeelden, waarin u wordt uitgelegd hoe u functies kunt maken om steeds complexere taken uit te voeren.

De Functie modus

Automatisch het hoofdlettergebruik van titels in documenten corrigeren

Hier zullen we gebruik maken van een van de ingebouwde functies in de editor om automatisch het hoofdlettergebruik van alle tekst binnen heading-tags naar elk woord met hoofdletters te veranderen:

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

For the function, simply choose the Title-case text (ignore tags) builtin function. This 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.

Uw eerste aangepaste functie - koppeltekens slim maken

The real power of function mode comes from being able to create your own functions to process text in arbitrary ways. The Smarten Punctuation tool in the editor leaves individual hyphens alone, so you can use this function to replace them with em-dashes.

Om een nieuwe functie te maken, klik je gewoon op de knop Maken/bewerken om een nieuwe functie te maken en kopieer je de Python-code hieronder.

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

Elke aangepaste functie Zoeken en vervangen moet een unieke naam hebben en bestaan uit een Python-functie met de naam replace, die alle hierboven weergegeven argumenten accepteert. Voorlopig houden we ons niet bezig met alle verschillende argumenten voor de functie replace(). Concentreer je gewoon op het argument match. Dit vertegenwoordigt een overeenkomst bij het uitvoeren van een zoek- en vervangactie. De volledige documentatie is beschikbaar hier. match.group() retourneert eenvoudigweg alle overeenkomende tekst en het enige wat we doen is de koppeltekens in die tekst vervangen door em-streepjes, waarbij we eerst dubbele koppeltekens vervangen en daarna enkele koppeltekens.

Gebruik deze functie met de zoek reguliere expressie:

>[^<>]+<

En het vervangt alle koppeltekens met em-streepjes maar enkel in echte tekst en niet in HTML tag definities.

De kracht van functie modus - een spellingswoordenboek gebruiken om fout koppeltekengebruik te herstellen

E-boeken gecreëerd van scans van gedrukte boeken bevatten dikwijls fout koppeltekengebruik – woorden gesplitst aan het einde van een regel in d gedrukte tekst. We gaan een eenvoudige functie schrijven om zulke woorden automatisch te vinden en herstellen.

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

Gebruik deze functie met dezelfde zoek reguliere expressie als voorheen, namelijk:

>[^<>]+<

En het zal op magische wijze alle verkeerd gespelde woorden in de tekst van het boek corrigeren. De belangrijkste truc is om een van de handige extra argumenten voor de vervangingsfunctie te gebruiken, namelijk “woordenboeken”. Dit verwijst naar de woordenboeken die de editor zelf gebruikt om de tekst in het boek op spelling te controleren. Deze functie zoekt naar woorden die door een koppelteken worden gescheiden, verwijdert het koppelteken en controleert of het woordenboek het samengestelde woord herkent. Als dat het geval is, worden de oorspronkelijke woorden vervangen door het samengestelde woord zonder koppelteken.

Houd er rekening mee dat een beperking van deze techniek is dat deze alleen werkt voor eentalige boeken, omdat dictionaries.recognized() standaard de hoofdtaal van het boek gebruikt.

Automatische nummering secties

Nu gaan we iets anders bekijken. Stel dat uw HTML-bestand veel secties bevat, elk met een koptekst in een <h2> tag die eruitziet als <h2>Enige tekst</h2>. Je kunt een aangepaste functie maken die deze koppen automatisch nummert met opeenvolgende sectienummers, zodat ze eruit zien als <h2>1. Enige tekst</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'

Gebruik het met de find expresse:

(?s)(<h2[^<>]*>)(.+?</h2>)

Plaats de cursor aan het begin van het bestand en klik op Alle vervangen.

Deze functie maakt gebruik van een ander handig extra argument van replace(): het argument number. Bij het uitvoeren van een Replace All wordt het getal automatisch verhoogd voor elke opeenvolgende overeenkomst.

Een andere nieuwe functie is het gebruik van de replace.file_order – instelling die voor 'spine' betekent dat als deze zoekopdracht draait op meerdere HTML files, de bestanden verwerkt worden in de volgorde waarin ze verschijnen in het book. Bekijk Kies bestandsvolgorde bij uitvoeren meerdere HTML bestanden voor details.

Maak automatische inhoudsopgave aan

Finally, let’s try something a little more ambitious. Suppose your book has headings in h1 and h2 tags that look like <h1 id="someid">Some Text</h1>. We will auto-generate an HTML Table of Contents based on these headings. Create the custom function below:

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'

En gebruik het met de find expressie:

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

Voer de zoekopdracht uit op Alle tekstbestanden en aan het einde van de zoekopdracht verschijnt er een venster met “Debug-uitvoer van uw functie” met de HTML-inhoudsopgave, klaar om te worden geplakt in toc.html.

De bovenstaande functie is uitgebreid van commentaar voorzien, dus deze zou gemakkelijk te volgen moeten zijn. De belangrijkste nieuwe functie is het gebruik van een ander nuttig extra argument voor de functie replace(), het object data. Het object data is een Python woordenboek dat blijft bestaan tussen alle opeenvolgende aanroepen van replace() tijdens een enkele Replace All-bewerking.

Een andere nieuwe functie is het gebruik van call_after_last_match – als je dat instelt op True in de functie replace(), betekent dit dat de editor replace() nog een keer extra aanroept nadat alle overeenkomsten zijn gevonden. Voor deze extra aanroep is het overeenkomstobject None.

Dit was slechts een demonstratie om u de kracht van de functiemodus te laten zien. Als u echt een inhoudsopgave wilt genereren op basis van de kopjes in uw boek, kunt u beter de speciale inhoudsopgavetool gebruiken in Tools → Table of Contents.

De API voor de functie modus

Alle functiemodusfuncties moeten Python-functies zijn met de naam replace en de volgende handtekening:

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

Wanneer een zoek-/vervangactie wordt uitgevoerd, wordt voor elke gevonden overeenkomst de functie replace() aangeroepen, die de vervangende tekenreeks voor die overeenkomst moet retourneren. Als er geen vervangingen moeten worden uitgevoerd, moet deze match.group() retourneren, wat de oorspronkelijke tekenreeks is. De verschillende argumenten voor de functie replace() worden hieronder beschreven.

Het argument match

Het argument match vertegenwoordigt de momenteel gevonden overeenkomst. Het is een Python Match-object. De meest bruikbare methode is group(), die kan worden gebruikt om de overeenkomende tekst op te halen die overeenkomt met individuele capture-groepen in de reguliere zoekuitdrukking.

The number argument

Het argument number is het nummer van de huidige overeenkomst. Wanneer u Replace All uitvoert, zal elke opeenvolgende overeenkomst ervoor zorgen dat replace() wordt aangeroepen met een oplopend nummer. De eerste overeenkomst heeft nummer 1.

Het file_name argument

Dit is de bestandsnaam van het bestand waarin de huidige overeenkomst is gevonden. Bij het zoeken in gemarkeerde tekst is de bestandsnaam leeg. De bestandsnaam is in canonieke vorm, een pad ten opzichte van de root van het boek, waarbij / als padscheidingsteken wordt gebruikt.

Het metadata argument

Dit vertegenwoordigt de metadata van het huidige boek, zoals titel, auteurs, taal, enz. Het is een object van klasse calibre.ebooks.metadata.book.base.Metadata. Nuttige attributen zijn onder andere title, authors (een lijst met auteurs) en language (de taalcode).

Het dictionaries argument

Dit vertegenwoordigt de verzameling woordenboeken die worden gebruikt voor spellingcontrole van het huidige boek. De meest bruikbare methode is dictionaries.recognized(word), die True retourneert als het doorgegeven woord wordt herkend door het woordenboek voor de taal van het huidige boek.

Het data argument

Dit is een eenvoudige Python-woordenlijst. Wanneer u Alles vervangen uitvoert, zal elke opeenvolgende overeenkomst ervoor zorgen dat replace() wordt aangeroepen met dezelfde woordenlijst als gegevens. U kunt dit dus gebruiken om willekeurige gegevens op te slaan tussen aanroepen van replace() tijdens een Alles vervangen-bewerking.

Het functions argument

The functions argument gives you access to all other user defined functions. This is useful for code re-use. You can define utility functions in one place and re-use them in all your other functions. For example, suppose you create a function named My Function like this:

def utility():
   # do something

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

Dan, in een andere functie, hebt u toegang tot de utility() functie, zo:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    utility = functions['My Function']['utility']
    ...

U kunt het functiesobject ook gebruiken om permanente gegevens op te slaan, die door andere functies opnieuw kunnen worden gebruikt. U kunt bijvoorbeeld één functie hebben die bij uitvoering met Replace All bepaalde gegevens verzamelt, en een andere functie die deze gegevens gebruikt wanneer deze daarna wordt uitgevoerd. Bekijk de volgende twee functies eens:

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

Foutzoeken in uw functies

U kunt de functies die u maakt debuggen met behulp van de standaardfunctie print() van Python. De uitvoer van print wordt weergegeven in een pop-upvenster nadat het zoeken/vervangen is voltooid. Hierboven zag u een voorbeeld van het gebruik van print() om een volledige inhoudsopgave weer te geven.

Kies bestandsvolgorde bij uitvoeren meerdere HTML bestanden

When you run a Replace all on multiple HTML files, the order in which the files are processed depends on what files you have open for editing. You can force the search to process files in the order in which they appear by setting the file_order attribute on your function, like this:

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

replace.file_order = 'spine'

file_order accepteert twee waarden, spine en spine-reverse, waardoor de zoekopdracht meerdere bestanden verwerkt in de volgorde waarin ze in het boek voorkomen, respectievelijk voorwaarts of achterwaarts.

Je functie wordt nog een keer aangeroepen nadat de laatste wedstrijd is gevonden.

Soms, zoals in het voorbeeld van het automatisch genereren van een inhoudsopgave hierboven, is het handig om uw functie nog een keer aan te roepen nadat de laatste overeenkomst is gevonden. U kunt dit doen door het attribuut call_after_last_match in te stellen op uw functie, zoals hier:

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

replace.call_after_last_match = True

De uitvoer van de functie toevoegen aan gemarkeerde tekst

When running search and replace on marked text, it is sometimes useful to append some text to the end of the marked text. You can do that by setting the append_final_output_to_marked attribute on your function (note that you also need to set call_after_last_match), like this:

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

Het resultaatvenster onderdrukken bij het zoeken in gemarkeerde tekst

U kunt ook het resultatendialoogvenster onderdrukken (wat het herhaaldelijk toepassen van een zoek-/vervangactie op veel tekstblokken kan vertragen) door het kenmerk suppress_result_dialog in uw functie in te stellen, zoals hier:

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

replace.suppress_result_dialog = True

Meer voorbeelden

Meer nuttige voorbeelden, aangeleverd door Calibre-gebruikers, zijn te vinden op het Calibre E-book editor forum.