Reemplazar cadena que contiene nueva línea en un archivo enorme

16

¿Alguien sabe de una herramienta no basada en líneas para buscar / reemplazar cadenas "binarias" de una manera algo eficiente en la memoria? Ver esta pregunta también.

Tengo un archivo de texto de + 2GB que me gustaría procesar similar a lo que parece hacer:

sed -e 's/>\n/>/g'

Eso significa que quiero eliminar todas las nuevas líneas que ocurren después de un >, pero no en ningún otro lugar, así que eso descarta tr -d.

Este comando (que obtuve de la respuesta de una pregunta similar ) falla con couldn't re-allocate memory:

sed --unbuffered ':a;N;$!ba;s/>\n/>/g'

Entonces, ¿hay otros métodos sin recurrir a C? Odio a Perl, pero estoy dispuesto a hacer una excepción en este caso :-)

No estoy seguro de ningún carácter que no aparezca en los datos, por \nlo que me gustaría evitar el reemplazo temporal con otro carácter si es posible.

¿Alguna buena idea, alguien?

MattBianco
fuente
¿Has probado la opción --unbuffered?
ctrl-alt-delor
Con o sin --unbuffered
quedarse
¿Qué $!hacer?
ctrl-alt-delor
Lo que está mal con el primer comando sed. El segundo parece estar leyendo todo en el espacio de patrones, aunque no sé $!si es así. Esto espero que necesite MUCHA memoria.
ctrl-alt-delor
El problema es que sed lee todo como líneas, es por eso que el primer comando no elimina las nuevas líneas, ya que genera el texto fila por fila nuevamente. El segundo comando es solo una solución. Creo que sedno es la herramienta adecuada en este caso.
MattBianco

Respuestas:

14

Esto es realmente trivial en Perl, ¡no deberías odiarlo!

perl -i.bak -pe 's/>\n/>/' file

Explicación

  • -i: edite el archivo en su lugar y cree una copia de seguridad del original llamado file.bak. Si no quieres una copia de seguridad, solo perl -i -peúsala.
  • -pe: lea el archivo de entrada línea por línea e imprima cada línea después de aplicar el script dado como -e.
  • s/>\n/>/: la sustitución, al igual que sed.

Y aquí hay un awkenfoque:

awk  '{if(/>$/){printf "%s",$0}else{print}}' file2 
terdon
fuente
3
+1. awk golf:awk '{ORS=/>$/?"":"\n"}1'
glenn jackman
1
Por qué no me gusta Perl en general es la misma razón por la que elegí esta respuesta (o en realidad tu comentario a la respuesta de Gnouc): legibilidad. Usar perl -pe con un simple "patrón sed" es mucho más legible que una expresión sed compleja.
MattBianco
3
@MattBianco es justo, pero, para que lo sepas, eso no tiene nada que ver con Perl. El aspecto subyacente que utilizó Gnouc es una característica de algunos lenguajes de expresión regular (incluidos, entre otros, PCRE), y no la culpa de Perl en absoluto. Además, después de incluir esta monstruosidad sed ':a;N;$!ba;s/>\n/>/g'en su pregunta, ¡ha renunciado a su derecho a quejarse de la legibilidad! : P
terdon
@glennjackman agradable! Estaba jugando con el foo ? bar : bazconstructo pero no pude hacerlo funcionar.
terdon
@terdon: Yeap, mi error. Bórralo.
Cuonglm
7

Una perlsolución:

$ perl -pe 's/(?<=>)\n//'

Explicación

  • s/// se usa para la sustitución de cadenas.
  • (?<=>) es mirar hacia atrás patrón.
  • \n coincide con nueva línea.

Todo el significado del patrón elimina todas las líneas nuevas que tienen >antes.

Cuonglm
fuente
2
¿te gustaría comentar qué hacen las partes del programa? Siempre estoy buscando aprender.
MattBianco
2
¿Por qué molestarse con el lookbehind? ¿Por qué no solo s/>\n/>/?
terdon
1
o s/>\K\n//también funcionaría
glenn jackman
@terdon: Solo lo primero que pensé, eliminar en lugar de reemplazar
cuonglm
@glennjackman: buen punto!
Cuonglm
3

Qué tal esto:

sed ':loop
  />$/ { N
    s/\n//
    b loop
  }' file

Para GNU sed, también puede intentar agregar la opción -u( --unbuffered) según la pregunta. GNU sed también está contento con esto como una simple frase:

sed ':loop />$/ { N; s/\n//; b loop }' file
Graeme
fuente
Eso no elimina el último \nsi el archivo termina >\n, pero probablemente sea preferible de todos modos.
Stéphane Chazelas
@ StéphaneChazelas, ¿por qué el cierre }debe estar en una expresión separada? ¿Esto no funcionará como una expresión multilínea?
Graeme
1
Eso funcionará en seds POSIX con b loop\n}o -e 'b loop' -e '}'pero no como b loop;}y ciertamente no como b loop}porque }y ;son válidos en los nombres de etiqueta (aunque nadie en su sano juicio lo usaría. Y eso significa que GNU sed no es compatible con POSIX) y el }comando necesita ser separado del bcomando
Stéphane Chazelas
@ StéphaneChazelas, GNU sedestá contento con todo lo anterior, incluso con --posix! El estándar también tiene lo siguiente para las expresiones de llaves - The list of sed functions shall be surrounded by braces and separated by <newline>s. ¿Esto no significa que los puntos y comas solo deben usarse fuera de las llaves?
Graeme
@mikeserv, el bucle es necesario para manejar líneas consecutivas que terminan en >. El original nunca tuvo uno, esto fue señalado por Stéphane.
Graeme
1

