Ordene un archivo de texto por longitud de línea incluyendo espacios

Tengo un archivo CSV que se parece a esto

 AS2345, ASDF1232, Sr. Plain Example, 110 Binary ave., Atlantis, RI, 12345, (999) 123-5555,1.56
 AS2345, ASDF1232, Mrs. Plain Example, 1121110 Ternary st.  110 Binary ave .., Atlantis, RI, 12345, (999) 123-5555,1.56
 AS2345, ASDF1232, Sr. Plain Example, 110 Binary ave., Liberty City, RI, 12345, (999) 123-5555,1.56
 AS2345, ASDF1232, Sr. Plain Example, 110 Ternary ave., Some City, RI, 12345, (999) 123-5555,1.56

Necesito ordenarlo por longitud de línea, incluyendo espacios. El siguiente comando no incluye espacios, ¿hay alguna manera de modificarlo para que funcione para mí?

cat $@ | awk '{ print length, $0 }' | sort -n | awk '{$1=""; print $0}' 

Responder

 cat testfile | awk '{ print length, $0 }' | sort -n -s | cut -d" " -f2- 

O bien, para hacer su subordenación original (quizás no intencional) de cualquier línea de igual longitud:

 cat testfile | awk '{ print length, $0 }' | sort -n | cut -d" " -f2- 

En ambos casos, hemos resuelto su problema establecido alejándonos de awk para su corte final.

Líneas de longitud coincidente: qué hacer en caso de empate:

La pregunta no especificaba si se deseaba o no una clasificación adicional para las líneas de longitud coincidente. Supuse que esto no era deseado y sugerí el uso de -s ( --stable ) para evitar que esas líneas se --stable entre sí, y mantenerlas en el orden relativo en que ocurren en la entrada.

(Aquellos que quieran tener más control para clasificar estos vínculos pueden mirar la opción de ordenar --key ).

Por qué falla la solución intentada de la pregunta (awk line-rebuilding):

Es interesante notar la diferencia entre:

 echo "hello awk world" | awk '{print}' echo "hello awk world" | awk '{$1="hello"; print}' 

Ellos ceden respectivamente

 hello awk world hello awk world 

La sección relevante del manual de (gawk) solo menciona como un lado que awk va a reconstruir el total de $ 0 (basado en el separador, etc.) cuando cambia un campo. Supongo que no es un comportamiento loco. Tiene esto:

“Finalmente, hay momentos en los que es conveniente forzar awk para reconstruir todo el registro, usando el valor actual de los campos y OFS. Para hacer esto, use la asignación aparentemente inocua:”

  $1 = $1 # force record to be reconstituted print $0 # or whatever else with $0 

“Esto obliga a awk a reconstruir el récord”.

Entrada de prueba que incluye algunas líneas de igual longitud:

 aa A line with MORE spaces bb The very longest line in the file ccb 9 dd equal len. Orig pos = 1 500 dd equal len. Orig pos = 2 ccz cca ee A line with some spaces 1 dd equal len. Orig pos = 3 ff 5 dd equal len. Orig pos = 4 g 

Pruebe este comando en su lugar:

 awk '{print length, $0}' your-file | sort -n | cut -d " " -f2- 

La solución AWK de neillb es genial si realmente quieres usar awk y explica por qué es una molestia allí, pero si lo que quieres es hacer el trabajo rápidamente y no te importa en qué lo hagas, una solución es utilice la función sort() Perl con una rutina de caparison personalizada para iterar sobre las líneas de entrada. Aquí hay un trazador de líneas:

 perl -e 'print sort { length($a) <=> length($b) } <>' 

Puedes poner esto en tu canalización donde sea que lo necesites, ya sea recibiendo STDIN (de cat o un redireccionamiento de shell) o simplemente dale el nombre de archivo a perl como otro argumento y deja que abra el archivo.

En mi caso, yo necesitaba las líneas más largas primero, así que cambié $a y $b en la comparación.

Pure Bash:

 declare -a sorted while read line; do if [ -z "${sorted[${#line}]}" ] ; then # does line length already exist? sorted[${#line}]="$line" # element for new length else sorted[${#line}]="${sorted[${#line}]}\n$line" # append to lines with equal length fi done < data.csv for key in ${!sorted[*]}; do # iterate over existing indices echo -e "${sorted[$key]}" # echo lines with equal length done 

La función length() incluye espacios. Haría solo pequeños ajustes en tu canalización (incluido evitar UUOC ).

 awk '{ printf "%d:%s\n", length($0), $0;}' "$@" | sort -n | sed 's/^[0-9]*://' 

