awk - ejemplos - grep expresiones regulares extendidas
¿Cómo uso awk para extraer datos dentro de delimitadores anidados utilizando expresiones regulares no codiciosas? (2)
Esta pregunta ocurre repetidamente en muchas formas con muchos delimitadores diferentes de caracteres múltiples, por lo que en mi humilde opinión, vale la pena una respuesta canónica.
Dado un archivo de entrada como:
<foo> .. 1 <foo> .. a<2 .. </foo> .. </foo> <foo> .. @{<>}@ <foo> .. 4 .. </foo> .. </foo> <foo> .. 5 .. </foo>
¿Cómo se extrae el texto entre los delimitadores de inicio anidado ( <foo>
) y final ( </foo>
) usando un ajuste no codicioso con awk?
La salida deseada (en cualquier orden) es:
<foo> .. a<2 .. </foo>
<foo> .. 1 .. </foo>
<foo> .. 4 .. </foo>
<foo> .. @{<>}@ .. </foo>
<foo> .. 5 .. </foo>
Tenga en cuenta que el inicio o el final podría ser cualquier cadena de caracteres múltiples y el texto entre ellos podría ser cualquier cosa excepto esas cadenas, incluidos los caracteres que forman parte de esas cadenas, como los caracteres <
o >
en este ejemplo.
Mi solución (versión actual) aborda el problema desde el frente, por lo que la salida no es exactamente la misma:
<foo> .. 1 # second
<foo> .. a<2 .. </foo> .. # first in my approach
</foo>
<foo> .. @{<>}@ # fourth
<foo> .. 4 .. </foo> .. # third
</foo>
<foo> .. 5 .. </foo> # fifth
si el programa atraviesa las matrices arr
y seps
hacia atrás, la salida sería la misma (probablemente), pero me acabo de quedar sin tiempo temporalmente.
En Gnu awk (para usar split
con cuatro params para analizar los datos).
EDITAR Para la compatibilidad con otros que no sean Gnu awk, agregué la función gsplit()
que es una sustitución cruda de Gnu awk.
$ cat program.awk
{ data=data $0 } # append all records to one var
END {
n=gsplit(data, arr, "</?foo>", seps) # split by every tag
for(i=1;i<=n;i++) { # atm iterate arrays from front to back
if(seps[i]=="<foo>") # if element opening tag
stack[++j]=seps[i] arr[i+1] # store tag ang wait for closing tag
else {
stack[j]=stack[j] (seps[i]==prev ? arr[i] : "")
print stack[j--] seps[i]
}
prev = seps[i]
}
}
# elementary gnu awk split compatible replacement
function gsplit(str, arr, pat, seps, i) {
delete arr; delete seps; i=0
while(match(str, pat)) {
arr[++i]=substr(str,1,(RSTART-1))
seps[i]=substr(str,RSTART,RLENGTH)
str=substr(str,(RSTART+RLENGTH))
}
arr[++i]=substr(str,1)
return i
}
Ejecutarlo:
$ awk -f program.awk file
<foo> .. a<2 .. </foo>
<foo> .. 1 .. </foo>
<foo> .. 4 .. </foo>
<foo> .. @{<>}@ .. </foo>
<foo> .. 5 .. </foo>
El principal desafío es que, como awk solo admite coincidencias codiciosas, no puede escribir ninguna variación de <foo>.*</foo>
que se detendrá en el primer </foo>
en la línea en lugar de en el último </foo>
. La solución es convertir cada cadena inicial y final en un único carácter que no puede aparecer en la entrada para que pueda escribir x[^xy]*y
donde x e y son esos caracteres iniciales / finales, pero ¿cómo elegir un personaje que pueda ¿aparece en la entrada? Tú no, tú haces uno:
$ cat nonGreedy.awk
{
$0 = encode($0)
while ( match($0,/({[^{}]*})/) ) {
print decode(substr($0,RSTART,RLENGTH))
$0 = substr($0,1,RSTART-1) substr($0,RSTART+RLENGTH)
}
}
function encode(str) {
gsub(/@/,"@A",str)
gsub(/{/,"@B",str); gsub(/}/,"@C",str)
gsub(/<foo>/,"{",str); gsub(/<//foo>/,"}",str)
return str
}
function decode(str) {
gsub(/}/,"</foo>",str); gsub(/{/,"<foo>",str)
gsub(/@C/,"}",str); gsub(/@B/,"{",str)
gsub(/@A/,"@",str)
return str
}
$ awk -f nonGreedy.awk file
<foo> .. a<2 .. </foo>
<foo> .. 1 .. </foo>
<foo> .. 4 .. </foo>
<foo> .. @{<>}@ .. </foo>
<foo> .. 5 .. </foo>
Lo anterior funciona seleccionando cualquier caracter que no pueda aparecer SOLO EN LAS CADENAS DE INICIO / FIN (note que no tiene que ser un caracter que no pueda aparecer en la entrada, solo que no en esas cuerdas), en en este caso, estoy eligiendo @
y agregando una A
después de cada aparición en la entrada. En este punto, cada aparición de @A
representa un carácter @
y se garantiza que no habrá apariciones de @B
o @
seguidas de cualquier otra cosa en cualquier lugar de la entrada.
Ahora podemos elegir otros 2 caracteres que queremos usar para representar las cadenas de inicio / final, en este caso estoy eligiendo {
y }
, y las convierto en algunas cadenas @B
como @B
y @C
y en este punto cada aparición de @B
representa un carácter y @C
representa un carácter }
y no hay {
s o }
s en ninguna parte de la entrada.
Ahora todo lo que queda por hacer para encontrar las cadenas que queremos extraer es convertir cada cadena de inicio <foo>
al carácter de inicio que hemos elegido, {
y cada cadena final </foo>
al carácter final }
y luego podemos use una expresión regular simple de {[^{}]*}
para representar una versión no codiciosa de <foo>.*</foo>
.
A medida que encontramos cada cadena, simplemente desenrollamos las conversiones que hicimos arriba en orden inverso (tenga en cuenta que debe desenrollar las sustituciones en cada cadena coincidente exactamente en el orden inverso al que las aplicó al registro completo) así que {
vuelve a <foo>
y @B
vuelve a {
, y @A
vuelve a @
, etc. y tenemos el texto original para esa cadena.
Lo anterior funcionará en cualquier awk. Si sus cadenas de inicio / final contienen metacaracteres RE, entonces tendría que escapar de ellas o usar un ciclo while(index(substr()))
lugar de gsub()
para reemplazarlas.
Tenga en cuenta que si usa gawk y las etiquetas no están anidadas, entonces puede mantener las 2 funciones exactamente como arriba y cambiar el resto del script a solo:
BEGIN { FPAT="{[^{}]*}" }
{
$0 = encode($0)
for (i=1; i<=NF; i++) {
print decode($i)
}
}
Obviamente, no es necesario poner la funcionalidad de codificación / decodificación en funciones separadas, solo lo separé aquí para hacer que esa funcionalidad sea explícita y separada del ciclo que la usa para mayor claridad.
Para obtener otro ejemplo de cuándo / cómo aplicar el enfoque anterior, consulte https://.com/a/40540160/1745001 .