Sélectionnez les lignes du fichier texte dont les identifiants sont répertoriés dans un autre fichier


13

J'utilise beaucoup de tri grek awk dans mon shell Unix pour travailler avec des fichiers texte de colonne de taille moyenne (environ 10 à 100 millions de lignes) séparés par des tabulations. À cet égard, le shell Unix est ma feuille de calcul.

Mais j'ai un gros problème, c'est la sélection des enregistrements en fonction d'une liste d'ID.

Ayant un table.csvfichier au format id\tfoo\tbar...et un ids.csvfichier avec la liste des identifiants, sélectionnez uniquement les enregistrements table.csvdont l'identifiant est présent dans ids.csv.

type de /programming/13732295/extract-all-lines-from-text-file-based-on-a-given-list-of-ids mais avec shell, pas perl.

grep -Fproduit évidemment des faux positifs si les identifiants sont de largeur variable. joinest un utilitaire que je ne pourrais jamais comprendre. Tout d'abord, cela nécessite un tri alphabétique (mes fichiers sont généralement triés numériquement), mais même dans ce cas, je ne peux pas le faire fonctionner sans me plaindre d'un ordre incorrect et sans sauter certains enregistrements. Donc je n'aime pas ça. grep -f par rapport au fichier avec ^id\t-s est très lent lorsque le nombre d'identifiants est élevé. awkest encombrant.

Existe-t-il de bonnes solutions pour cela? Des outils spécifiques pour les fichiers séparés par des tabulations? Des fonctionnalités supplémentaires seront également les bienvenues.

UPD: corrigé sort->join


Si elle grep -fest trop lente, le maintien de cette stratégie semble plus difficile que cela en vaut la peine - les variations seront probablement la proie des mêmes problèmes de performances O (N * M). Peut-être que votre temps serait mieux consacré à apprendre à utiliser une base de données SQL normalisée ...
goldilocks

1
Pourquoi ne pas utiliser le script Perl de la question que vous avez liée? Alternativement, il devrait être possible d'écrire un script similaire dans awk.
cjm

Bash 4 a des tableaux associatifs, ce dont vous avez besoin pour contourner les boucles imbriquées à l'exemple de Perl.
goldilocks

1
sortpeut faire toutes sortes de tri, numérique, alphabétique et autres. Tu vois man sort.
terdon

J'ai une requête ici, comment pouvons-nous faire de même si le fichier source d'où nous voulons extraire les données est un fichier non délimité

Réponses:


19

Je suppose que vous grep -fne vouliez pas dire cela, grep -Fmais vous avez en fait besoin d'une combinaison des deux et -w:

grep -Fwf ids.csv table.csv