El comando sed elimina directamente los dígitos y los puntos añadidos por el comando awk . Alternativamente, manteniendo el formato de awk :

 awk '{ print length($0), $0;}' "$@" | sort -n | sed 's/^[0-9]* //' 

Descubrí que estas soluciones no funcionarán si su archivo contiene líneas que comienzan con un número, ya que se ordenarán numéricamente junto con todas las líneas contadas. La solución es sort el indicador -g (tipo-general-numérico) en lugar de -n (tipo-numérico):

 awk '{ print length, $0 }' lines.txt | sort -g | cut -d" " -f2- 

Con POSIX Awk:

 { c = length m[c] = m[c] ? m[c] RS $0 : $0 } END { for (c in m) print m[c] } 

Ejemplo

Aquí hay un método compatible con multibyte para clasificar líneas por longitud. Requiere:

  1. wc -m está disponible para usted (macOS lo tiene).
  2. Su configuración regional actual admite caracteres de varios bytes, por ejemplo, al configurar LC_ALL=UTF-8 . Puede configurar esto en su .bash_profile, o simplemente anteponiéndolo antes del siguiente comando.
  3. testfile tiene una encoding de caracteres que coincide con su configuración regional (por ejemplo, UTF-8).

Aquí está el comando completo:

 cat testfile | awk '{l=$0; gsub(/\047/, "\047\"\047\"\047", l); cmd=sprintf("echo \047%s\047 | wc -m", l); cmd | getline c; close(cmd); sub(/ */, "", c); { print c, $0 }}' | sort -ns | cut -d" " -f2- 

Explicando parte por parte:

  • l=$0; gsub(/\047/, "\047\"\047\"\047", l); ← crea una copia de cada línea en awk variable l y double-escapes cada ' para que la línea se pueda repetir con seguridad como un comando de shell ( \047 es una comilla simple en notación octal).
  • cmd=sprintf("echo \047%s\047 | wc -m", l); ← Este es el comando que ejecutaremos, que hace eco de la línea escapada a wc -m .
  • cmd | getline c; ← ejecuta el comando y copia el valor de recuento de caracteres que se devuelve a la variable awk c .
  • close(cmd); ← cierre la tubería al comando shell para evitar golpear un límite del sistema en la cantidad de archivos abiertos en un proceso.
  • sub(/ */, "", c); ← recorta el espacio en blanco del valor de recuento de caracteres devuelto por wc .
  • { print c, $0 } ← imprime el valor del recuento de caracteres de la línea, un espacio y la línea original.
  • | sort -ns | sort -ns ← ordena las líneas (por valores de recuento de caracteres antepuestos) numéricamente ( -n ) y mantiene el orden de clasificación estable ( -s ).
  • | cut -d" " -f2- | cut -d" " -f2- ← elimina los valores de recuento de caracteres | cut -d" " -f2- .

Es lento (solo 160 líneas por segundo en un Macbook Pro rápido) porque debe ejecutar un subcomando para cada línea.

Alternativamente, haz esto solo con gawk (a partir de la versión 3.1.5, gawk es multibyte aware), que sería significativamente más rápido. Es un montón de problemas hacer todos los escapes y las comillas dobles para pasar las líneas de manera segura a través de un comando de shell desde awk, pero este es el único método que pude encontrar que no requiere la instalación de software adicional (gawk no está disponible por defecto en Mac OS).

Resultados de referencia

Además, otra solución de Perl:
perl -ne 'push @a, $_; END{ print sort { length $a <=> length $b } @a }' file

Los experimentos se realizaron usando:

  • 10 ejecuciones secuenciales en una máquina rápida, promediada
  • Perl 5.24
  • awk 3.1.5 (gawk 4.1.0 veces fueron ~ 2% más rápido)
  • El archivo de entrada es una monstruosidad de línea de 550MB y 6 millones (British Corpus txt)

Resultados:

  • La solución fgm bash tarda 400 veces más que las soluciones awk (utilizando un caso de prueba truncado de 100000 líneas). Funciona bien, solo lleva mucho tiempo.
  • La solución jonathan awk tomó 25 segundos
  • anubhava awk solución tomó 24 segundos
  • La solución # 2 de neillb awk tomó 23 segundos
  • La solución # 1 de neillb awk tomó 20 segundos
  • mi solución perl tomó 11.6 segundos
  • la perl soution de Caleb tomó 11.2 segundos