J'ai deux fichiers, file1.txt
et file2.txt
. file1.txt
compte environ 14 000 lignes et file2.txt
a environ 2 milliards. file1.txt
a un seul champ f1
par ligne pendant que file2.txt
a 3 champs, f1
à travers f3
, délimité par |
.
Je veux trouver toutes les lignes de file2.txt
où f1
de file1.txt
allumettes f2
de file2.txt
(ou n'importe où sur la ligne si nous ne voulons pas passer plus de temps à diviser les valeurs de file2.txt
).
file1.txt (environ 14K lignes, non trié):
foo1
foo2
...
bar1
bar2
...
file2.txt (environ 2 milliards de lignes, non triées):
date1|foo1|number1
date2|foo2|number2
...
date1|bar1|number1
date2|bar2|number2
...
Sortie attendue:
date1|foo1|number1
date2|foo2|number2
...
date1|bar1|number1
date2|bar2|number2
...
Voici ce que j'ai essayé et cela semble prendre plusieurs heures à courir:
fgrep -F -f file1.txt file2.txt > file.matched
Je me demande s'il existe un moyen meilleur et plus rapide de faire cette opération avec les commandes Unix courantes ou avec un petit script.
Un petit morceau de code Perl a résolu le problème. Voici l'approche adoptée:
file1.txt
dans un hachagefile2.txt
ligne par ligne, analyser et extraire le deuxième champVoici le code:
#!/usr/bin/Perl -w
use strict;
if (scalar(@ARGV) != 2) {
printf STDERR "Usage: fgrep.pl smallfile bigfile\n";
exit(2);
}
my ($small_file, $big_file) = ($ARGV[0], $ARGV[1]);
my ($small_fp, $big_fp, %small_hash, $field);
open($small_fp, "<", $small_file) || die "Can't open $small_file: " . $!;
open($big_fp, "<", $big_file) || die "Can't open $big_file: " . $!;
# store contents of small file in a hash
while (<$small_fp>) {
chomp;
$small_hash{$_} = undef;
}
close($small_fp);
# loop through big file and find matches
while (<$big_fp>) {
# no need for chomp
$field = (split(/\|/, $_))[1];
if (defined($field) && exists($small_hash{$field})) {
printf("%s", $_);
}
}
close($big_fp);
exit(0);
J'ai exécuté le script ci-dessus avec 14K lignes dans file1.txt et 1,3M lignes dans file2.txt. Il s'est terminé en 13 secondes environ, produisant 126 000 matchs. Voici la sortie time
pour la même chose:
real 0m11.694s
user 0m11.507s
sys 0m0.174s
J'ai exécuté le code awk
de @ Inian:
awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (match($0,i)) {print; break}}' file1.txt FS='|' file2.txt
C'était beaucoup plus lent que la solution Perl, car elle boucle 14K fois pour chaque ligne dans file2.txt - ce qui est vraiment cher. Il a avorté après avoir traité 592K enregistrements de file2.txt
et produisant 40K lignes appariées. Voici combien de temps cela a pris:
awk: illegal primary in regular expression 24/Nov/2016||592989 at 592989
input record number 675280, file file2.txt
source line number 1
real 55m5.539s
user 54m53.080s
sys 0m5.095s
Utilisation de l'autre solution awk
de @ Inian, ce qui élimine le problème de boucle:
time awk -F '|' 'FNR==NR{hash[$1]; next}$2 in hash' file1.txt FS='|' file2.txt > awk1.out
real 0m39.966s
user 0m37.916s
sys 0m0.743s
time LC_ALL=C awk -F '|' 'FNR==NR{hash[$1]; next}$2 in hash' file1.txt FS='|' file2.txt > awk.out
real 0m41.057s
user 0m38.475s
sys 0m0.904s
awk
est très impressionnant ici, étant donné que nous n'avons pas eu à écrire un programme entier pour le faire.
J'ai exécuté @=iv Python également. Il a fallu environ 15 heures pour terminer le travail, et il semblait qu'il produisait les bons résultats. Construire une énorme expression régulière n'est pas aussi efficace que d'utiliser un hachage Voici la sortie time
:
real 895m14.862s
user 806m59.219s
sys 1m12.147s
J'ai essayé de suivre la suggestion d'utiliser parallèle . Cependant, il a échoué avec fgrep: memory exhausted
erreur, même avec de très petites tailles de bloc.
Ce qui m'a surpris, c'est que fgrep
était totalement inadapté à cela. Je l'ai avorté après 22 heures et il a produit environ 100 000 matchs. J'aimerais que fgrep
ait une option pour forcer le contenu de -f file
à conserver dans un hachage, tout comme ce que le code Perl a fait.
Je n'ai pas vérifié l'approche join
- je ne voulais pas la surcharge supplémentaire de tri des fichiers. De plus, étant donné les mauvaises performances de fgrep
, je ne pense pas que join
aurait fait mieux que le code Perl.
Merci à tous pour votre attention et vos réponses.
Une solution Perl. [Voir Remarque ci-dessous.]
Utilisez un hachage pour le premier fichier. Lorsque vous lisez le gros fichier ligne par ligne, extrayez le champ par expression régulière (capture le premier motif entre ||
) Ou split
(obtient le deuxième mot) et imprimez-le exists
. Ils diffèrent probablement un peu en vitesse (chronométrez-les). La vérification defined
n'est pas nécessaire dans l'expression régulière tandis que pour split
utilisez //
(Défini ou) qui court-circuite.
use warnings;
use strict;
# If 'prog smallfile bigfile' is the preferred use
die "Usage: $0 smallfile bigfile\n" if @ARGV != 2;
my ($smallfile, $bigfile) = @ARGV;
open my $fh, '<', $smallfile or die "Can't open $smallfile: $!";
my %Word = map { chomp; $_ => 1 } <$fh>;
open $fh, '<', $bigfile or die "Can't open $bigfile: $!";
while (<$fh>)
{
exists $Word{ (/\|([^|]+)/)[0] } && print;
# Or
#exists $Word{ (split /\|/)[1] // '' } && print;
}
close $fh;
Éviter la branche if
et utiliser les courts-circuits est plus rapide, mais très peu. Sur des milliards de lignes, ces ajustements s'additionnent, mais encore une fois pas trop. Il peut (ou non) être un peu plus rapide pour lire le petit fichier ligne par ligne, au lieu du contexte de liste comme ci-dessus, mais cela devrait pas être perceptible.
Mise à jour L'écriture dans STDOUT
enregistre deux opérations et je la chronomètre à plusieurs reprises pour qu'elle soit un peu plus rapide que l'écriture dans un fichier. Une telle utilisation est également compatible avec la plupart des outils UNIX, j'ai donc changé pour écrire dans STDOUT
. Ensuite, le test exists
n'est pas nécessaire et le supprimer épargne une opération. Cependant, j'obtiens toujours un meilleur temps d'exécution avec ça, alors qu'il transmet également mieux le but. Au total, je le laisse. Merci à ikegami pour les commentaires.
Remarque La version commentée est environ 50% plus rapide que l'autre, par mon repère ci-dessous. Celles-ci sont toutes deux données car elles sont différentes, l'une trouvant la première correspondance et l'autre le deuxième champ. Je le garde comme un choix plus générique, car la question est ambiguë à ce sujet.
Quelques comparaisons (benchmark) [Mis à jour pour écrire dans STDOUT
, voir "Mettre à jour" ci-dessus]
Il y a une analyse approfondie dans le réponse de HåkonHægland , chronométrant un cycle de la plupart des solutions. Voici une autre prise, comparant les deux solutions ci-dessus, la propre réponse de l'OP et la fgrep
publiée, qui devrait être rapide et utilisée dans la question et dans de nombreuses réponses.
Je crée des données de test de la manière suivante. Une poignée de lignes de la longueur à peu près comme indiqué sont faites avec des mots aléatoires, pour les deux fichiers, afin de correspondre dans le deuxième champ. Ensuite, je remplis cette "graine" pour les échantillons de données avec des lignes qui ne correspondent pas, donc pour imiter les rapports entre les tailles et les correspondances citées par OP: pour 14K lignes dans un petit fichier, il y a 1,3M lignes dans le gros fichier, ce qui donne 126K correspondances. Ensuite, ces exemples sont écrits à plusieurs reprises pour créer des fichiers de données complets sous forme d'OP, shuffle
- édités à chaque fois en utilisant List :: Util .
Toutes les exécutions comparées ci-dessous produisent des correspondances de 106_120
Pour les tailles de fichier ci-dessus (diff
- ed pour une vérification), donc la fréquence de correspondance est assez proche. Ils sont comparés en appelant des programmes complets à l'aide de my $res = timethese(60 ...)
. Le résultat de cmpthese($res)
sur v5.16 est
Taux regex c pour fgrep divisé Regex 1,05/s - -23% -35% -44% Cfor 1,36/s 30% - -16% -28% divisé 1,62/s 54% 19% - -14% fgrep 1,89/s 80% 39% 17% -
Le fait que le programme C optimisé fgrep
arrive en tête n'est pas surprenant. Le décalage de " regex" derrière " split" peut être dû à la surcharge de démarrage du moteur pour les petits matchs, beaucoup fois. Cela peut varier selon les versions de Perl, compte tenu des optimisations du moteur regex en constante évolution. J'inclus la réponse de @codeforester (" cfor") car elle a été déclarée la plus rapide, et son 20%
Est en retard sur le très similaire " split "est probablement dû à de petites inefficacités dispersées (voir un commentaire sous cette réponse).†
Ce n'est pas complètement différent, bien qu'il existe des variations sûres entre le matériel et les logiciels et les détails des données. J'ai exécuté ceci sur différentes Perls et machines, et la différence notable est que dans certains cas fgrep
était en effet un ordre de grandeur plus rapide.
L'expérience de l'OP de très lent fgrep
est surprenante. Compte tenu de leurs temps d'exécution cités, d'un ordre de grandeur plus lent que ci-dessus, je suppose qu'il y a un vieux système à "blâmer".
Même si cela est entièrement basé sur les E/S, il est avantageux de le placer sur plusieurs cœurs et je m'attendrais à une bonne accélération, jusqu'à un facteur de quelques-uns.
† Hélas, le commentaire a été supprimé (?). En bref: utilisation inutile d'un scalaire (coûts), d'une branche if
, de defined
, de printf
au lieu de print
(lent!). Ceux-ci comptent pour l'efficacité sur 2 milliards de lignes.
J'ai essayé de faire une comparaison entre certaines des méthodes présentées ici.
J'ai d'abord créé un script Perl pour générer les fichiers d'entrée _file1.txt
_ et _file2.txt
_. Afin de comparer certaines des solutions, je me suis assuré que les mots de _file1.txt
_ ne pouvaient apparaître que dans le deuxième champ de _file2.txt
_. Aussi pour pouvoir utiliser la solution join
présentée par @GeorgeVasiliou, j'ai trié _file1.txt
_ et _file2.txt
_. Actuellement, j'ai généré les fichiers d'entrée sur la base de seulement 75 mots aléatoires (extraits de https://www.randomlists.com/random-words ). Seulement 5 de ces 75 mots ont été utilisés dans _file1.txt
_ les 70 mots restants ont été utilisés pour remplir les champs dans _file2.txt
_. Il pourrait être nécessaire d'augmenter considérablement le nombre de mots pour obtenir des résultats réalistes (selon le PO, le _file1.txt
_ original contenait 14000 mots). Dans les tests ci-dessous, j'ai utilisé un _file2.txt
_ avec 1000000 (1 million) de lignes. Le script génère également le fichier _regexp1.txt
_ requis par la solution grep de @BOC.
gen_input_files.pl :
_#! /usr/bin/env Perl
use feature qw(say);
use strict;
use warnings;
use Data::Printer;
use Getopt::Long;
GetOptions ("num_lines=i" => \my $nlines )
or die("Error in command line arguments\n");
# Generated random words from site: https://www.randomlists.com/random-words
my $Word_filename = 'words.txt'; # 75 random words
my $num_match_words = 5;
my $num_file2_lines = $nlines || 1_000_000;
my $file2_words_per_line = 3;
my $file2_match_field_no = 2;
my $file1_filename = 'file1.txt';
my $file2_filename = 'file2.txt';
my $file1_regex_fn = 'regexp1.txt';
say "generating $num_file2_lines lines..";
my ( $words1, $words2 ) = get_words( $Word_filename, $num_match_words );
write_file1( $file1_filename, $words2 );
write_file2(
$file2_filename, $words1, $words2, $num_file2_lines,
$file2_words_per_line, $file2_match_field_no
);
write_BOC_regexp_file( $file1_regex_fn, $words2 );
sub write_BOC_regexp_file {
my ( $fn, $words ) = @_;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh '\\|' . (join "|", @$words) . '\\|';
close $fh;
}
sub write_file2 {
my ( $fn, $words1, $words2, $nlines, $words_per_line, $field_no ) = @_;
my $nwords1 = scalar @$words1;
my $nwords2 = scalar @$words2;
my @lines;
for (1..$nlines) {
my @words_line;
my $key;
for (1..$words_per_line) {
my $Word;
if ( $_ != $field_no ) {
my $index = int (Rand $nwords1);
$Word = @{ $words1 }[$index];
}
else {
my $index = int (Rand($nwords1 + $nwords2) );
if ( $index < $nwords2 ) {
$Word = @{ $words2 }[$index];
}
else {
$Word = @{ $words1 }[$index - $nwords2];
}
$key = $Word;
}
Push @words_line, $Word;
}
Push @lines, [$key, (join "|", @words_line)];
}
@lines = map { $_->[1] } sort { $a->[0] cmp $b->[0] } @lines;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh (join "\n", @lines);
close $fh;
}
sub write_file1 {
my ( $fn, $words ) = @_;
open( my $fh, '>', $fn ) or die "Could not open file '$fn': $!";
print $fh (join "\n", sort @$words);
close $fh;
}
sub get_words {
my ( $fn, $N ) = @_;
open( my $fh, '<', $fn ) or die "Could not open file '$fn': $!";
my @words = map {chomp $_; $_} <$fh>;
close $fh;
my @words1 = @words[$N..$#words];
my @words2 = @words[0..($N - 1)];
return ( \@words1, \@words2 );
}
_
Ensuite, j'ai créé un sous-dossier solutions
avec tous les cas de test:
_$ tree solutions/
solutions/
├── BOC1
│ ├── out.txt
│ └── run.sh
├── BOC2
│ ├── out.txt
│ └── run.sh
├── codeforester
│ ├── out.txt
│ ├── run.pl
│ └── run.sh
[...]
_
Ici, les fichiers _out.txt
_ sont la sortie des greps pour chaque solution. Les scripts _run.sh
_ exécutent la solution pour le cas de test donné.
_BOC1
_ : Première solution présentée par @BOC
_grep -E -f regexp1.txt file2.txt
_
_BOC2
_ : Deuxième solution proposée par @BOC:
_LC_ALL=C grep -E -f regexp1.txt file2.txt
_
codeforester
: Solution Perl acceptée par @codeforester (voir source )
_codeforester_orig
_ : Solution originale présentée par @codeforested:
_fgrep -f file1.txt file2.txt
_
dawg
: Python utilisant le dictionnaire et la ligne de séparation proposée par @dawg (voir source )
_gregory1
_ : solution utilisant Gnu Parallel proposée par @gregory
_parallel -k --pipepart -a file2.txt --block "$block_size" fgrep -F -f file1.txt
_
Voir la note ci-dessous pour savoir comment choisir _$block_size
_.
_hakon1
_ : solution Perl fournie par @ HåkonHægland (voir source ). Cette solution nécessite la compilation de l'extension c lors de la première exécution du code. Il ne nécessite pas de recompilation lorsque _file1.txt
_ ou _file2.txt
_ change. Remarque: Le temps utilisé pour compiler l'extension c lors de l'exécution initiale n'est pas inclus dans les temps d'exécution présentés ci-dessous.
ikegami
: Solution utilisant l'expression rationnelle assemblée et utilisant _grep -P
_ comme indiqué par @ikegami. Remarque: l'expression rationnelle assemblée a été écrite dans un fichier distinct _regexp_ikegami.txt
_, de sorte que l'exécution de la génération de l'expression rationnelle n'est pas incluse dans la comparaison ci-dessous. Voici le code utilisé:
_regexp=$(< "regexp_ikegami.txt")
grep -P "$regexp" file2.txt
_
_inian1
_ : Première solution par @Inian en utilisant match()
_awk 'FNR==NR{
hash[$1]; next
}
{
for (i in hash) if (match($0,i)) {print; break}
}' file1.txt FS='|' file2.txt
_
_inian2
_ : Deuxième solution par @Inian en utilisant index()
_awk 'FNR==NR{
hash[$1]; next
}
{
for (i in hash) if (index($0,i)) {print; break}
}' file1.txt FS='|' file2.txt
_
_inian3
_ : Troisième solution en vérifiant @Inian uniquement _$2
_ champ:
_awk 'FNR==NR{
hash[$1]; next
}
$2 in hash' file1.txt FS='|' file2.txt
_
_inian4
_ : 4ème soultion par @Inian (essentiellement identique à _codeforester_orig
_ avec _LC_ALL
_):
_LC_ALL=C fgrep -f file1.txt file2.txt
_
_inian5
_ : 5ème solution par @Inian (identique à _inian1
_ mais avec _LC_ALL
_):
_LC_ALL=C awk 'FNR==NR{
hash[$1]; next
}
{
for (i in hash) if (match($0,i)) {print; break}
}' file1.txt FS='|' file2.txt
_
_inian6
_ : Identique à _inian3
_ mais avec _LC_ALL=C
_. Merci à @GeorgeVasiliou pour sa suggestion.
jjoao
: Code C généré par flex compilé comme proposé par @JJoao (voir source ). Remarque: La recompilation de l'exectuable doit être effectuée à chaque fois que _file1.txt
_ change. Le temps utilisé pour compiler l'exécutable n'est pas inclus dans les temps d'exécution présentés ci-dessous.
oliv
: Python fourni par @oliv (voir source )
Vasiliou
: Utilisation de join
comme suggéré par @GeorgeVasiliou:
_join --nocheck-order -11 -22 -t'|' -o 2.1 2.2 2.3 file1.txt file2.txt
_
_Vasiliou2
_ : Identique à Vasiliou
mais avec _LC_ALL=C
_.
zdim
: Utilisation du script Perl fourni par @zdim (voir source ). Remarque: Cela utilise la version de recherche d'expression régulière (au lieu de la solution de ligne fractionnée).
_zdim2
_ : Identique à zdim
sauf qu'il utilise la fonction split
au lieu de la recherche d'expression régulière pour le dans _file2.txt
_.
J'ai expérimenté un peu avec le parallèle Gnu (voir la solution _gregory1
_ ci-dessus) pour déterminer la taille de bloc optimale pour mon CPU. J'ai 4 cœurs et, actuellement, il semble que le choix optimal consiste à diviser le fichier (_file2.txt
_) en 4 morceaux de taille égale et à exécuter un seul travail sur chacun des 4 processeurs. D'autres tests pourraient être nécessaires ici. Donc, pour le premier cas de test où _file2.txt
_ est 20M, je règle _$block_size
_ à 5M (voir la solution _gregory1
_ ci-dessus), tandis que pour le cas plus réaliste présenté ci-dessous où _file2.txt
_ est 268M, un _$block_size
_ de 67M a été utilisé.
Les solutions _BOC1
_, _BOC2
_, _codeforester_orig
_, _inian1
_, _inian4
_, _inian5
_ et _gregory1
_ ont toutes utilisé une correspondance lâche. Cela signifie que les mots de _file1.txt
_ ne devaient pas correspondre exactement dans le champ # 2 de _file2.txt
_. Un match n'importe où sur la ligne a été accepté. Étant donné que ce comportement a rendu plus difficile leur comparaison avec les autres méthodes, certaines méthodes modifiées ont également été introduites. Les deux premières méthodes appelées _BOC1B
_ et _BOC2B
_ ont utilisé un fichier _regexp1.txt
_ modifié. Les lignes dans le _regexp1.txt
_ d'origine sur le formulaire _\|foo1|foo2|...|fooN\|
_ qui correspondraient aux mots à n'importe quelle limite de champ. Le fichier modifié, _regexp1b.txt
_, a ancré la correspondance au champ # 2 exclusivement en utilisant le formulaire _^[^|]*\|foo1|foo2|...|fooN\|
_ à la place.
Ensuite, les autres méthodes modifiées _codeforester_origB
_, _inian1B
_, _inian4B
_, _inian5B
_ et _gregory1B
_ ont utilisé un _file1.txt
_ modifié. Au lieu d'un littéral mot par ligne, le fichier modifié _file1b.txt
_ en utilisait un regex par ligne sur le formulaire:
_ ^[^|]*\|Word1\|
^[^|]*\|Word2\|
^[^|]*\|Word3\|
[...]
_
et en outre, _fgrep -f
_ a été remplacé par _grep -E -f
_ pour ces méthodes.
Voici le script utilisé pour exécuter tous les tests. Il utilise la commande Bash time
pour enregistrer le temps passé pour chaque script. Notez que la commande time
renvoie trois fois différents appels real
, user
et sys
. J'ai d'abord utilisé user
+ sys
, mais je me suis rendu compte que c'était incorrect lors de l'utilisation de la commande parallèle Gnu, donc l'heure indiquée ci-dessous est maintenant la partie real
retournée par time
. Voir cette question pour plus d'informations sur les différentes heures renvoyées par time
.
Le premier test est exécuté avec _file1.txt
_ contenant 5 lignes et _file2.txt
_ contenant _1000000
_ lignes. Voici les 52 premières lignes du script _run_all.pl
_, le reste du script est disponible ici .
run_all.pl
_#! /usr/bin/env Perl
use feature qw(say);
use strict;
use warnings;
use Cwd;
use Getopt::Long;
use Data::Printer;
use FGB::Common;
use List::Util qw(max shuffle);
use Number::Bytes::Human qw(format_bytes);
use Sys::Info;
GetOptions (
"verbose" => \my $verbose,
"check" => \my $check,
"single-case=s" => \my $case,
"expected=i" => \my $expected_no_lines,
) or die("Error in command line arguments\n");
my $test_dir = 'solutions';
my $output_file = 'out.txt';
my $wc_expected = $expected_no_lines; # expected number of output lines
my $tests = get_test_names( $test_dir, $case );
my $file2_size = get_file2_size();
my $num_cpus = Sys::Info->new()->device( CPU => () )->count;
chdir $test_dir;
my $cmd = 'run.sh';
my @times;
for my $case (@$tests) {
my $savedir = getcwd();
chdir $case;
say "Running '$case'..";
my $arg = get_cmd_args( $case, $file2_size, $num_cpus );
my $output = `bash -c "{ time -p $cmd $arg; } 2>&1"`;
my ($user, $sys, $real ) = get_run_times( $output );
print_timings( $user, $sys, $real ) if $verbose;
check_output_is_ok( $output_file, $wc_expected, $verbose, $check );
print "\n" if $verbose;
Push @times, $real;
#Push @times, $user + $sys; # this is wrong when using Gnu parallel
chdir $savedir;
}
say "Done.\n";
print_summary( $tests, \@times );
_
Voici le résultat de l'exécution des tests:
_$ run_all.pl --verbose
Running 'inian3'..
..finished in 0.45 seconds ( user: 0.44, sys: 0.00 )
..no of output lines: 66711
Running 'inian2'..
..finished in 0.73 seconds ( user: 0.73, sys: 0.00 )
..no of output lines: 66711
Running 'Vasiliou'..
..finished in 0.09 seconds ( user: 0.08, sys: 0.00 )
..no of output lines: 66711
Running 'codeforester_orig'..
..finished in 0.05 seconds ( user: 0.05, sys: 0.00 )
..no of output lines: 66711
Running 'codeforester'..
..finished in 0.45 seconds ( user: 0.44, sys: 0.01 )
..no of output lines: 66711
[...]
_
[Les résultats obtenus par @Vasiliou sont indiqués dans la colonne du milieu.]
_ |Vasiliou
My Benchmark |Results | Details
-------------------------------|---------|----------------------
inian4 : 0.04s |0.22s | LC_ALL fgrep -f [loose]
codeforester_orig : 0.05s | | fgrep -f [loose]
Vasiliou2 : 0.06s |0.16s | [LC_ALL join [requires sorted files]]
BOC1 : 0.06s | | grep -E [loose]
BOC2 : 0.07s |15s | LC_ALL grep -E [loose]
BOC2B : 0.07s | | LC_ALL grep -E [strict]
inian4B : 0.08s | | LC_ALL grep -E -f [strict]
Vasiliou : 0.08s |0.23s | [join [requires sorted files]]
gregory1B : 0.08s | | [parallel + grep -E -f [strict]]
ikegami : 0.1s | | grep -P
gregory1 : 0.11s |0.5s | [parallel + fgrep -f [loose]]
hakon1 : 0.14s | | [Perl + c]
BOC1B : 0.14s | | grep -E [strict]
jjoao : 0.21s | | [compiled flex generated c code]
inian6 : 0.26s |0.7s | [LC_ALL awk + split+dict]
codeforester_origB : 0.28s | | grep -E -f [strict]
dawg : 0.35s | | [python + split+dict]
inian3 : 0.44s |1.1s | [awk + split+dict]
zdim2 : 0.4s | | [Perl + split+dict]
codeforester : 0.45s | | [Perl + split+dict]
oliv : 0.5s | | [python + compiled regex + re.search()]
zdim : 0.61s | | [Perl + regexp+dict]
inian2 : 0.73s |1.7s | [awk + index($0,i)]
inian5 : 18.12s | | [LC_ALL awk + match($0,i) [loose]]
inian1 : 19.46s | | [awk + match($0,i) [loose]]
inian5B : 42.27s | | [LC_ALL awk + match($0,i) [strict]]
inian1B : 85.67s | | [awk + match($0,i) [strict]]
Vasiliou Results : 2 X CPU Intel 2 Duo T6570 @ 2.10GHz - 2Gb RAM-Debian Testing 64bit- kernel 4.9.0.1 - no cpu freq scaling.
_
J'ai ensuite créé un cas plus réaliste avec _file1.txt
_ ayant 100 mots et _file2.txt
_ ayant 10 millions de lignes (taille de fichier 268Mb). J'ai extrait 1000 mots aléatoires du dictionnaire à _/usr/share/dict/american-english
_ en utilisant _shuf -n1000 /usr/share/dict/american-english > words.txt
_ puis extrait 100 de ces mots dans _file1.txt
_ puis j'ai construit _file2.txt
_ de la même manière que celle décrite ci-dessus pour le premier test Cas. Notez que le fichier de dictionnaire était codé en UTF-8 et j'ai supprimé tous les caractères non ASCII du _words.txt
_.
Ensuite, je lance le test sans les trois méthodes les plus lentes du cas précédent. C'est à dire. _inian1
_, _inian2
_ et _inian5
_ ont été omis. Voici les nouveaux résultats:
_gregory1 : 0.86s | [parallel + fgrep -f [loose]]
Vasiliou2 : 0.94s | [LC_ALL join [requires sorted files]]
inian4B : 1.12s | LC_ALL grep -E -f [strict]
BOC2B : 1.13s | LC_ALL grep -E [strict]
BOC2 : 1.15s | LC_ALL grep -E [loose]
BOC1 : 1.18s | grep -E [loose]
ikegami : 1.33s | grep -P
Vasiliou : 1.37s | [join [requires sorted files]]
hakon1 : 1.44s | [Perl + c]
inian4 : 2.18s | LC_ALL fgrep -f [loose]
codeforester_orig : 2.2s | fgrep -f [loose]
inian6 : 2.82s | [LC_ALL awk + split+dict]
jjoao : 3.09s | [compiled flex generated c code]
dawg : 3.54s | [python + split+dict]
zdim2 : 4.21s | [Perl + split+dict]
codeforester : 4.67s | [Perl + split+dict]
inian3 : 5.52s | [awk + split+dict]
zdim : 6.55s | [Perl + regexp+dict]
gregory1B : 45.36s | [parallel + grep -E -f [strict]]
oliv : 60.35s | [python + compiled regex + re.search()]
BOC1B : 74.71s | grep -E [strict]
codeforester_origB : 75.52s | grep -E -f [strict]
_
Les solutions basées sur grep
cherchaient une correspondance sur toute la ligne, donc dans ce cas elles contenaient des fausses correspondances: les méthodes _codeforester_orig
_, _BOC1
_, _BOC2
_, _gregory1
_, _inian4
_ et oliv
ont extrait 1 087 609 lignes sur 10 000 000 lignes, tandis que les autres méthodes ont extrait les 997 993 lignes correctes de _file2.txt
_.
J'ai testé cela sur mon ordinateur portable Ubuntu 16.10 (processeur Intel Core i7-7500U à 2,70 GHz)
L'étude complète de référence est disponible ici .
Avez-vous essayé Awk
qui pourrait accélérer un peu les choses:
awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (match($0,i)) {print; break}}' file1.txt FS='|' file2.txt
(ou) en utilisant la fonction index()
dans Awk
comme suggéré par les commentaires de Benjamin W. , ci-dessous
awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (index($0,i)) {print; break}}' file1.txt FS='|' file2.txt
(ou) une correspondance regex plus directe comme suggéré par Ed Morton dans les commentaires,
awk 'FNR==NR{hash[$1]; next}{for (i in hash) if ($0~i) {print; break}}' file1.txt FS='|' file2.txt
est tout ce dont vous avez besoin. Je suppose que ce sera plus rapide mais pas exactement sur les fichiers avec plus d'un million d'entrées. Ici, le problème réside dans la possibilité de faire correspondre n'importe où le long de la ligne. Si le même avait été dans une colonne particulière (par exemple, dites $2
Seul), une approche plus rapide pourrait être
awk 'FNR==NR{hash[$1]; next}$2 in hash' file1.txt FS='|' file2.txt
Vous pouvez également accélérer les choses en jouant avec l'ensemble locale
de votre système. Paraphrasant ce merveilleux réponse de Stéphane Chazelas sur le sujet, vous pouvez accélérer les choses assez rapidement en définissant le passage des paramètres régionaux LC_ALL=C
À la commande localement en cours d'exécution.
Sur tout système basé sur GNU
, les valeurs par défaut pour locale
$ locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
Avec une variable LC_ALL
, Vous pouvez définir toutes les variables de type LC_
À la fois dans un environnement local spécifié
$ LC_ALL=C locale
LANG=en_US.UTF-8
LC_CTYPE="C"
LC_NUMERIC="C"
LC_TIME="C"
LC_COLLATE="C"
LC_MONETARY="C"
LC_MESSAGES="C"
LC_PAPER="C"
LC_NAME="C"
LC_ADDRESS="C"
LC_TELEPHONE="C"
LC_MEASUREMENT="C"
LC_IDENTIFICATION="C"
LC_ALL=C
Alors qu'est-ce que cela a un impact?
Autrement dit, lorsque vous utilisez le locale C
, Il utilisera par défaut le langage de base Unix/Linux du serveur ASCII
. Fondamentalement, lorsque vous grep
quelque chose, par défaut, votre locale va être internationalisée et définie sur UTF-8
, Qui peut représenter chaque caractère du jeu de caractères Unicode pour aider à afficher l'un des systèmes d'écriture du monde, actuellement plus de 110,000
caractères uniques, alors qu'avec ASCII
chaque caractère est codé dans une séquence d'un seul octet et son jeu de caractères ne comprend pas plus de 128
caractères uniques.
Donc, cela se traduit par cela, lorsque vous utilisez grep
sur un fichier codé dans le jeu de caractères UTF-8
, Il doit faire correspondre chaque caractère avec l'un des cent mille caractères uniques, mais juste 128
dans ASCII
, utilisez donc votre fgrep
comme
LC_ALL=C fgrep -F -f file1.txt file2.txt
En outre, la même chose peut être adaptée à Awk
, car elle utilise une correspondance regex
avec l'appel match($0,i)
, la définition des paramètres régionaux C
pourrait accélérer la correspondance de chaîne.
LC_ALL=C awk 'FNR==NR{hash[$1]; next}{for (i in hash) if (match($0,i)) {print; break}}' file1.txt FS='|' file2.txt
Hypothèses: 1. Vous souhaitez exécuter cette recherche uniquement sur votre poste de travail local. 2. Vous avez plusieurs cœurs/cpus pour profiter d'une recherche parallèle.
parallel --pipepart -a file2.txt --block 10M fgrep -F -f file1.txt
Quelques ajustements supplémentaires en fonction du contexte: A. Désactiver NLS avec LANG = C (cela est déjà mentionné dans une autre réponse) B. Définir un nombre maximum de correspondances avec l'indicateur -m.
Remarque: je suppose que le fichier 2 fait environ 4 Go et que la taille de bloc de 10 Mo est correcte, mais vous devrez peut-être optimiser la taille du bloc pour obtenir l'exécution la plus rapide.
Voici la solution Perl qui utilise Inline::C
pour accélérer la recherche des champs correspondants dans le gros fichier:
use strict;
use warnings;
use Inline C => './search.c';
my $smallfile = 'file1.txt';
my $bigfile = 'file2.txt';
open my $fh, '<', $smallfile or die "Can't open $smallfile: $!";
my %Word = map { chomp; $_ => 1 } <$fh>;
search( $bigfile, \%Word );
La sous-routine search()
est implémentée en C pur en utilisant perlapi
pour rechercher des clés dans le dictionnaire de petits fichiers %words
:
search.c :
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define BLOCK_SIZE 8192 /* how much to read from file each time */
static char read_buf[BLOCK_SIZE + 1];
/* reads a block from file, returns -1 on error, 0 on EOF,
else returns chars read, pointer to buf, and pointer to end of buf */
size_t read_block( int fd, char **ret_buf, char **end_buf ) {
int ret;
char *buf = read_buf;
size_t len = BLOCK_SIZE;
while (len != 0 && (ret = read(fd, buf, len)) != 0) {
if (ret == -1) {
if (errno == EINTR)
continue;
perror( "read" );
return ret;
}
len -= ret;
buf += ret;
}
*end_buf = buf;
*ret_buf = read_buf;
return (size_t) (*end_buf - *ret_buf);
}
/* updates the line buffer with the char pointed to by cur,
also updates cur
*/
int update_line_buffer( char **cur, char **line, size_t *llen, size_t max_line_len ) {
if ( *llen > max_line_len ) {
fprintf( stderr, "Too long line. Maximimum allowed line length is %ld\n",
max_line_len );
return 0;
}
**line = **cur;
(*line)++;
(*llen)++;
(*cur)++;
return 1;
}
/* search for first pipe on a line (or next line if this is empty),
assume line ptr points to beginning of line buffer.
return 1 on success
Return 0 if pipe could not be found for some reason, or if
line buffer length was exceeded */
int search_field_start(
int fd, char **cur, char **end_buf, char **line, size_t *llen, size_t max_line_len
) {
char *line_start = *line;
while (1) {
if ( *cur >= *end_buf ) {
size_t res = read_block( fd, cur, end_buf );
if (res <= 0) return 0;
}
if ( **cur == '|' ) break;
/* Currently we just ignore malformed lines ( lines that do not have a pipe,
and empty lines in the input */
if ( **cur == '\n' ) {
*line = line_start;
*llen = 0;
(*cur)++;
}
else {
if (! update_line_buffer( cur, line, llen, max_line_len ) ) return 0;
}
}
return 1;
}
/* assume cur points at starting pipe of field
return -1 on read error,
return 0 if field len was too large for buffer or line buffer length exceed,
else return 1
and field, and length of field
*/
int copy_field(
int fd, char **cur, char **end_buf, char *field,
size_t *flen, char **line, size_t *llen, size_t max_field_len, size_t max_line_len
) {
*flen = 0;
while( 1 ) {
if (! update_line_buffer( cur, line, llen, max_line_len ) ) return 0;
if ( *cur >= *end_buf ) {
size_t res = read_block( fd, cur, end_buf );
if (res <= 0) return -1;
}
if ( **cur == '|' ) break;
if ( *flen > max_field_len ) {
printf( "Field width too large. Maximum allowed field width: %ld\n",
max_field_len );
return 0;
}
*field++ = **cur;
(*flen)++;
}
/* It is really not necessary to null-terminate the field
since we return length of field and also field could
contain internal null characters as well
*/
//*field = '\0';
return 1;
}
/* search to beginning of next line,
return 0 on error,
else return 1 */
int search_eol(
int fd, char **cur, char **end_buf, char **line, size_t *llen, size_t max_line_len)
{
while (1) {
if ( *cur >= *end_buf ) {
size_t res = read_block( fd, cur, end_buf );
if (res <= 0) return 0;
}
if ( !update_line_buffer( cur, line, llen, max_line_len ) ) return 0;
if ( *(*cur-1) == '\n' ) {
break;
}
}
//**line = '\0'; // not necessary
return 1;
}
#define MAX_FIELD_LEN 80 /* max number of characters allowed in a field */
#define MAX_LINE_LEN 80 /* max number of characters allowed on a line */
/*
Get next field ( i.e. field #2 on a line). Fields are
separated by pipes '|' in the input file.
Also get the line of the field.
Return 0 on error,
on success: Move internal pointer to beginning of next line
return 1 and the field.
*/
size_t get_field_and_line_fast(
int fd, char *field, size_t *flen, char *line, size_t *llen
) {
static char *cur = NULL;
static char *end_buf = NULL;
size_t res;
if (cur == NULL) {
res = read_block( fd, &cur, &end_buf );
if ( res <= 0 ) return 0;
}
*llen = 0;
if ( !search_field_start( fd, &cur, &end_buf, &line, llen, MAX_LINE_LEN )) return 0;
if ( (res = copy_field(
fd, &cur, &end_buf, field, flen, &line, llen, MAX_FIELD_LEN, MAX_LINE_LEN
) ) <= 0)
return 0;
if ( !search_eol( fd, &cur, &end_buf, &line, llen, MAX_LINE_LEN ) ) return 0;
return 1;
}
void search( char *filename, SV *href)
{
if( !SvROK( href ) || ( SvTYPE( SvRV( href ) ) != SVt_PVHV ) ) {
croak( "Not a hash reference" );
}
int fd = open (filename, O_RDONLY);
if (fd == -1) {
croak( "Could not open file '%s'", filename );
}
char field[MAX_FIELD_LEN+1];
char line[MAX_LINE_LEN+1];
size_t flen, llen;
HV *hash = (HV *)SvRV( href );
while ( get_field_and_line_fast( fd, field, &flen, line, &llen ) ) {
if( hv_exists( hash, field, flen ) )
fwrite( line, sizeof(char), llen, stdout);
}
if (close(fd) == -1)
croak( "Close failed" );
}
Les tests indiquent qu'elle est environ 3 fois plus rapide que la solution Perl pure la plus rapide (voir la méthode zdim2
Dans mon autre réponse ) présentée ici.
Ce script Perl (a
) génère un modèle d'expression régulière:
#!/usr/bin/Perl
use strict;
use warnings;
use Regexp::Assemble qw( );
chomp( my @ids = <> );
my $ra = Regexp::Assemble->new();
$ra->add(quotemeta($_)) for @ids;
print("^[^|]*\\|(?:" . (re::regexp_pattern($ra->re()))[0] . ")\\|");
Voici comment il peut être utilisé:
$ LC_ALL=C grep -P "$( a file1.txt )" file2.txt
date1|foo1|number1
date2|foo2|number2
date1|bar1|number1
date2|bar2|number2
Notez que le script utilise Regexp :: Assemble, vous devrez donc peut-être l'installer.
Sudo su
cpan Regexp::Assemble
Remarques:
Contrairement aux solutions baptisées BOC1, BOC2, codeforester_orig, gregory1, inian2, inian4 et oliv, ma solution gère correctement
file1.txt
foo1
file2.txt
date1|foo12|number5
Le mien devrait être meilleur que le similaire solution par @BOC car le modèle est optimisé pour réduire le retour en arrière. (Le mien fonctionne également s'il y a plus de trois champs dans file2.txt
, alors que la solution liée peut échouer.)
Je ne sais pas comment cela se compare aux solutions split + dictionnaire.
Voici une solution Python utilisant des ensembles - à peu près équivalente à une clé Perl uniquement un tableau de hachage ou awk dans le concept.
#!/usr/bin/python
import sys
with open(sys.argv[1]) as f:
tgt={e.rstrip() for e in f}
with open(sys.argv[2]) as f:
for line in f:
cells=line.split("|")
if cells[1] in tgt:
print line.rstrip()
Lorsque j'exécute cela sur des fichiers de taille similaire, cela s'exécute en environ 8 secondes.
Même vitesse que:
$ awk 'FNR==NR{arr[$1]; next} $2 in arr{print $0}' FS="|" /tmp/f1 /tmp/f2
La solution Python et awk ne sont ici que des correspondances de chaînes complètes; pas une correspondance de style d'expression régulière partielle.
Étant donné que la solution awk est rapide et compatible POSIX, c'est la meilleure réponse.
Pouvez-vous essayer de join
? Les fichiers doivent cependant être triés ...
$ cat d.txt
bar1
bar2
foo1
foo2
$ cat e.txt
date1|bar1|number1
date2|bar2|number2
date3|bar3|number3
date1|foo1|number1
date2|foo2|number2
date3|foo3|number3
$ join --nocheck-order -11 -22 -t'|' -o 2.1 2.2 2.3 d.txt e.txt
date1|bar1|number1
date2|bar2|number2
date1|foo1|number1
date2|foo2|number2
Petite mise à jour:
En utilisant LC_ALL = C devant la jointure, les choses accélèrent vraiment comme on peut le voir dans le benchmark de Håkon Hægland
PS1: J'ai des doutes si la jointure peut être plus rapide que grep -f ...
Bien que ce fil soit terminé, mais toutes les méthodes similaires à grep entre deux fichiers sont rassemblées dans cet article, pourquoi ne pas ajouter cette alternative awk, similaire (ou même améliorée) à la solution awk d'Inian gagnante de primes:
awk 'NR==FNR{a[$0]=1;next}a[$2]' patterns.txt FS="|" datafile.txt >matches.txt # For matches restricted on Field2 of datafile
Cela équivaut à Inian awk $2 in hash
solution mais cela pourrait être encore plus rapide du fait que nous ne demandons pas à awk de vérifier si tout le tableau de hachage contient 2 $ de fichier2 - nous vérifions simplement si un [$ 2] a une valeur ou non.
Lors de la lecture du premier fichier de motifs, à part la création du tableau de hachage, nous attribuons également une valeur.
Si $2
du fichier de données avait été trouvé auparavant dans le fichier de signatures, puis a[$2]
aurait une valeur et sera donc affiché car n'est pas nul.
si a[$2]
du fichier de données ne renvoie aucune valeur (null) ceci est traduit en faux => pas d'impression.
Extension pour correspondre à l'un des trois champs du fichier de données:
awk 'NR==FNR{a[$0]=1;next}(a[$1] || a[$2] || a[$3])' patterns.txt FS="|" datafile.txt >matches.txt. #Printed if any of the three fields of datafile match pattern.
Dans les deux cas, appliquer LC_ALL = C devant awk, semble accélérer les choses.
PS1: Offcourse cette solution a aussi les pièges de toutes les solutions awk. N'est pas une correspondance de motifs. Est une correspondance directe/fixe entre les deux fichiers, comme la plupart des solutions ici.
PS2: Dans mon mauvais benchmark de machine en utilisant les petits fichiers de benchmark de Håkon Hægland , j'obtiens environ 20% de meilleures performances par rapport au awk 'FNR==NR{hash[$1]; next}$2 in hash' file1.txt FS='|' file2.txt
J'utiliserais SQLite3 :) Peut-être une base de données en mémoire ou autre chose. Importez les fichiers et utilisez la requête SQL.
Utilisation de flex:
$ awk 'NR==1{ printf "%%%%\n\n.*\\|(%s",$0 }
{ printf "|%s",$0 }
END { print ")\\|.*\\n ECHO;\n.*\\n ;\n%%\n" }' file1.txt > a.fl
$ flex -Ca -F a.fl ; cc -O Lex.yy.c -lfl
$ a.out < file2.txt > out
La compilation (cc ...) est un processus lent; cette approche ne paiera que pour les cas de file1.txt stable
(Dans ma machine) Le temps nécessaire pour exécuter un test de recherche "100 dans 10_000_000" dans cette approche est 3 fois plus rapide que LC_ALL=C fgrep...
Vous pouvez également utiliser Perl pour cela:
Veuillez noter que cela encombrera la mémoire et que votre machine/serveur en a mieux.
Exemple de données:
%_STATION@gaurav * /root/ga/pl> head file1.txt file2.txt
==> file1.txt <==
foo1
foo2
...
bar1
bar2
...
==> file2.txt <==
date1|foo1|number1
date2|foo2|number2
date3|foo3|number3
...
date1|bar1|number1
date2|bar2|number2
date3|bar3|number3
%_STATION@gaurav * /root/ga/study/pl>
Sortie de script: Le script produira final sortie dans un fichier nommé output_comp
.
%_STATION@gaurav * /root/ga/pl> ./comp.pl file1.txt file2.txt ; cat output_comp
date1|bar1|number1
date2|bar2|number2
date2|foo2|number2
date1|foo1|number1
%_STATION@gaurav * /root/ga/pl>
Script:
%_STATION@gaurav * /root/ga/pl> cat comp.pl
#!/usr/bin/Perl
use strict ;
use warnings ;
use Data::Dumper ;
my ($file1,$file2) = @ARGV ;
my $output = "output_comp" ;
my %hash ; # This will store main comparison data.
my %tmp ; # This will store already selected results, to be skipped.
(scalar @ARGV != 2 ? (print "Need 2 files!\n") : ()) ? exit 1 : () ;
# Read all files at once and use their name as the key.
for (@ARGV) {
open FH, "<$_" or die "Cannot open $_\n" ;
while (my $line = <FH>) {chomp $line ;$hash{$_}{$line} = "$line"}
close FH ;
}
# Now we churn through the data and compare to generate
# the sorted output in the output file.
open FH, ">>$output" or die "Cannot open outfile!\n" ;
foreach my $k1 (keys %{$hash{$file1}}){
foreach my $k2 (keys %{$hash{$file2}}){
if ($k1 =~ m/^.+?$k2.+?$/) {
if (!defined $tmp{"$hash{$file2}{$k2}"}) {
print FH "$hash{$file2}{$k2}\n" ;
$tmp{"$hash{$file2}{$k2}"} = 1 ;
}
}
}
}
close FH ;
%_STATION@gaurav * /root/ga/pl>
Merci.
À mon humble avis, grep est un bon outil hautement optimisé pour un énorme fichier2.txt mais peut-être pas pour autant de modèles à rechercher. Je suggère de combiner toutes les chaînes de file1.txt en une seule expression rationnelle énorme comme\| bar1 | bar2 | foo1 | foo2\|
echo '\|'$(paste -s -d '|' file1.txt)'\|' > regexp1.txt
grep -E -f regexp1.txt file2.txt > file.matched
Et bien sûr, LANG = C peut aider. Veuillez donner votre avis ou envoyer vos fichiers afin que je puisse me tester.
Une façon possible est d'utiliser python
:
$ cat test.py
import sys,re
with open(sys.argv[1], "r") as f1:
patterns = f1.read().splitlines() # read pattern from file1 without the trailing newline
m = re.compile("|".join(patterns)) # create the regex
with open(sys.argv[2], "r") as f2:
for line in f2:
if m.search(line) :
print line, # print line from file2 if this one matches the regex
et utilisez-le comme ceci:
python test.py file1.txt file2.txt
définir la langue, etc. aide un peu, peut-être.
sinon, je ne peux pas penser à une solution magique pour échapper à votre problème de base: les données ne sont pas structurées, vous aurez donc une recherche qui se résume au nombre de lignes dans le fichier1 multiplié par le nombre de lignes dans le fichier2.
mettre le milliard de lignes dans une base de données et l'indexer de manière intelligente est la seule vitesse à laquelle je peux penser. cet indice devrait être très intelligent, bien que ......
La solution simple est: avoir suffisamment de mémoire pour tout contenir. sinon vous ne pouvez rien faire de plus à ce sujet ....