Funktionsläge för Sök & ersätt i Redigeraren

Sök & ersätt verktyget i redigeraren stöder ett funktionsläge. I detta läge kan du kombinera reguljära uttryck (see Allt om att använda reguljära uttryck i calibre) med godtyckliga kraftfulla Python-funktioner för att göra alla typer av avancerad textbehandling.

I vanliga regexp läget för sök och ersätt kan du ange både ett reguljärt uttryck för att söka efter såväl som en mall som används för att ersätta alla funna träffar. I funktionsläge, istället för använda en fast mall, kan du ange en godtycklig funktion i programmeringsspråket Python. Detta gör att du kan göra massor av saker som inte är möjligt med enkla mallar.

Tekniker för att använda funktionsläge och syntaxen beskrivs med hjälp av exempel, som visar dig hur du skapar funktioner för att utföra allt mer komplexa uppgifter.

The Function mode

Automatiskt justera skiftläget med rubrikerna i dokumentet

Här kommer vi att utnyttja en av de inbyggda funktioner i redigeraren för att automatiskt ändra skiftläget för all text i rubriketiketter till versal:

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

För funktionen, helt enkelt välj inbyggd funktion Titelskiftlägestext. Det kommer att ändra titlar som ser ut: <h1>någon TITEL</h1> till <h1>Någon Titel</h1>. Det kommer även att fungera om det finns andra HTML-etiketter inuti rubriketiketter.

Din första egna funktion - smarta bindestreck

Den verkliga kraften i funktionsläge kommer från att kunna skapa egna funktioner för att bearbeta text på godtyckliga sätt. Förbättra interpunktion-verktyget i redigeraren lämnar enskilda bindestreck i fred, så att du kan använda denna funktion för att ersätta dem med em-streck.

För att skapa en ny funktion, klickar du bara på knappen Skapa/redigera för att skapa en ny funktion och kopiera Python-koden från nedan.

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

Varje Sök och rrsätt anpassad funktion måste ha ett unikt namn och består av en Python-funktion som heter ersätta, som accepterar alla argument som visas ovan. För tillfället ska vi inte oroa för alla olika argument till replace()-funktionen. Bara fokusera på match argumentet. Det representerar en träff när man kör en sök och ersätt. Dess fulla dokumentation finns tillgänglig här. match.group() helt enkelt tillbaka all den passande texten och allt vi gör är att ersätta bindestreck i den texten med em-streck, först ersätt dubbla bindestreck och sedan enskilt bindestreck.

Använd denna funktion med att hitta reguljära uttrycket:

>[^<>]+<

Och det kommer att ersätta alla bindestreck med em-streck, men bara i själva texten och inte inne i HTML-etikettdefinitioner.

Kraften av funktionsläge - med hjälp av en stavningsordlistan att åtgärda felaktigt avstavade ord

Ofta innehåller böcker skapade av genomsökningar av tryckta böcker felavstavade ord – ord som delades vid slutet av raden på den utskrivna sidan. Vi kommer att skriva en enkel funktion för att automatiskt hitta och åtgärda sådana ord.

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

Använd denna funktion med samma sökuttryck som tidigare, det vill säga:

>[^<>]+<

Och det kommer magiskt åtgärda alla felavstavade ord i texten i boken. Huvudtricket är att använda en av de användbara extra argument till replace-funktionen dictionaries. Detta syftar på de ordlistor redigeraren själv använder för att stavningskontrollera text i boken. Vad denna funktion gör är att leta efter ord separerade med ett bindestreck, tar bort bindestrecket och kontrollerar om ordlistan godkänner sammansatt ordet, om den gör det, ersättas de ursprungliga orden med de bindestrecksfria sammansatta orden.

Observera att en begränsning med denna teknik är att det kommer bara att fungera för enspråkiga böcker, därför att, som standard använder, dictionaries.recognized() det huvudsakliga språket i boken.

Numrera sektioner automatiskt

Nu kommer vi att se något lite annorlunda. Anta att din HTML-fil har många sektioner, var och en med en rubrik i en <h2> etikett som ser ut som <h2>Någon text</h2>. Du kan skapa en anpassad funktion som automatiskt numrerar dessa rubriker med avsnittsnummer i följd, så att de ser ut <h2>1. Någon 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'

Använd det för att hitta uttryck:

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

Placera markören i början av filen och klicka Ersätt alla.

Denna funktion använder andra användbara extra argumenten för replace(): number argumentet. När alternativet Ersätt alla används räknas numret upp automatiskt för varje efterföljande träff.

En annan nyhet är att använda replace.file_order – inställning som till 'spine' innebär att om sökningen körs på flera HTML filer, bearbetas filerna i den ordning som de visas i boken. Se Välj filordning när flera HTML-filer körs för detaljer.

Skapa innehållsförteckning automatiskt

Slutligen, låt oss prova något lite mer ambitiöst. Antag att din bok har rubriker i h1 och h2 etiketter som ser ut som <h1 id="someid">Någon text</h1>. Vi kommer automatiskt generera en HTML-innehållsförteckning baserad på dessa rubriker. Skapa den anpassade funktionen nedan:

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'

Och använd den med sökuttrycket:

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

Kör sökningen på Alla textfiler och vid slutet av sökningen, kommer ett fönster dyka upp med ”Felsök utdata från den funktion” vilken kommer ha HTML-innehållsförteckning, klar att klistras in i toc.html.

