Modo de función para buscar y sustituir en el editor

La herramienta Buscar y sustituir en el editor admite un modo de función. En este modo se pueden combinar expresiones regulares (ver Todo acerca de cómo utilizar expresiones regulares en calibre) con funciones Python de complejidad arbitraria para realizar todo tipo de procesamiento de texto avanzado.

En el modo estándar de expresiones regulares para buscar y sustituir, se especifica una expresión regular para buscar y una plantilla que se usa para sustituir todas las coincidencias. En el modo de función, en lugar de usar una sola plantilla, se especifica una función arbitraria en el lenguaje de programación Python. Esto le permite hacer muchas cosas que no son posibles sólo con plantillas.

Algunas técnicas para usar el modo de función y su sintaxis se describirán a través de ejemplos, que le mostrarán cómo crear funciones para realizar tareas cada vez más complejas.

The Function mode

Corregir automáticamente las mayúsculas y minúsculas en las cabeceras del documento

Aquí aprovecharemos una de las funciones incorporadas del editor para poner la primera letra de cada palabra dentro de una etiqueta de encabezado en mayúscula:

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

Como función, elija la función predefinida Capitalize (ignore tags). Esto cambiará los títulos de la forma <h1>algún TÍTULO</h1> a <h1>Algún título</h1>. Funcionará incluso si hay otras etiquetas HTML en la etiqueta de cabecera.

La primera función personalizada: mejorar guiones

La verdadera potencia del modo de función procede de la posibilidad de crear funciones propias para procesar el texto de manera arbitraria. La herramienta Mejorar puntuación del editor no modifica los guiones aislados, así que puede usar esta función para sustituirlos por rayas.

Para crear una nueva función, simplemente pulse en el botón Crear o modificar y copie el código Python siguiente.

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

Toda función personalizada de Buscar y sustituir debe tener un nombre único y consister en una función Python llamada «replace», que acepte todos los argumentos mostrados arriba. Por ahora no vamos a preocuparnos de los diferentes argumentos de la función replace(). Fíjese sólo en el argumento match. Representa una coincidencia al realizar una búsqueda y sustitución. La documentación completa se encuentra aquí. match.group() devuelve todo el texto de la coincidencia y todo lo que hacemos es sustituir los guiones del texto por rayas, sustituyendo primero los guiones dobles y luego los sencillos.

Use esta función con la expresión regular de búsqueda:

>[^<>]+<

Y sustituirá todos los guiones por rayas, pero sólo en texto real y no dentro de las definiciones de etiqueta HTML.

La potencia del modo de función: usar un diccionario para corregir palabras mal divididas por guiones

A menudo, los libros electrónicos creados a partir de imágenes de libros impresos contienen palabras mal divididas por guiones: palabras que estaban divididas al final de un renglón en la página impresa. Vamos a escribir una función sencilla para encontrar y corregir automáticamente estas palabras.

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

Use esta función con la misma expresión de búsqueda anterior, es decir:

>[^<>]+<

Y corregirá automáticamente todas las palabras con guiones incorrectos en el el texto del libro. El truco principal es utilizar uno de los argumentos adicionales de la función «replace»: dictionaries. Esto se refiere a los diccionarios que el propio editor usa para comprobar la ortografía en el libro. Lo que hace esta función es buscar palabras unidas por un guión, eliminar el guión y comprobar si el diccionario reconoce la palabra compuesta; si lo hace, las palabras originales se sustituyen por la palabra sin guión.

Tenga en cuenta que una limitación de esta técnica es que sólo funciona par libros monolingües, porque de manera predeterminada ``dictionaries.recognized()``usa el idioma principal del libro.

Enumerar secciones automáticamente

Ahora veremos algo un poco diferente. Supongamos que un archivo HTML tiene muchas secciones, cada una con una cabecera en una etiqueta <h2> de esta forma: <h2>Algún texto</h2>. Puede crear una función personalizada que numere automáticamente estas cabeceras con números consecutivos, para que tengan esta forma: <h2>1. Algún texto</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'

Úselo con la expresión de búsqueda:

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

Coloque el cursor al principio del archivo y pulse Sustituir todo.

Esta función usa otro de los prácticos argumentos adicionales de replace(): el argumento number. Al usar Sustituir todo, number se incrementa automáticamente en cada coincidencia.

Otra característica nueva es el uso de replace.file_order. Establecerlo en 'spine' significa que si la búsqueda se ejecuta sobre múltiples archivos HTML, los archivos se procesarán en el orden en el que aparecen en el libro. Más detalles en Elegir un orden de archivos al ejecutar sobre múltiples archivos HTML.

Crear un índice automáticamente

Por último, vamos a intentar algo un poco más ambicioso. Supongamos que el libro tiene encabezados en etiquetas h1 y h2 del tipo <h1 id="un_id">Algún texto</h1>. Vamos a crear un índice HTML generado automáticamente a partir de estos encabezados. Creamos la siguiente función:

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'

Y úselo con la expresión de búsqueda:

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

Ejecutamos la búsqueda sobre Todos los archivos de texto y al final de la búsqueda se abrirá una ventana con «Salida de depuración de la función», que contendrá el índice HTML, listo par copiarlo en toc.html.

