Grep un grand nombre de modèles à partir d'un fichier énorme

18

J'ai un fichier qui augmente d'environ 200 000 lignes par jour, et il est formé de blocs de trois lignes en tant que tels:

1358726575123       # key
    Joseph Muller   # name
    carpenter       # job
9973834728345
    Andres Smith
    student
7836472098652
    Mariah Anthony
    dentist

Maintenant, j'ai un autre fichier à partir duquel j'extrais environ 10 000 modèles de clés, tels que 1358726575123. Ensuite, je lance une forboucle avec ces modèles et je dois les comparer avec le premier fichier. Si le fichier ne contient pas un tel modèle, j'enregistre le modèle dans un troisième fichier pour un traitement ultérieur:

for number in $(grep -o '[0-9]\{12\}' file2); do  # finds about 10.000 keys
     if ! grep -q ^$number$ file1; then           # file1 is a huge file
         printf "$number\n" >>file3               # we'll process file3 later
     fi
done

L'exemple de code récupère un énorme fichier 10 000 fois et j'exécute cette boucle environ une fois par minute, pendant toute la journée .

Étant donné que l'énorme fichier continue de croître, que puis-je faire pour accélérer tout cela et économiser du processeur? Je me demande si le tri du fichier par sa clé (si oui, comment?) Ou l'utilisation d'une base de données au lieu de texte brut aiderait ...

Teresa e Junior
la source

Réponses:

11

Cette réponse est basée sur la awkréponse postée par potong ..
Elle est deux fois plus rapide que la commméthode (sur mon système), pour les mêmes 6 millions de lignes dans le fichier principal et 10 mille clés ... (maintenant mis à jour pour utiliser FNR, NR)

Bien qu'il awksoit plus rapide que votre système actuel, et vous donnera à vous et à vos ordinateurs un peu de répit, sachez que lorsque le traitement des données est aussi intense que vous l'avez décrit, vous obtiendrez les meilleurs résultats globaux en passant à une base de données dédiée; par exemple. SQlite, MySQL ...


awk '{ if (/^[^0-9]/) { next }              # Skip lines which do not hold key values
       if (FNR==NR) { main[$0]=1 }          # Process keys from file "mainfile"
       else if (main[$0]==0) { keys[$0]=1 } # Process keys from file "keys"
     } END { for(key in keys) print key }' \
       "mainfile" "keys" >"keys.not-in-main"

# For 6 million lines in "mainfile" and 10 thousand keys in "keys"

# The awk  method
# time:
#   real    0m14.495s
#   user    0m14.457s
#   sys     0m0.044s

# The comm  method
# time:
#   real    0m27.976s
#   user    0m28.046s
#   sys     0m0.104s

Peter.O
la source
C'est rapide, mais je ne comprends pas grand chose d'awk: à quoi devraient ressembler les noms de fichiers? J'ai essayé file1 -> mainfileet file2 -> keysavec gawk et mawk, et il émet de mauvaises touches.
Teresa e Junior
file1 a des clés, des noms et des travaux.
Teresa e Junior
'mainfile' est le gros fichier (avec clés, noms et travaux). Je viens d' appeler cela « mainfile parce que je continuais à se lever mélangé dossier qui était qui (fichier1 vs fichier2) .. « clés » contient les 10 mille, mais beaucoup, clés .. Pour votre situaton NE PAS redirect anyting. .. utilisez simplement file1 EOF file2 Ce sont les noms de vos fichiers .. "EOF" est un fichier creadte d'une ligne par le script pour indiquer la fin du premier fichier (fichier de données principal) et le début du deuxième fichier ( . touches) awkvous permettent de lire dans une série de fichiers .. dans ce cas , cette série a 3 fichiers qu'il contient La sortie va.stdout
Peter.O
Ce script imprimera toutes les clés qui sont présentes dans mainfile, ET il imprimera également toutes les clés du keysfichier qui ne sont pas dans mainfile... C'est probablement ce qui se passe ... (Je vais y aller un peu plus loin ...
Peter.O
Merci, @ Peter.O! Étant donné que les fichiers sont confidentiels, j'essaie de créer des exemples de fichiers avec $RANDOMpour le téléchargement.
Teresa e Junior
16