Funktionen ovan är kraftigt kommenterad, så det borde vara lätt att följa. Det viktigaste nya funktionen är att använda ett annan användbart extra argument till replace() funktion, data objektet. data objektet är en Python dict som kvarstår mellan alla efterföljande anrop av replace() under en enda Ersätt alla operation.

En annan ny funktion är användning av call_after_last_match – genom att sätta denna till Truereplace() funktionen innebär att redigeraren kommer anropa replace() en extra gång efter alla träffar hittats. För detta extra anrop, kommer träffobjektet vara None.

Det var bara en demonstration för att visa styrkan av funktionsläge, om du verkligen behövde generera en innehållsförteckning från rubriker i din bok, kommer du ha större nytta av att använda det dedikerade innehållsförteckningsverktyget i Vertyg → Innehållsförteckning.

API för funktionsläget

Alla funktionsläge funktioner måste vara Python-funktioner som heter ”replace” med följande signatur:

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

När en sök/ersätt körs, för varje träff som hittas kommer funktionen replace() att kallas, den måste återge ersättningssträngen för träffen. Om inga ersättare finns att göra, bör den återge tillbaka, match.group() som är den ursprungliga strängen. De olika argument till replace() funktion dokumenteras nedan.

match argumentet

match argumentet representerar den aktuella funna träffen. Det är ett Python objekt. Den mest användbara metoden är group() som kan användas för att få den matchande texten motsvarar enskilda fångstgrupper i reguljära sökuttrycket.

number argumentet

number argumentet är numret på den aktuella träffen. När du kör Ersätt alla, kommer varje efterföljande träff få replace() att anropas med ett ökande nummer. Den första träffen har nummer 1.

file_name argumentet

Detta är filnamnet på den fil i vilken den aktuella träffen hittades. När du söker inne i markerad text file_name är tom. file_name i kanonisk form, en relativ sökväg till roten av boken, med hjälp av / som sökvägsavgränsare.

metadata argumentet

Detta representerar metadata för den aktuella boken, som titel, författare, språk, o.s.v. Det är ett objekt av klassen calibre.ebooks.metadata.book.base.Metadata. Användbara egenskaper inkluderar, title, authors (en lista med författare) och language (språkkod).

dictionaries argumentet

Detta innebär insamling av ordlistor som används för stavningskontroll den aktuella boken. Den mest användbara metoden dictionaries.recognized(word) vilken som återger True om givet in ord känns igen av ordlista för den aktuella bokens språk.

data-argumentet

Detta en enkel Python dict. När du kör Ersätt alla, kommer varje efterföljande träff orsaka replace() att kallas med samma dict som data. Du kan alltså använda den för att lagra godtyckliga data mellan anrop av replace() under en Ersätt alla-process.

functions-argumentet

functions-argumentet ger dig tillgång till alla andra användardefinierade funktioner. Detta är användbart för kodåteranvändning. Du kan definiera nyttofunktioner på ett ställe och återanvända dem i alla dina andra funktioner. Anta till exempel att du skapar ett funktionsnamn My Function så här:

def utility():
   # do something

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

Sedan, i en annan funktion kan du komma åt utility() funktionen så här:

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

Du kan även använda funktionsobjekt för att lagra beständiga data, som kan återanvändas av andra funktioner. Till exempel kan du ha en funktion som när det körs med Ersätt alla samlar några uppgifter och en annan funktion som använder det när det körs i efterhand. Betrakta följande två funktioner:

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

Felsöka dina funktioner

Du kan felsöka de funktioner som du skapar med hjälp av standard print() funktionen från Python. Utskriften av print kommer visas i ett extrafönster efter att Sök/ersätt är klar. Du såg ett exempel på att använda print() för att mata ut en hel innehållsförteckning ovan.

Välj filordning när flera HTML-filer körs

När du kör Ersätt alla på flera HTML-filer, beror ordningen för vilka filer behandlas på vilka filer du har öppnat för redigering. Du kan tvinga sökningen att behandla filer i den ordning de dyker upp genom att sätta file_order attributet på din funktion, så här.

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

replace.file_order = 'spine'

file_order accepterar två värden, spine och spine-reverse vilka får sökprocessen att behandla multipla filer i den ordning de dyker upp i boken, antingen framlänges eller baklänges.

Med din funktion kallad en extra tid efter den sista träffen hittas

Ibland, som i automatisk generera innehållsförteckning som i exemplet ovan, är det användbart att din funktion anropas en extra gång efter sista träffen hittades. Du kan göra detta genom att sätta call_after_last_match attributet på din funktion, så här:

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

replace.call_after_last_match = True

Lägga till utdata från funktionen till markerad text

När du kör söka och ersätta den markerade texten, är det ibland lämpligt att lägga så text till slutet av den markerade texten. Du kan göra det genom att ställa in append_final_output_to_marked attributet på din funktion (observera att du måste också ställa call_after_last_match), så här:

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

Undertryck resultatdialogen när sökning sker med markerad text

Du kan också undertrycka resultatdialogen (som kan bromsa upp upprepad tillämpning av en sök/ersätt på många textblock ) genom att ställa in suppress_result_dialog attributet på din funktion, så här:

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

replace.suppress_result_dialog = True

Fler exempel

Mer användbara exempel, som tillförs av calibre användare, kan hittas i calibre Editor forum.