web-dev-qa-db-fra.com

Trier un fichier texte par longueur de ligne, espaces compris

J'ai un fichier CSV qui ressemble à ceci

 AS2345, ASDF1232, M. Plain Exemple, 110 avenue binaire, Atlantis, RI, 12345, (999) 123-5555,1.56 
 AS2345, ASDF1232, Mme. Plain Exemple, 1121110 st sternaire . 110 Avenue binaire, Atlantis, RI, 12345, (999) 123-5555,1,56 
 AS2345, ASDF1232, M. Plain Example, 110 avenue binaire, Liberty City, RI, 12345, (999) 123 -5555,1,56 
 AS2345, ASDF1232, M. Plain Exemple, 110 avenue Ternary, Une ville, RI, 12345, (999) 123-5555,1,56 

Je dois le trier par longueur de ligne, espaces compris. La commande suivante n'inclut pas d'espaces. Y a-t-il un moyen de le modifier pour que cela fonctionne pour moi?

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

Répondre

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

Ou, pour faire votre sous-tri initial (peut-être involontaire) de lignes égales:

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

Dans les deux cas, nous avons résolu votre problème en nous écartant de awk pour votre dernière coupe.

Lignes de longueur assortie - que faire en cas d'égalité:

La question ne précisait pas si un tri supplémentaire était souhaité pour les lignes de longueur correspondante. J'ai supposé que c'était indésirable et ai suggéré l'utilisation de -s (--stable) pour éviter que ces lignes ne soient triées les unes contre les autres et conservez-les dans l'ordre relatif dans lequel elles apparaissent dans l'entrée.

(Ceux qui veulent plus de contrôle sur le tri de ces liens peuvent regarder le tri de --key option.)

Pourquoi la solution tentée a-t-elle échoué (reconstruction de ligne awk):

Il est intéressant de noter la différence entre:

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

Ils cèdent respectivement

hello   awk   world
hello awk world

La section pertinente du manuel (de gawk) mentionne seulement de façon indirecte que awk reconstruira l'intégralité de $ 0 (en fonction du séparateur, etc.) lorsque vous modifiez un champ. Je suppose que ce n'est pas un comportement fou. Il a ceci:

"Enfin, il est parfois utile de forcer awk à reconstruire l'intégralité de l'enregistrement, en utilisant la valeur actuelle des champs et OFS. Pour ce faire, utilisez la tâche apparemment anodine:"

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

"Cela oblige awk à reconstruire le disque."

Entrée de test comprenant quelques lignes d'égale longueur:

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
192
neillb

La solution AWK de neillb est très bien si vous voulez vraiment utiliser awk et explique pourquoi c'est compliqué, mais si ce que vous voulez, c'est que le travail soit fait rapidement et que vous ne le fassiez pas. Peu importe ce que vous faites, une solution consiste à utiliser la fonction sort() de Perl avec une routine de paramétrage personnalisée pour effectuer une itération sur les lignes en entrée. Voici un one-liner:

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

Vous pouvez mettre cela dans votre pipeline où vous en avez besoin, soit en recevant STDIN (de cat ou une redirection de shell), soit en donnant le nom du fichier à Perl comme autre argument et en le laissant ouvrir le fichier.

Dans mon cas, j'avais besoin des lignes les plus longues en premier, alors j'ai échangé $a et $b dans la comparaison.

20
Caleb

Essayez cette commande à la place:

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

Résultats de référence

Vous trouverez ci-dessous les résultats d’une analyse comparative des solutions proposées ci-après.

Méthode d'essai

  • 10 exécutions séquentielles sur une machine rapide, moyennées
  • Perl 5.24
  • awk 3.1.5 (gawk 4.1.0 fois environ ~ 2% plus rapide)
  • Le fichier d’entrée est une monstruosité de 550Mo, 6 millions de lignes (British National Corpus txt)

Résultats

  1. La solution Perl de Caleb a pris 11,2 secondes
  2. ma solution Perl a pris 11,6 secondes
  3. La solution awk de neillb # 1 a pris 20 secondes
  4. La solution awk de neillb # 2 a pris 23 secondes
  5. La solution awk d'Anubhava a pris 24 secondes.
  6. La solution awk de Jonathan a pris 25 secondes
  7. La solution bash de Fretz prend 400 fois plus longtemps que les solutions awk (en utilisant un scénario de test tronqué de 100 000 lignes). Cela fonctionne bien, prend juste pour toujours.

