Trier un fichier texte par longueur de ligne, espaces compris


137

J'ai un fichier CSV qui ressemble à ceci

AS2345, ASDF1232, Mr. Plain Example, 110 Binary ave., Atlantis, RI, 12345, (999) 123-5555,1.56
AS2345, ASDF1232, Mme Plain Example, 1121110 Ternary st. 110 avenue binaire .., Atlantis, RI, 12345, (999) 123-5555,1.56
AS2345, ASDF1232, M. Plain Example, 110 Binary ave., Liberty City, RI, 12345, (999) 123-5555,1.56
AS2345, ASDF1232, Mr. Plain Example, 110 Ternary ave., Some City, 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 la modifier pour qu'elle fonctionne pour moi?

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

21
J'aimerais vraiment vivre dans Binary Avenue ou Ternary Street, ces gens seraient certainement d'accord avec des choses comme "8192 est un nombre rond"
schnaader

Réponses:


224

Répondre

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

Ou, pour faire votre sous-tri original (peut-être involontaire) de toutes les lignes de longueur égale:

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

Dans les deux cas, nous avons résolu votre problème déclaré en nous éloignant de awk pour votre coupe finale.

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

La question ne précisait pas si un autre tri était souhaité ou non pour les lignes de longueur correspondante. J'ai supposé que c'était indésirable et j'ai suggéré l'utilisation de -s( --stable) pour éviter que de telles lignes soient triées les unes par rapport aux autres, et les conserver 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 pourraient envisager l' --keyoption de tri .)

Pourquoi la tentative de solution de la question échoue (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 en aparté que awk va reconstruire la totalité de $ 0 (basé sur le séparateur, etc.) lorsque vous modifiez un champ. Je suppose que ce n'est pas un comportement fou. Il a ceci:

"Enfin, il y a des moments où il est pratique de forcer awk à reconstruire l'intégralité de l'enregistrement, en utilisant la valeur actuelle des champs et OFS. Pour ce faire, utilisez l'affectation 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 de même 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

1
heemayl, oui c'est, merci. J'ai essayé de faire correspondre la forme de la solution tentée par OP dans la mesure du possible, pour lui permettre de se concentrer uniquement sur les différences importantes entre la sienne et la mienne.
neillb

1
Cela vaut la peine de souligner que cat $@c'est cassé aussi. Vous voulez absolument le citer, commecat "$@"
tripleee

27

La solution AWK de neillb est idéale si vous voulez vraiment l'utiliser awket elle explique pourquoi c'est un problème, mais si vous voulez faire le travail rapidement et que vous ne vous souciez pas de ce que vous faites, une solution est d'utiliser sort()Fonction de Perl avec une routine de caparison personnalisée pour parcourir les lignes d'entrée. Voici une seule ligne:

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

Vous pouvez le mettre dans votre pipeline là où vous en avez besoin, soit en recevant STDIN (de catou une redirection shell) ou simplement donner le nom de fichier à perl comme un autre argument et le laisser ouvrir le fichier.

Dans mon cas, j'avais d'abord besoin des lignes les plus longues, alors j'ai échangé $aet $bdans la comparaison.


C'est la meilleure solution car awk provoque un tri inattendu lorsque le fichier d'entrée contient des lignes numériques et alfanumériques. Ici, la commande en ligne: $ cat testfile | perl -e 'print sort {length ($ a) <=> length ($ b)} <>'
alemol

Vite! Fichier de 465000 lignes (un mot par ligne) en <1 seconde, lorsque la sortie est redirigée vers un autre fichier - donc:cat testfile.txt | perl -e 'print sort { length($a) <=> length($b) } <>' > out.txt
cssyphus

Windows avec StrawberryPerl fonctionne:type testfile.txt | perl -e "print sort { length($a) <=> length($b) } <>" > out.txt
bryc

14

Essayez plutôt cette commande:

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

10

Résultats de référence

Vous trouverez ci-dessous les résultats d'un benchmark des solutions issues d'autres réponses à cette question.

Méthode d'essai

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

Résultats

  1. La perlsolution de Caleb a pris 11,2 secondes
  2. ma perlsolution a pris 11,6 secondes
  3. La awksolution n ° 1 de neillb a pris 20 secondes
  4. La awksolution n ° 2 de neillb a pris 23 secondes
  5. La awksolution d'Anubhava a pris 24 secondes
  6. La awksolution de Jonathan a pris 25 secondes
  7. La bashsolution de Fretz prend 400 fois plus de temps que les awksolutions (en utilisant un cas de test tronqué de 100 000 lignes). Cela fonctionne bien, cela prend juste une éternité.

perlOption supplémentaire

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

perl -ne 'push @a, $_; END{ print sort { length $a <=> length $b } @a }' file

6

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

3

La length()fonction inclut des espaces. Je ne ferais que 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 sedcommande supprime directement les chiffres et les deux points ajoutés par la awkcommande. Vous pouvez également conserver votre mise en forme awk:

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

2

J'ai trouvé que ces solutions ne fonctionneront pas si votre fichier contient des lignes commençant par un nombre, car elles seront triées numériquement avec toutes les lignes comptées. La solution est de donner sortle -gdrapeau (general-numeric-sort) au lieu de -n(numeric-sort):

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

2
Salut, Markus. Je n'observe pas le contenu de la ligne (numérique ou non) - par opposition à la longueur de ligne - comme ayant un effet sur le tri, sauf dans le cas des lignes avec des longueurs correspondantes. Est-ce que c'est ce que vous vouliez dire? Dans de tels cas, je n'ai pas trouvé que le passage des méthodes de tri de -nà votre suggérée -gentraînerait une amélioration, donc je ne m'attends pas à ce que ce soit le cas. J'ai maintenant abordé, dans ma réponse, comment interdire le sous-tri des lignes de longueur égale (en utilisant --stable). Que ce soit ce que vous vouliez dire ou non, merci de l'avoir porté à mon attention! J'ai également ajouté une entrée considérée pour tester.
neillb

4
Non, laissez-moi vous expliquer en le décomposant. Seule la awkpièce générera une liste de lignes précédées d'une longueur de ligne et d'un espace. La tuyauterie sort -nfonctionnera comme prévu. Mais si l'une de ces lignes a déjà un nombre au début, ces lignes commenceront par longueur + espace + nombre. sort -nne tient pas compte de cet espace et le traitera comme un nombre concaténé de longueur + nombre. L'utilisation de l' -gindicateur s'arrêtera à la place au premier espace, donnant un tri correct. Essayez-le vous-même en créant un fichier avec des lignes numérotées et exécutez la commande étape par étape.
Markus Amalthea Magnuson

1
J'ai également constaté que cela sort -nne tient pas compte de l'espace et produit un tri incorrect. sort -gproduit le bon ordre.
Robert Smith

Je ne peux pas reproduire le problème décrit avec -ndans sort (GNU coreutils) 8.21. La infodocumentation décrit -gcomme moins efficace et potentiellement moins précise (elle convertit les nombres en flottants), donc ne l'utilisez probablement pas si vous n'en avez pas besoin.
phils

nb documentation pour -n: "Trier numériquement. Le nombre commence chaque ligne et se compose de blancs facultatifs, d'un signe optionnel '-' et de zéro ou plusieurs chiffres éventuellement séparés par des milliers de séparateurs, éventuellement suivis d'un caractère décimal et zéro ou plusieurs chiffres . Un nombre vide est traité comme '0'. La locale 'LC_NUMERIC' spécifie le caractère décimal et le séparateur des milliers. Par défaut, un espace est un espace ou une tabulation, mais la locale 'LC_CTYPE' peut changer cela. "
phils


2

1) solution pure awk. Supposons que la longueur de la ligne ne puisse pas être supérieure à 1024 alors

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