Debería poder usarlo sedcon el Ncomando, pero el truco consistirá en eliminar una línea del espacio del patrón cada vez que agregue otra (de modo que el espacio del patrón siempre contenga solo 2 líneas consecutivas, en lugar de intentar leer en su totalidad archivo) - intente

sed ':a;$!N;s/>\n/>/;P;D;ba'

EDITAR: después de releer el famoso Sed One-Liners de Peteris Krumins explicado , creo que una mejor sedsolución sería

sed -e :a -e '/>$/N; s/\n//; ta'

que solo agrega la siguiente línea en el caso de que ya se haya hecho una >coincidencia al final, y debe condicionalmente retroceder para manejar el caso de líneas coincidentes consecutivas (es el 39 de Krumin. Agregue una línea a la siguiente si termina con una barra invertida "\" exactamente excepto por la sustitución de >for \como el carácter de unión, y el hecho de que el carácter de unión se retiene en la salida).

conductor de acero
fuente
2
Eso no funciona si terminan 2 líneas consecutivas >(eso también es específico de GNU)
Stéphane Chazelas
1

sedno proporciona una forma de emitir salida sin una nueva línea final. Su enfoque usandoN fundamentalmente funciona, pero almacena líneas incompletas en la memoria y, por lo tanto, puede fallar si las líneas se vuelven demasiado largas (las implementaciones sed no suelen estar diseñadas para manejar líneas extremadamente largas).

Puedes usar awk en su lugar.

awk '{if (/<$/) printf "%s", $0; else print}'

Un enfoque alternativo es utilizar trpara intercambiar el carácter de nueva línea con un carácter "aburrido" y frecuente. El espacio podría funcionar aquí: elija un carácter que tiende a aparecer en cada línea o al menos en una gran proporción de líneas en sus datos.

tr ' \n' '\n ' | sed 's/> />/g' | tr '\n ' ' \n'
Gilles 'SO- deja de ser malvado'
fuente
Ambos métodos ya están demostrados aquí para tener un mejor efecto en otras respuestas. Y su enfoque con sedno funciona sin un búfer de 2.5 gigabytes.
mikeserv
¿Alguien mencionó awk? Oh, lo extrañaba, solo había notado a Perl en la respuesta de Terdon por alguna razón. Nadie mencionó el trenfoque: mikeserv, publicaste un enfoque diferente (válido, pero menos genérico) que también se usa tr.
Gilles 'SO- deja de ser malvado'
válido, pero menos genérico para mí, como lo acabas de llamar una solución funcional y específica. Creo que es difícil argumentar que tal cosa no es útil, lo cual es extraño porque tiene 0 votos a favor. La mayor diferencia que puedo ver entre mi propia solución y su oferta más genérica es que la mía resuelve específicamente un problema, mientras que la suya generalmente. Eso puede hacer que valga la pena, e incluso puedo revertir mi voto, pero también está el molesto asunto de las 7 horas entre ellos y el tema recurrente de sus respuestas que imitan a otros. ¿Puede explicar esto?
mikeserv
-1

Hay muchas maneras de hacer esto, y la mayoría aquí son realmente buenas, pero creo que esta es mi favorita:

tr '>\n' '\n>' | sed 's/^>*//;H;/./!d;x;y/\n>/>\n/'

O incluso:

tr '>\n' '\n>' | sed 's/^>*//' | tr '\n>' '>\n'
mikeserv
fuente
No puedo obtener tu primera respuesta para trabajar en absoluto. Si bien admiro la elegancia del segundo, creo que debes eliminar el *. Tal como está ahora, eliminará las líneas en blanco que siguen a una línea que termina con a >. ... mmm. Mirando hacia atrás a la pregunta, veo que es un poco ambigua. La pregunta dice: "Quiero eliminar todas las nuevas líneas que ocurren después de un >...". Interpreto que eso significa que >\n\n\n\n\nfoodebería cambiarse a \n\n\n\nfoo, pero supongo que foopodría ser el resultado deseado.
Scott
@Scott: probé con variaciones sobre lo siguiente: printf '>\n>\n\n>>\n>\n>>>\n>\nf\n\nff\n>\n' | tr '>\n' '\n>' | sed 's/^>*//;H;/./!d;x;y/\n>/>\n/'- eso >>>>>>>>>>f\n\nff\n\nme da la primera respuesta. Sin embargo, tengo curiosidad por saber qué estás haciendo para romperlo, porque me gustaría arreglarlo. En cuanto al segundo punto, no estoy de acuerdo en que sea ambiguo. El OP no solicita eliminar todo lo > anterior a una línea \nelectrónica, sino eliminar todas las \n líneas electrónicas que siguen a >.
mikeserv
1
Sí, pero una interpretación válida es que, en >\n\n\n\n\n, solo la primera línea nueva es después de un >; Todos los demás siguen otras líneas nuevas. Tenga en cuenta que la sugerencia del OP "esto es lo que quiero, si solo funcionó" fue sed -e 's/>\n/>/g', no sed -e 's/>\n*/>/g'.
Scott
1
@Scott: la sugerencia no funcionó y nunca pudo. No creo que la sugerencia de código de alguien que no comprende completamente el código pueda considerarse un punto de interpretación válido como el lenguaje simple que esa persona también usa. Y además, la salida, si realmente funcionó, de s/>\n/>/on >\n\n\n\n\ntodavía sería algo que s/>\n/>/se editaría.
mikeserv