La raison pour laquelle vous obtenez des faux positifs est (je suppose, vous ne l'avez pas expliqué) car si un identifiant peut être contenu dans un autre, les deux seront imprimés. -wsupprime ce problème et -Fs'assure que vos modèles sont traités comme des chaînes et non comme des expressions régulières. De man grep:

   -F, --fixed-strings
          Interpret PATTERN as a  list  of  fixed  strings,  separated  by
          newlines,  any  of  which is to be matched.  (-F is specified by
          POSIX.)
   -w, --word-regexp
          Select  only  those  lines  containing  matches  that form whole
          words.  The test is that the matching substring must  either  be
          at  the  beginning  of  the  line,  or  preceded  by  a non-word
          constituent character.  Similarly, it must be either at the  end
          of  the  line  or  followed by a non-word constituent character.
          Word-constituent  characters  are  letters,  digits,   and   the
          underscore.

   -f FILE, --file=FILE
          Obtain  patterns  from  FILE,  one  per  line.   The  empty file
          contains zero patterns, and therefore matches nothing.   (-f  is
          specified by POSIX.)

Si vos faux positifs sont dus au fait qu'un ID peut être présent dans un champ non-ID, parcourez votre fichier à la place:

while read pat; do grep -w "^$pat" table.csv; done < ids.csv

ou, plus rapidement:

xargs -I {} grep "^{}" table.csv < ids.csv

Personnellement, je le ferais perlcependant:

perl -lane 'BEGIN{open(A,"ids.csv"); while(<A>){chomp; $k{$_}++}} 
            print $_ if defined($k{$F[0]}); ' table.csv

1
+1 Mais: que se passe-t-il s'il y a des faux positifs potentiels qui correspondent exactement à l'ID mot par mot, mais pas dans la colonne id? Si vous ne pouvez pas utiliser ^avec -F, vous ne pouvez pas cibler spécifiquement la première colonne.
goldilocks

@goldilocks s'ils correspondent exactement, ce ne sont pas des faux positifs. Je comprends ce que vous voulez dire, mais dans ce cas, l'OP devrait montrer leurs fichiers d'entrée.
terdon

Le ^id\tbit de l'OP implique idpeut se produire dans une autre colonne. Sinon, cela n'a pas d'importance.
goldilocks

@goldilocks fair point, réponse modifiée.
terdon

La façon dont nous le faisions était de créer des fichiers temporaires (en utilisant awk ou sed) qui ajoutaient un caractère unique (par exemple, contrôle-A) délimitant le champ que nous voulions rechercher, puis utilisez grep -F -f temppatternfile temptargetfile | tr -d '\ 001'
Mark Plotnick

7

L' joinutilitaire est ce que vous voulez. Il nécessite que les fichiers d'entrée soient triés lexicalement.

En supposant que votre shell est bash ou ksh:

join -t $'\t' <(sort ids.csv) <(sort table.csv)

Sans avoir besoin de trier, la solution habituelle awk est

awk -F '\t' 'NR==FNR {id[$1]; next} $1 in id' ids.csv table.csv

Comme j'ai essayé mais finalement échoué à transmettre, rejoindre est un coup de coude. Ça ne marche pas si bien pour moi.
alamar

1
joinn'est pas un coup de coude: vos mots étaient que vous ne pouviez pas le comprendre. Ouvrez votre esprit et apprenez. Quel résultat avez-vous obtenu et en quoi cela diffère-t-il de ce que vous attendez?
glenn jackman

+1, c'est un travail pour join.
don_crissti

La awksolution ici est très rapide et efficace pour mes besoins (j'extraye des sous-ensembles de quelques centaines de fichiers avec ~ 100 millions de lignes)
Luke

2

Les réponses à cette question SO m'ont aidé à contourner les problèmes avec join. Essentiellement, lorsque vous triez le fichier en vue de l'envoyer à la jointure, vous devez vous assurer que vous triez en fonction de la colonne sur laquelle vous vous joignez. Donc, si c'est le premier, vous devez lui dire quel est le caractère séparateur dans le fichier et que vous voulez qu'il trie sur le premier champ (et seulement le premier champ). Sinon, si le premier champ a des largeurs variables (par exemple), vos séparateurs et éventuellement d'autres champs peuvent commencer à affecter l'ordre de tri.

Donc, utilisez l'option -t de tri pour spécifier votre caractère de séparation, et utilisez l'option -k pour spécifier le champ (en vous rappelant que vous avez besoin d'un champ de début et de fin - même s'il est le même - ou il triera à partir de ce caractère à la fin de la ligne).

Donc, pour un fichier séparé par des tabulations comme dans cette question, ce qui suit devrait fonctionner (avec merci à la réponse de glenn pour la structure):

join -t$'\t' <(sort -d ids.csv) <(sort -d -t$'\t' -k1,1 table.csv) > output.csv

(Pour référence, l'indicateur -d signifie le tri par dictionnaire. Vous pouvez également utiliser l'indicateur -b pour ignorer les espaces principaux, voir man sortet man join).

À titre d'exemple plus général, supposons que vous joignez deux fichiers séparés par des virgules - input1.csvsur la troisième colonne et input2.csvsur la quatrième. Vous pourriez utiliser

join -t, -1 3 -2 4 <(sort -d -t, -k3,3 input2.csv) <(sort -d -t, -k4,4 input2.csv) > output.csv

Ici, les options -1et -2spécifient les champs à joindre respectivement dans les premier et deuxième fichiers d'entrée.


0

Vous pouvez également utiliser ruby ​​pour faire quelque chose de similaire:

ruby -pe 'File.open("id.csv").each { |i| puts i if i =~ /\$\_/ }' table.csv
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.