wandres.dev
PATRONES · no son expresiones regulares

Capturas: simples, de posición, anidadas y hacia atrás

Cómo extraer trozos de una coincidencia con paréntesis, qué devuelve la captura vacía de posición, cómo se numeran las capturas anidadas y qué significa referirse a una captura ya hecha dentro del mismo patrón.

⏱ 19 min

Buscar sirve de poco si no puedes quedarte con lo encontrado. Las capturas son el mecanismo que convierte el motor de patrones de un detector en un extractor: marcas con paréntesis las partes del patrón que te interesan y las funciones de búsqueda te las devuelven como valores independientes. Lua ofrece cuatro variedades —la simple, la de posición, la anidada y la referencia hacia atrás— y las cuatro caben en el mismo par de paréntesis. La última es, además, la que empuja al humilde motor de patrones fuera de la clase de los lenguajes regulares y le da un poder de reconocimiento que sorprende a quien lo daba por un juguete.

🎯 Al terminar esta lección sabrás
  • Extraer valores con capturas simples y entender qué devuelve una coincidencia sin ellas.
  • Usar la captura vacía para obtener posiciones sin construir subcadenas.
  • Predecir la numeración de un conjunto de capturas anidadas.
  • Escribir referencias hacia atrás y explicar qué clase de lenguaje reconocen.

Capturas simples

Un fragmento del patrón rodeado de paréntesis se captura, y las funciones de búsqueda devuelven una copia del texto que encajó en él. Si el patrón no lleva ningún paréntesis, lo que se devuelve es la coincidencia entera; en cuanto aparece uno, la coincidencia entera deja de devolverse y solo se obtienen las capturas, en el orden en que se abrieron.

local linea = "usuario = ada, edad = 36"

print(linea:match("%a+ = %a+"))              --> usuario = ada   (sin capturas)
print(linea:match("(%a+) = (%a+)"))          --> usuario   ada
print(linea:match("edad = (%d+)"))           --> 36
print(("clave: valor"):match("^(%w+):%s*(.+)$"))  --> clave   valor

Si necesitas a la vez el todo y las partes, envuelve el patrón completo en un paréntesis adicional: no existe una variable implícita para la coincidencia entera dentro del patrón, aunque sí la hay en el reemplazo de una sustitución. Y hay un límite duro que casi nunca se alcanza pero conviene conocer: un patrón admite como máximo treinta y dos capturas, y superarlo produce el error de demasiadas capturas.

Las capturas viajan también por las otras funciones de búsqueda, y en cada una ocupan un lugar distinto del retorno. En la localización se añaden detrás de las dos posiciones; en la iteración se entregan como valores del bucle; en la sustitución alimentan el reemplazo. Ese detalle del orden es la causa de un desliz clásico: recoger la primera captura de una localización en la primera variable y encontrarse con un número.

local ini, fin, clave, valor = ("ancho=120"):find("(%w+)=(%w+)")
print(ini, fin, clave, valor)        --> 1   9   ancho   120

-- el desliz: aqui clave recibe la posicion inicial, no el texto
local clave_mal = ("ancho=120"):find("(%w+)=")
print(clave_mal)                     --> 1

Conviene además conocer los tres errores que produce el motor al analizar capturas mal escritas, porque sus mensajes son escuetos y se reconocen mejor si se han leído antes: captura sin terminar cuando falta un paréntesis de cierre, índice de captura no válido cuando te refieres a una captura que no existe o que aún no ha cerrado, y demasiadas capturas al superar el límite. Ninguno es un fallo silencioso, y eso los convierte en los más benignos de este capítulo.

⚠️
Un paréntesis no agrupa para cuantificar

Escribir (%a)+ no repite el grupo. El motor cierra la captura de una letra y a continuación encuentra un signo más que, al no seguir a una clase, se interpreta como un carácter literal. El patrón pasa a significar «una letra seguida del símbolo de suma» y no protesta: falla en silencio. Es el error más difícil de ver de todo el nivel porque no hay mensaje.