La función de arriba está muy comentada, así que debe ser fácil de seguir. La principal nueva característica es el uso de un argumento adicional de la función replace(), el objeto data. El objeto data es un diccionario Python que sobrevive entre sucesivas llamadas a la función replace() durante una única operación Sustituir todo.

Otra característica nueva es el uso de call_after_last_match. Establecerlo en True en la función replace() significa que el editor ejecutará replace() una vez adicional después de haber encontrado todas las coincidencias. En esa ejecución adicional el objeto de coincidencia será None.

Esto ha sido sólo una demostración de la potencia del modo de función, si realmente necesita generar un índice a partir de encabezados en un libro, es mejor usar la herramienta específica para índices en Herramientas > Índice.

La API para el modo de función

Todas las funciones del modo función deben ser funciones Python con el nombre «replace» y con la siguiente firma:

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

Cuando se realiza una búsqueda y sustitución, la función replace() se ejecuta para cada coincidencia, debe devolver el texto de sustitución para dicha coincidencia. Si no se ha de efectuar ninguna sustitución, debe devolver match.group(), que es el texto original. Los distintos argumentos de la función replace() se documentan a continuación.

El argumento match

El argumento match representa la coincidencia actual. Es un objeto Match de Python. Su método más útil es group(), que puede emplearse para obtener el texto correspondiente a los grupos de captura individuales en la expresión regular de búsqueda.

El argumento number

El argumento number es el número de la coincidencia actual. Al ejecutar Sustituir todo, cada coincidencia sucesiva da lugar a una ejecución de replace(), con un número que va en aumento. La primera coincidencia tiene el número 1.

El argumento file_name

Éste es el nombre del archivo donde se encontró la coincidencia actual. Al buscar un texto marcado, file_name está vacío. El argumento file_name está en forma canónica: una ruta de acceso relativa a la raíz del libro, usando / como separador.

El argumento metadata

Esto representa los metadatos del libro actual, como título, autores, idioma, etc. Es un objeto de clase calibre.ebooks.metadata.book.base.Metadata. Algunos atributos útiles son title, authors (una lista de autores) y language (el código del idioma).

El argumento dictionaries

Esto representa la colección de diccionarios usados para la comprobación de ortografía del libro actual. Su método más útil es dictionaries.recognized(word), que devuelve True si la palabra word es reconocida por el diccionario para el idioma del libro actual.

El argumento data

Esto es un diccionario de Python sencillo. Al ejecutar Sustituir todo, cada coincidencia sucesiva da lugar a una ejecución de replace() con el mismo data. Por lo tanto puede usarlo para almacenar datos arbitrarios entre ejecuciones de replace() durante una operación de Sustituir todo.

El argumento functions

El argumento functions proporciona acceso a todas las otras funciones definidas por el usuario. Esto es útil para la reutilización de código. Puede definir funciones en un lugar y luego reutilizarlas en todas las otras funciones. Por ejemplo, supongamos que crea una función con nombre My Function así:

def utility():
   # do something

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

Luego, en otra función, puede acceder a la función utility() de esta manera:

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

También puede usar el objeto functions para guardar datos persistentes que sean accesibles a otras funciones. Por ejemplo, puede tener una función que al ejecutarse con Sustituir todo recopile datos y otra función que los use cuando se ejecuta a continuación. Considere las dos funciones siguientes:

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

Depurar las funciones propias

Puede depurar las funciones que cree con la función estándar de Python print(). La salida de print() se mostrará en una ventana emergente cuando la búsqueda y sustitución se haya completado. Ya hemos visto anteriormente un ejemplo del uso de print() para mostrar un índice completo.

Elegir un orden de archivos al ejecutar sobre múltiples archivos HTML

Al ejecutar Sustituir todo sobre múltiples archivos HTML, el orden en que se procesan los archivos depende de qué archivos estén abiertos para modificar. Puede hacer que la búsqueda procese los archivos en el orden en el que aparecen estableciendo el attributo file_order de la función, de esta manera:

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

replace.file_order = 'spine'

file_order acepta dos valores, spine y spine-reverse, que hacen que el la búsqueda procese archivos múltiples en el orden en que aparecen en el libro, hacia adelante o hacia atrás, respectivamente

Hacer que una función se ejecute una vez más después de la última coincidencia

A veces, como en el ejemplo anterior del índice generado automáticamente, es útil que la función se ejecute una vez adicional después de haber encontrado la última coincidencia. Puede conseguir esto estableciendo el atributo call_after_last_match en la función, de esta manera:

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

replace.call_after_last_match = True

Añadir la salida de una función al texto marcado

Al ejecutar una búsqueda y sustitución sobre un texto marcado, a veces es útil añadir algún texto al final del texto marcado. Puede hacer esto estableciéndo el atributo append_final_output_to_marked en la función (tenga en cuenta que también debe establecer call_after_last_match), de esta manera:

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

No mostrar el cuadro de diálogo de resultados al hacer búsquedas en texto marcado

También puede evitar que se muestre el cuadro de diálogo de resultados (que puede ralentizar la aplicación de una búsqueda o sustitución en múltiples bloques de texto) estableciendo el atributo suppress_result_dialog de la función, de esta manera:

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

replace.suppress_result_dialog = True

Más ejemplos

Otros ejemplos útiles, creados por usarios de calibre, pueden encontrarse en el foro del editor de calibre (en inglés).