2) une solution de bash de ligne en supposant que toutes les lignes n'ont qu'un seul mot, mais peut être retravaillée pour tous les cas où toutes les lignes ont le même nombre de mots:

LINES = $ (nom de fichier cat); pour k dans $ LINES; faire printf "$ k"; echo $ k | wc -L; fait | sort -k2 | tête -n 1 | couper -d "" -f1


1

Voici une méthode compatible multi-octets de tri des lignes par longueur. Cela demande:

  1. wc -m est disponible pour vous (macOS l'a).
  2. Votre locale actuelle prend en charge les caractères multi-octets, par exemple, en définissant LC_ALL=UTF-8. Vous pouvez le définir soit dans votre .bash_profile, soit simplement en l'ajoutant avant la commande suivante.
  3. testfile a un encodage de caractères correspondant à votre locale (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);← fait une copie de chaque ligne dans la variable awk let double-échappe chaque 'pour que la ligne puisse être renvoyée en toute sécurité comme une commande shell ( \047est un guillemet simple en notation octale).
  • cmd=sprintf("echo \047%s\047 | wc -m", l);← c'est la commande que nous exécuterons, qui fait écho à 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); ← fermez le tube à la commande shell pour éviter d'atteindre une limite système sur le nombre de fichiers ouverts dans un processus.
  • sub(/ */, "", c);← supprime l'espace blanc de la valeur du nombre de caractères renvoyée 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 valeurs de nombre de caractères préfixées) numériquement ( -n) et maintient un ordre de tri stable ( -s).
  • | cut -d" " -f2- ← supprime les valeurs de nombre de caractères ajoutées au début.

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

Sinon, faites-le uniquement avec gawk(à partir de la version 3.1.5, gawk est compatible multi-octets), ce qui serait beaucoup plus rapide. C'est beaucoup de mal à faire toutes les échappements et les doubles guillemets pour passer en toute sécurité les lignes à travers une commande shell de awk, mais c'est la seule méthode que j'ai pu trouver qui ne nécessite pas l'installation de logiciel supplémentaire (gawk n'est pas disponible par défaut sur macOS).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.