Supprimez toutes les lignes du fichier A qui contiennent les chaînes du fichier B

15

J'ai un fichier CSV users.csvavec une liste de UserNames, userIDs et autres données:

username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"Paul McCartny", 30923833, "left", "black"
"Ringo Starr", 77392318, "right", "blue"
"George Harrison", 72349482, "left", "green"

Dans un autre fichier, toremove.txtj'ai une liste d'utilisateurs:

30923833
77392318

Existe-t-il un moyen intelligent et efficace de supprimer toutes les lignes du users.csvfichier contenant les ID toremove.txt? J'ai écrit une simple application Python pour analyser les deux fichiers et écrire dans un nouveau fichier uniquement les lignes qui ne se trouvent pas toremove.txt, mais c'est extrêmement lent. Peut-être que certains sedou la awkmagie peuvent aider ici?

C'est le résultat souhaité, compte tenu des exemples ci-dessus:

username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"
dotancohen
la source
Vous devriez peut-être partager votre script python. Je soupçonne qu'il y a quelque chose qui ne va pas, comme être O (N²), bien que si vous gardez et supprimez des millions de disques, la magie ne vous aidera pas trop.
Ángel
Le script est en fait O (n <sup> 2 </sup>): n pour les users.csvlignes du fichier, et n pour les lignes de toremove.txt. Je ne sais pas vraiment comment le faire avec une complexité moindre. L'essentiel de c'est: for u in users: if not any(toremove in u): outputfile.write(u). Je peux le poster sur Code Review.
dotancohen
1
Je lirais toremove.txten sauvegardant les entrées sous forme de clés . Itérer les utilisateurs.csv, en imprimant ceux dont l'id n'est pas dans le dict. Vous obtenez le traitement O (n) pour les deux toremove.txtet users.csv, et l'utilisation de la mémoire O (n) pour toremove.txt(qui est probablement relativement petite)
Ángel
@ Ángel: Oui, c'est exactement comme ça que le script fonctionne!
dotancohen
1
Vérifier si une clé existe dans un dictionnaire, équivaut à une vérification de table de hachage, qui est (presque) O (1). D'un autre côté, s'il a besoin d'itérer les éléments à supprimer, c'est O (m)
Ángel

Réponses:

15

Avec grep, vous pouvez faire:

$ grep -vwF -f toremove.txt users.txt 
username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"

Avec awk:

$ awk -F'[ ,]' 'FNR==NR{a[$1];next} !($4 in a)' toremove.txt users.txt 
username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"
cuonglm
la source
@terdon: Dang! J'allais le dire. Notez, cependant, que la réponse de Gnouc (sans doute) fait ce que la question demande , mais ce n'est peut-être pas ce que l'utilisateur souhaite.
Scott
La awksolution est très sensible au formatage des fichiers exactement comme indiqué dans la question. Le plus flagrant, si un nom est juste un mot / jeton (c'est-à-dire qu'il ne contient aucun espace; par exemple, "Bono") ou est plus de deux jetons (c'est-à-dire qu'il contient plus d'un espace; par exemple, "Sir Paul McCartney"), il passera même si le correspond à l'ID utilisateur. Moins évidemment, la même chose se produit s'il n'y a pas d'espace entre la première virgule et l'ID utilisateur, ou s'il y a plus d'un espace (par exemple, "John Lennon", 90123412, …).
Scott
@Scott: Oui, c'est la raison pour laquelle j'ai mis la awksolution derrièregrep
cuonglm
4

Voici la awkréponse de Gnouc , modifiée pour être aveugle à l'espace:

awk -F, 'FNR==NR{a[$1];next} !(gensub("^ *","",1,$2) in a)' toremove.txt users.csv

Puisqu'il n'utilise que des virgules (et non des espaces) comme délimiteurs, $1is "John Lennon", $2is  90123412(avec un espace de tête), etc. Nous utilisons donc gensubpour supprimer n'importe quel nombre d'espaces de tête $2 avant de vérifier s'il (l'ID utilisateur) était dans le toremove.txtfichier.

Scott
la source
Vous pourriez peut-être faire d'autres choses intelligentes ici (penser juste à haute voix) comme analyser la "pièce exacte" de la chaîne qui ne devrait pas correspondre, et la comparer avec le tableau associatif, ou autre chose.
rogerdpack
Je crois que c'est ce que je fais. Qu'avais tu en tête?
Scott
Oui, vous l'êtes. Je faisais juste référence à si vous aviez besoin de faire quelque chose de plus génial comme supprimer la première moitié d'une ligne ou quelque chose comme ça (downcasing, etc. stackoverflow.com/a/4784647/32453 ) juste une analyse spécialisée
rogerdpack
0

OK d'une manière rubis: si vous avez une liste de chaînes dans un fichier et que vous souhaitez supprimer toutes les lignes d'un autre fichier qui contiennent même n'importe quelle chaîne dans le premier fichier (dans ce cas, supprimer "fichier2" de "fichier1") fichier ruby :

b=File.read("file2").split # subtract this one out
remove_regex = Regexp.new(b.join('|'))
File.open("file1", "r").each_line do |line|
  if line !~ remove_regex
    puts line
  end
end

malheureusement, avec un gros fichier "à supprimer", cela semble dégrader la complexité en O (N ^ 2) (mon hypothèse est que l'expression régulière a beaucoup de travail à faire), mais peut encore être utile à quelqu'un là-bas (si vous veulent plus que la suppression des lignes complètes). Cela pourrait être plus rapide dans certains cas.

Une autre option si vous optez pour la vitesse consiste à utiliser le même mécanisme de vérification du hachage, mais à "analyser" soigneusement la ligne pour les chaînes qui pourraient correspondre, puis à les comparer avec votre hachage.

En rubis, cela pourrait ressembler à ceci:

b=File.read("file2").split # subtract this one out
hash={}
for line in b
  hash[line] = 1
end

ARGF.each_line do |line|
  ok = true
  for number in line.scan(/\d{9}/)
    if hash.key? number
      ok=false
    end
  end
  if (ok)
    puts line
  end
end

Voir aussi la réponse de Scott, similaire aux réponses awk proposées ici, et évite la complexité O (N ^ 2) (ouf).

rogerdpack
la source