La captura de posición

Un par de paréntesis vacíos no captura texto: captura la posición del sujeto en la que el motor se encontraba al llegar a ese punto del patrón, y la devuelve como número entero. Es la herramienta correcta cuando quieres saber dónde está algo sin pagar la construcción de subcadenas, y también la forma canónica de tokenizar registrando desplazamientos.

local s = "alfa beta gamma"

print(s:match("()beta()"))                 --> 6   10
for pos, palabra in s:gmatch("()(%a+)") do
  print(pos, palabra)                      --> 1 alfa / 6 beta / 11 gamma
end

-- medir sangria sin cortar la cadena
local function sangria(l) return l:match("^%s*()") - 1 end
print(sangria("      texto"))              --> 6

Que el valor devuelto sea un número y no una cadena tiene consecuencias que sorprenden más adelante: en una sustitución con función, ese argumento llega como número; en una sustitución con tabla, se usa como clave numérica; y si intentas referirte a una captura de posición como referencia hacia atrás dentro del patrón, el motor rechaza el índice porque no hay texto que comparar.

🔍

Posición sin subcadena

Obtener el desplazamiento con una captura vacía no reserva memoria. Recorrer un fichero grande registrando posiciones en lugar de fragmentos cambia por completo la presión sobre el recolector.

🧩

La posición es la de entrada

La captura vacía se evalúa cuando el motor la alcanza, así que ponerla antes o después de un elemento da posiciones distintas. Al final de un patrón, señala el byte siguiente al último consumido.

🧵

Compatible con las demás

Puedes mezclar capturas vacías y de texto en el mismo patrón. Se numeran juntas, en el mismo orden de apertura.

⚠️

Los índices empiezan en uno

Como toda la biblioteca de cadenas, la posición devuelta es de base uno. Restar uno para calcular longitudes es el desliz más frecuente.

Anidadas y su numeración

Los paréntesis pueden contener otros paréntesis, y la regla de numeración es la única posible si se quiere que sea estable: las capturas se numeran por el orden en que se abren, no por el orden en que se cierran ni por el nivel de anidamiento. Una captura exterior siempre precede a las que contiene.

flowchart TB
A[Patron con capturas anidadas] --> B[Captura uno abre primero y abarca la fecha entera]
B --> C[Captura dos es el ano]
B --> D[Captura tres es el mes]
B --> E[Captura cuatro es el dia]
C --> F[Orden de retorno segun apertura de parentesis]
D --> F
E --> F
local fecha = "registro 2026-08-04 fin"
local todo, ano, mes, dia = fecha:match("((%d%d%d%d)-(%d%d)-(%d%d))")
print(todo, ano, mes, dia)        --> 2026-08-04   2026   08   04

-- mezcla de posicion y texto
local p1, clave, p2, valor = ("ancho=120"):match("()(%w+)=()(%d+)")
print(p1, clave, p2, valor)       --> 1   ancho   7   120

Hay una consecuencia de esa regla que conviene tener presente al modificar patrones ajenos: añadir un paréntesis renumera todo lo que viene detrás. Si el patrón alimenta una sustitución cuyo reemplazo se refiere a las capturas por número, envolver un fragmento para capturarlo también rompe silenciosamente esas referencias. Es el argumento más fuerte a favor de mantener los patrones cortos y de asignar sus resultados a variables con nombre en la misma línea en que se obtienen.

El anidamiento no es puramente cosmético: permite obtener una vista jerárquica de una coincidencia en una sola pasada. Como el lenguaje carece de grupos que se cuantifiquen, el anidamiento es también el único mecanismo de estructura que ofrece, y por eso conviene usarlo con moderación. Un patrón con cinco niveles de paréntesis y ocho capturas es ilegible, no verificable y casi siempre la señal de que el problema pedía dos búsquedas encadenadas o un analizador de verdad.

Referencias hacia atrás