Option supplémentaire Perl

De plus, j'ai ajouté une autre solution Perl:

Perl -ne 'Push @a, $_; END{ print sort { length $a <=> length $b } @a }' file
7
Chris Koknat

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
5
Fritz G. Mehner

La fonction length() inclut des espaces. Je ferais juste des ajustements mineurs à votre pipeline (y compris en évitant UUOC ).

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

La commande sed supprime directement les chiffres et les points ajoutés par la commande awk. Sinon, conservez votre mise en forme de awk:

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

Avec POSIX Awk:

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

Exemple

2
Steven Penny

1) solution d'awk pure. Supposons que la longueur de la ligne ne puisse pas dépasser 1024 alors

cat nom du fichier | awk 'BEGIN {min = 1024; s = "";} {l = longueur ($ 0); si (l <min) {min = l; s = $ 0;}} END {print s} '

2) une solution de liner bash en supposant que toutes les lignes ont seulement 1 mot, mais peut être retravaillée pour tous les cas où toutes les lignes ont le même nombre de mots:

LINES = $ (cat filename); pour k dans $ LINES; faire printf "$ k"; echo $ k | wc -L; fait | trier -k2 | tête -n 1 | cut -d "" -f1

2
Michael Yuniverg

J'ai trouvé que ces solutions ne fonctionneraient pas si votre fichier contenait des lignes commençant par un nombre, car elles seraient triées numériquement avec toutes les lignes comptées. La solution consiste à donner à sort l'indicateur -g (Tri général-numérique) au lieu de -n (Tri numérique):

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

Voici une méthode compatible avec plusieurs octets pour trier les lignes par longueur. Cela demande:

  1. wc -m Est disponible pour vous (macOS l'a).
  2. Les paramètres régionaux actuels prennent en charge les caractères multi-octets, par exemple, en définissant LC_ALL=UTF-8. Vous pouvez le définir soit dans votre profil .bash, ou simplement en le ajoutant avant la commande suivante.
  3. testfile a un codage de caractères correspondant à votre langue (par exemple, UTF-8).

Voici la commande complète:

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-

Expliquer partie par partie:

  • l=$0; gsub(/\047/, "\047\"\047\"\047", l); ← crée une copie de chaque ligne dans la variable awk l et double-échappe tous les ' afin que la ligne puisse être répercutée en tant que une commande Shell (\047 est une simple citation en notation octale).
  • cmd=sprintf("echo \047%s\047 | wc -m", l); ← C'est la commande à exécuter, qui renvoie la ligne échappée à wc -m.
  • cmd | getline c; ← exécute la commande et copie la valeur du nombre de caractères renvoyée dans la variable awk c.
  • close(cmd); ← ferme le canal de la commande Shell pour éviter de limiter le nombre de fichiers ouverts dans un processus.
  • sub(/ */, "", c); ← élimine les espaces à partir du nombre de caractères renvoyé par wc.
  • { print c, $0 } ← imprime la valeur du nombre de caractères de la ligne, un espace et la ligne d'origine.
  • | sort -ns ← trie les lignes (par nombre de caractères ajoutés) numériquement (-n) Et maintient l'ordre de tri stable (-s).
  • | cut -d" " -f2- ← supprime les valeurs de nombre de caractères ajoutées.

C'est lent (seulement 160 lignes par seconde sur un Macbook Pro rapide) car il doit exécuter une sous-commande pour chaque ligne.

Vous pouvez également le faire uniquement avec gawk (à partir de la version 3.1.5, gawk prend en charge plusieurs octets), ce qui serait nettement plus rapide. Il est très difficile de faire toutes les échappements et les guillemets pour passer les lignes en toute sécurité via une commande Shell de awk, mais c'est la seule méthode que je n'ai pas pu trouver et qui ne nécessite pas l'installation de logiciel supplémentaire (gawk n'est pas disponible par défaut sur macOS).

1
Quinn Comendant