Le problème, bien sûr, est que vous exécutez grep sur le gros fichier 10 000 fois. Vous ne devez lire les deux fichiers qu'une seule fois. Si vous voulez rester en dehors des langages de script, vous pouvez le faire de cette façon:

  1. Extraire tous les numéros du fichier 1 et les trier
  2. Extraire tous les numéros du fichier 2 et les trier
  3. Exécuter commsur les listes triées pour obtenir ce qui n'est que sur la deuxième liste

Quelque chose comme ça:

$ grep -o '^[0-9]\{12\}$' file1 | sort -u -o file1.sorted
$ grep -o  '[0-9]\{12\}'  file2 | sort -u -o file2.sorted
$ comm -13 file1.sorted file2.sorted > file3

Tu vois man comm.

Si vous pouviez tronquer le gros fichier tous les jours (comme un fichier journal), vous pourriez garder un cache de numéros triés et n'auriez pas besoin de l'analyser en entier à chaque fois.

angus
la source
1
Soigné! 2 secondes (sur des disques pas particulièrement rapides) avec 200 000 entrées de lignes aléatoires dans le fichier principal (soit 600 000 lignes) et 143 000 clés aléatoires (c'est ainsi que mes données de test se sont retrouvées) ... testées, et cela fonctionne (mais vous saviez que: ) ... Je me {12}
pose des
2
Juste une petite note, vous pouvez le faire sans traiter les fichiers temporaires en utilisant l' <(grep...sort)emplacement des noms de fichiers.
Kevin
Merci, mais saluer et trier les fichiers prend beaucoup plus de temps que ma boucle précédente (+ 2min.).
Teresa e Junior
@Teresa e Junior. Quelle est la taille de votre fichier principal? ... Vous avez mentionné qu'il augmente à 200 000 lignes par jour, mais pas à quel point il est important ... Pour réduire la quantité de données que vous devez traiter, vous pouvez lire uniquement les 200 000 lignes de la journée en prenant note de le dernier numéro de ligne traité (hier) et utilisé tail -n +$linenumpour sortir uniquement les dernières données. De cette façon, vous ne traiterez qu'environ 200 000 lignes par jour .. Je viens de le tester avec 6 millions de lignes dans le fichier principal et 10 000 clés ... temps : réel 0m0.016s, utilisateur 0m0.008s, sys 0m0.008s
Peter.O
Je suis vraiment très perplexe / curieux de savoir comment vous pouvez greper votre fichier principal 10 000 fois et le trouver plus rapidement que cette méthode qui ne le greps qu'une seule fois (et une fois pour le fichier beaucoup plus petit1 ) ... Même si votre tri prend plus de temps que mon test, je n'arrive pas à comprendre l'idée que la lecture d'un gros fichier qui souvent ne l'emporte pas sur un seul tri (dans le temps)
Peter.O
8

Oui, utilisez certainement une base de données. Ils sont faits exactement pour des tâches comme celle-ci.

Mika Fischer
la source
Merci! Je n'ai pas beaucoup d'expérience avec les bases de données. Quelle base de données recommandez-vous? J'ai installé MySQL et la commande sqlite3.
Teresa e Junior
1
Ils sont tous les deux parfaits pour cela, sqlite est plus simple car c'est simplement un fichier et une API SQL pour y accéder. Avec MySQL, vous devez configurer un serveur MySQL pour l'utiliser. Bien que ce ne soit pas très difficile non plus, sqlite pourrait être préférable de commencer.
Mika Fischer
3

Cela pourrait fonctionner pour vous:

 awk '/^[0-9]/{a[$0]++}END{for(x in a)if(a[x]==1)print x}' file{1,2} >file3

ÉDITER:

Script modifié pour permettre les doublons et les clés inconnues dans les deux fichiers, produit toujours des clés à partir du premier fichier non présentes dans le second:

 awk '/^[0-9]/{if(FNR==NR){a[$0]=1;next};if($0 in a){a[$0]=2}}END{for(x in a)if(a[x]==1)print x}' file{1,2} >file3
potong
la source
Cela manquera de nouvelles clés qui se produisent plus d'une fois dans le fichier principal (et d'ailleurs, qui se produisent plus d'une fois dans le fichier de clés). Il semble exiger que l'incrémentation du nombre de tableaux du fichier principal ne dépasse pas 1, ou une solution de contournement équivalente (+1 parce qu'il est assez proche de la marque)
Peter.O
1
J'ai essayé avec gawk et mawk, et cela génère des touches erronées ...
Teresa e Junior
@ Peter.OI a supposé que le fichier principal avait des clés uniques et que le fichier 2 était un sous-ensemble du fichier principal.
potong
@potong Le second fonctionne bien et très rapidement! Je vous remercie!
Teresa e Junior
@Teresa e Junior Êtes-vous sûr qu'il fonctionne encore correctement? .. En utilisant les données de test que vous avez fournies , qui devraient produire 5000 clés, lorsque je l'exécute, il produit 136703 clés, tout comme j'ai obtenu jusqu'à ce que je comprenne enfin quelles étaient vos exigences. ... @potong Bien sûr! FNR == NR (je ne l'ai jamais utilisé auparavant :)
Peter.O
2

Avec autant de données, vous devriez vraiment passer à une base de données. En attendant, une chose que vous devez faire pour obtenir des performances décentes est de ne pas rechercher file1séparément chaque clé. Exécutez-en un greppour extraire toutes les clés non exclues à la fois. Étant donné que cela greprenvoie également les lignes qui ne contiennent pas de clé, filtrez-les.

grep -o '[0-9]\{12\}' file2 |
grep -Fxv -f - file1 |
grep -vx '[0-9]\{12\}' >file3

( -Fxsignifie littéralement rechercher des lignes entières. -f -signifie lire une liste de modèles à partir d'une entrée standard.)

Gilles 'SO- arrête d'être méchant'
la source
Sauf erreur, cela ne résout pas le problème du stockage des clés qui ne sont pas dans le gros fichier, il stockera les clés qui s'y trouvent.
Kevin
@Kevin exactement, et cela m'a obligé à utiliser la boucle.
Teresa e Junior
@TeresaeJunior: l'ajout de -v( -Fxv) peut s'en occuper.
pause jusqu'à nouvel ordre.
@DennisWilliamson Cela sélectionnerait toutes les lignes du gros fichier qui ne correspondent à aucune dans le fichier clé, y compris les noms, les travaux, etc.
Kevin
@Kevin Merci, j'ai mal lu la question. J'ai ajouté un filtre pour les lignes non clés, même si ma préférence va maintenant à l' utilisationcomm .
Gilles 'SO- arrête d'être méchant'
2

Permettez-moi de renforcer ce que les autres ont dit: "Accédez à une base de données!"

Des binaires MySQL sont disponibles gratuitement pour la plupart des plateformes.

Pourquoi pas SQLite? Il est basé sur la mémoire, charge un fichier plat lorsque vous le démarrez, puis le ferme lorsque vous avez terminé. Cela signifie que si votre ordinateur tombe en panne ou que le processus SQLite disparaît, il en va de même pour toutes les données.

Votre problème ne ressemble qu'à quelques lignes de SQL et s'exécutera en millisecondes!

Après avoir installé MySQL (que je recommande par rapport à d'autres choix), je débourserais 40 $ pour le livre de recettes SQL d'O'Reilly par Anthony Molinaro, qui présente de nombreux modèles de problèmes, commençant par des SELECT * FROM tablerequêtes simples et passant par des agrégats et plusieurs jointures.

Jan Steinman
la source
Oui, je vais commencer la migration de mes données vers SQL dans quelques jours, merci! Cependant, les scripts awk m'ont beaucoup aidé jusqu'à ce que tout soit fait!
Teresa e Junior
1

Je ne sais pas si c'est la sortie exacte que vous recherchez, mais probablement le moyen le plus simple est:

grep -o '[0-9]\{12\}' file2 | sed 's/.*/^&$/' > /tmp/numpatterns.grep
grep -vf /tmp/numpatterns.grep file1 > file3
rm -f /tmp/numpatterns.grep

Vous pouvez également utiliser:

sed -ne '/.*\([0-9]\{12\}.*/^\1$/p' file2 > /tmp/numpatterns.grep
grep -vf /tmp/numpatterns.grep file1 > file3
rm -f /tmp/numpatterns.grep

Chacun d'eux crée un fichier de motif temporaire qui est utilisé pour extraire les nombres du grand fichier ( file1).

Arcege
la source
Je crois que cela aussi trouve des chiffres qui sont dans le gros dossier, pas ceux qui ne le sont pas.
Kevin
Correct, je n'ai pas vu le '!' dans le PO. Juste besoin d'utiliser grep -vfau lieu de grep -f.
Arcege
2
Non @arcege, grep -vf n'affichera pas les clés qui ne correspondent pas, il affichera tout, y compris les noms et les travaux.
Teresa e Junior
1

Je suis entièrement d'accord avec vous pour obtenir une base de données (MySQL est assez facile à utiliser). Jusqu'à ce que vous exécutiez cela, j'aime la commsolution d'Angus , mais tellement de gens essaient grepet se trompent que je pensais montrer la (ou au moins une) bonne façon de le faire grep.

grep -o '[0-9]\{12\}' keyfile | grep -v -f <(grep -o '^[0-9]\{12\}' bigfile) 

Le premier greprécupère les clés. Le troisième grep(dans le <(...)) prend toutes les clés utilisées dans le gros fichier, et le <(...)passe comme un fichier comme argument -fdans le deuxième grep. Cela oblige le second grepà l'utiliser comme une liste de lignes à faire correspondre. Il l'utilise ensuite pour faire correspondre son entrée (la liste des clés) à partir du canal (en premier grep), et imprime toutes les clés extraites du fichier de clés et non ( -v) le gros fichier.

Bien sûr, vous pouvez le faire avec des fichiers temporaires dont vous devez garder la trace et ne pas oublier de supprimer:

grep -o '[0-9]\{12\}'  keyfile >allkeys
grep -o '^[0-9]\{12\}' bigfile >usedkeys
grep -v -f usedkeys allkeys

Cela imprime toutes les lignes allkeysqui n'apparaissent pas dans usedkeys.

Kevin
la source
Malheureusement, c'est lent et j'obtiens une erreur de mémoire après 40 secondes:grep: Memory exhausted
Peter.O
@ Peter.O Mais c'est correct. Quoi qu'il en soit, c'est pourquoi je suggère une base de données ou comm, dans cet ordre.
Kevin
Oui, cela fonctionne, mais est beaucoup plus lent que la boucle.
Teresa e Junior
1

Le fichier clé ne change pas? Ensuite, vous devez éviter de rechercher encore et encore les anciennes entrées.

Avec tail -fvous pouvez obtenir la sortie d'un fichier en pleine croissance.

tail -f growingfile | grep -f keyfile 

grep -f lit les motifs dans un fichier, une ligne comme motif.

Utilisateur inconnu
la source
Ce serait bien, mais le fichier clé est toujours différent.
Teresa e Junior
1

N'allait pas publier ma réponse parce que je pensais qu'une telle quantité de données ne devrait pas être traitée avec un script shell, et la bonne réponse pour utiliser une base de données était déjà donnée. Mais depuis maintenant, il existe 7 autres approches ...

Lit le premier fichier en mémoire, puis recherche le deuxième fichier pour les nombres et vérifie si les valeurs sont stockées en mémoire. Cela devrait être plus rapide que plusieurs greps, si vous avez suffisamment de mémoire pour charger le fichier entier, c'est-à-dire.

declare -a record
while read key
do
    read name
    read job
    record[$key]="$name:$job"
done < file1

for number in $(grep -o '[0-9]\{12\}' file2)
do
    [[ -n ${mylist[$number]} ]] || echo $number >> file3
done
forcefsck
la source
J'ai assez de mémoire, mais j'ai trouvé celui-ci encore plus lent. Merci quand même!
Teresa e Junior
1

Je suis d'accord avec @ jan-steinman que vous devez utiliser une base de données pour ce type de tâche. Il existe de nombreuses façons de pirater une solution avec un script shell comme le montrent les autres réponses, mais le faire de cette façon entraînera beaucoup de misère si vous allez utiliser et maintenir le code pendant une durée supérieure à juste un projet jetable d'une journée.

En supposant que vous êtes sur une boîte Linux, vous avez probablement installé Python par défaut qui inclut la bibliothèque sqlite3 à partir de Python v2.5. Vous pouvez vérifier votre version Python avec:

% python -V
Python 2.7.2+

Je recommande d'utiliser la bibliothèque sqlite3 car c'est une solution simple basée sur des fichiers qui existe pour toutes les plateformes (y compris à l'intérieur de votre navigateur Web!) Et qui ne nécessite pas l'installation d'un serveur. Essentiellement zéro configuration et zéro entretien.

Vous trouverez ci-dessous un simple script python qui analysera le format de fichier que vous avez donné en exemple, puis effectue une simple requête "tout sélectionner" et génère tout ce qu'il a stocké dans la base de données.

#!/usr/bin/env python

import sqlite3
import sys

dbname = '/tmp/simple.db'
filename = '/tmp/input.txt'
with sqlite3.connect(dbname) as conn:
    conn.execute('''create table if not exists people (key integer primary key, name text, job text)''')
    with open(filename) as f:
        for key in f:
            key = key.strip()
            name = f.next().strip()
            job = f.next().strip()
            try:
                conn.execute('''insert into people values (?,?,?)''', (key, name, job))
            except sqlite3.IntegrityError:
                sys.stderr.write('record already exists: %s, %s, %s\n' % (key, name, job))
    cur = conn.cursor()

    # get all people
    cur.execute('''select * from people''')
    for row in cur:
        print row

    # get just two specific people
    person_list = [1358726575123, 9973834728345]
    cur.execute('''select * from people where key in (?,?)''', person_list)
    for row in cur:
        print row

    # a more general way to get however many people are in the list
    person_list = [1358726575123, 9973834728345]
    template = ','.join(['?'] * len(person_list))
    cur.execute('''select * from people where key in (%s)''' % (template), person_list)
    for row in cur:
        print row

Oui, cela signifie que vous devrez apprendre du SQL , mais cela en vaudra la peine à long terme. De plus, au lieu d'analyser vos fichiers journaux, vous pouvez peut-être écrire des données directement dans votre base de données sqlite.

aculich
la source
Merci pour le script python! Je pense que cela /usr/bin/sqlite3fonctionne de la même manière pour les scripts shell ( packages.debian.org/squeeze/sqlite3 ), même si je ne l'ai jamais utilisé.
Teresa e Junior
Oui, vous pouvez l'utiliser /usr/bin/sqlite3avec des scripts shell, mais je recommande d'éviter les scripts shell, sauf pour les programmes simples à jeter et d'utiliser à la place un langage comme python qui a une meilleure gestion des erreurs et est plus facile à maintenir et à développer.
aculich