Dentro del patrón, un porcentaje seguido de un dígito del uno al nueve no designa una clase ni un carácter literal: designa una copia exacta del texto que capturó esa captura. El motor no vuelve a interpretar el patrón, compara byte a byte con lo ya capturado. Es lo que permite exigir que dos fragmentos distantes de la cadena sean iguales.

-- el delimitador de cierre debe ser el mismo que el de apertura
local s = [[dijo 'hola' y luego "adios"]]
for comilla, texto in s:gmatch("(['\"])(.-)%1") do
  print(comilla, texto)            --> '  hola   /   "  adios
end

-- palabras duplicadas consecutivas
print(("el el gato"):match("(%a+)%s+%1"))     --> el

-- delimitadores largos al estilo de las cadenas largas de Lua
print(("[==[cuerpo]==]"):match("%[(=*)%[(.-)%]%1%]"))  --> ==   cuerpo
La referencia hacia atrás rompe la barrera de lo regular

Aquí ocurre algo que merece detenerse a mirar de frente, porque es el punto en que un motor de quinientas líneas se sale de la teoría que uno le presupone. Un autómata finito no puede recordar lo que leyó: tiene un número fijo de estados y, en cuanto la información que necesita conservar crece con la entrada, deja de poder. Por eso el lenguaje de las cadenas de la forma w seguido de w, es decir, un fragmento arbitrario repetido dos veces, no es regular; y tampoco es libre de contexto, cosa que se demuestra con el lema de bombeo correspondiente. Está por encima de las dos primeras clases de la jerarquía. Y sin embargo (%a+)%1 lo reconoce en Lua, en un motor que no construye autómatas, no compila nada y cabe en un fichero de la biblioteca estándar. La razón es que la referencia hacia atrás no es una operación del álgebra regular: es una comparación de igualdad diferida que el motor puede permitirse porque conserva un puntero al texto ya capturado en el sujeto. Esa capacidad extra tiene un precio conocido y demostrado: el problema de decidir si una expresión con referencias hacia atrás acepta una cadena es NP-completo, mientras que el de una expresión regular pura se resuelve en tiempo lineal. Cuando escribes %1 no estás usando una comodidad sintáctica, estás cambiando de clase de complejidad, y el motor lo paga explorando el espacio de posibilidades con retroceso. La moraleja práctica es doble. Primera: las referencias hacia atrás resuelven en una línea problemas que de otro modo exigen dos pasadas y comparación manual, y son la razón por la que en Lua se recortan cadenas largas y bloques con delimitadores de longitud variable sin escribir un analizador. Segunda: cada %1 que añades multiplica el trabajo potencial del motor, así que pertenecen a patrones cortos sobre entradas acotadas, nunca al centro de un bucle que procesa megabytes.

📝
Lo esencial

Los paréntesis capturan y se numeran por orden de apertura; sin capturas se devuelve la coincidencia entera, con ellas solo las capturas. Los paréntesis vacíos capturan una posición como número. Los paréntesis no agrupan para cuantificar, y un cuantificador tras el paréntesis de cierre se interpreta como carácter literal sin dar error. Un porcentaje seguido de dígito dentro del patrón exige una copia literal de lo ya capturado y saca al motor de la clase de los lenguajes regulares.

⚔️ Extrae, sitúa y compara
  1. Escribe un patrón que descomponga una línea de configuración en clave y valor tolerando espacios alrededor del igual, y otro que además devuelva la línea entera.
  2. Tokeniza un párrafo devolviendo pares de posición y palabra, y reconstruye después las palabras con string.sub a partir de las posiciones. Compara la memoria de ambos enfoques.
  3. Predice sobre papel la numeración de un patrón con tres niveles de anidamiento y dos capturas de posición intercaladas, y verifícalo.
  4. Detecta palabras duplicadas consecutivas en un texto real. Explica por qué el patrón falla cuando las dos palabras difieren en la caja y arréglalo.
  5. Escribe (%a)+ y explica exactamente contra qué cadena encaja. Después escribe la versión que sí hace lo que pretendías.