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

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

I standardläget regexp för sök och ersätt anger du både ett reguljärt uttryck för att söka efter och en mall som används för att ersätta alla hittade matchningar. I funktionsläget, istället för att använda en fast mall, anger du en godtycklig funktion, i programmeringsspråket Python. Det här gör att du kan göra massor av saker som inte är möjliga 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.

Funktionsläget

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 rubriktaggar 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-taggar inuti rubriktaggar.

Din första anpassade funktion - smartare bindestreck

Den verkliga kraften i funktionsläge kommer från att kunna skapa dina egna funktioner för att bearbeta text på godtyckliga sätt. Förbättra skiljeteckenhanteringsverktyget 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 helt enkelt 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 ersätt anpassad funktion måste ha ett unikt namn och bestå av en Python-funktion med namnet replace, som accepterar alla argument som visas ovan. För tillfället kommer vi inte oroa oss för alla olika argument för funktionen replace(). Fokusera bara på argumentet match. Det representerar en matchning när du söker och ersätter. Dess fullständiga dokumentation finns tillgänglig här. match.group() returnerar helt enkelt all den matchad text och allt vi gör är att ersätta bindestreck i den texten med em-bindestreck, ersätt först dubbla bindestreck och sedan enkla bindestreck.

Använd den här funktionen med det reguljära uttrycket hitta:

>[^<>]+<

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

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

Ofta innehåller böcker skapade av skanningar 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 ordböcker 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 ordboken godkänner sammansatt ordet, om den gör det, ersättas de ursprungliga orden med de bindestrecksfria sammansatta orden.

Observera att en begränsning av denna teknik är att den bara fungerar för enspråkiga böcker, eftersom dictionaries.recognized() som standard använder bokens huvudspråk.

Automatisk numrera avsnitt

Nu kommer vi att se något lite annorlunda. Anta att din HTML-fil har många avsnitt, var och en med en rubrik i en <h2> tagg 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 med sökuttrycket:

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

Placera markören överst i filen och klicka på Ersätt alla.

Den här funktionen använder ett annat av de användbara extraargumenten för argumentet replace(): number. När du gör ett Ersätt alla-räknas numret upp automatiskt för varje efterföljande träff.

En annan ny funktion är användningen av replace.file_order – inställningen att 'spine' betyder att om denna sökning körs på flera HTML-filer bearbetas filerna i den ordning de visas i boken. Se Välj filordning när flera HTML-filer körs för detaljer.

Skapa automatiskt en innehållsförteckning

Slutligen, låt oss testa något lite mer ambitiöst. Antag att din bok har rubriker i h1 och h2 taggar som ser ut som <h1 id="someid">Någon text</h1>. Vi kommer automatiskt skapa 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 utmatning från din funktion” vilken kommer ha HTML-innehållsförteckning, klar att klistras in i toc.html.

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

En annan ny funktion är användningen av call_after_last_match – genom att ställa in det till True på funktionen replace() innebär det att redigeraren kommer att anropa replace() en extra gång efter att alla träffar har 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 skapa 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 anropas, den måste returnera ersättningssträngen för den träffen. Om inga ersättare ska göras, bör den returnera match.group() som är den ursprungliga strängen. De olika argumenten för funktionen replace() dokumenteras nedan.

Argumentet match

Argumentet match representerar den aktuella hittade matchningen. Det är ett Python objekt. Den användbaraste metoden är group() som kan användas för att få den matchande texten som motsvarar enskilda fångstgrupper i reguljära sökuttrycket.

Argumentet number

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

Argumentet file_name

Det här är filnamnet på filen däre den aktuella träffen hittades. När du söker inuti markerad text är file_name tom. file_name är i kanonisk form, en relativ sökväg till roten av boken, med / som sökvägsavgränsare.

Argumentet metadata

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).

Argumentet dictionaries

Detta innebär insamling av ordböcker som används för stavningskontroll den aktuella boken. Den användbaraste metoden dictionaries.recognized(word) vilken som returnerar True om givet in ord känns igen av ordbok för den aktuella bokens språk.

data-argumentet

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

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 också använda funktionsobjektet för att lagra beständiga data, som kan återanvändas av andra funktioner. Till exempel kan du ha en funktion som när den körs med Ersätt alla samlar in en del data och en annan funktion som använder den när den körs efteråt. 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ökning av 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 som filerna behandlas på på vilka filer du har öppna för redigering. Du kan tvinga sökningen att behandla filer i den ordning de visas genom att ställa in attributet file_order i 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 flera filer i den ordning de dyker upp i boken, antingen framåt eller bakåt.

Att få din funktion anropad en extra gång efter att den senaste träffen hittats

Ibland, som i exemplet med den automatiskt skapande av innehållsförteckning ovan, är det användbart att få din funktion anropad en extra gång efter att den sista träffen hittats. Du kan göra detta genom att ställa in attributet call_after_last_match 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 utmatning från funktionen till markerad text

När du kör sök och ersätt på markerad text är det ibland användbart att lägga till sådan text i slutet av den markerade texten. Du kan göra det genom att ställa in attributet append_final_output_to_marked på din funktion (observera att du också måste ställa in 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 resultatdialogrutan när sökning sker med markerad text

Du kan också undertrycka resultatdialogrutan (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 forumet för calibre e-bokredigeraren.