web-dev-qa-db-fra.com

Compter les lignes dans les gros fichiers

Je travaille couramment avec des fichiers texte d’une taille d’environ 20 Go et je me retrouve très souvent à compter le nombre de lignes d’un fichier donné.

La façon dont je le fais maintenant, c’est juste cat fname | wc -l, et cela prend très longtemps. Y at-il une solution qui serait beaucoup plus rapide?

Je travaille dans un cluster hautes performances sur lequel Hadoop est installé. Je me demandais si une approche de réduction de carte pourrait aider.

J'aimerais que la solution soit aussi simple qu'une ligne, comme la solution wc -l, mais je ne sais pas si c'est faisable.

Des idées?

62
Dnaiel

Essayez: sed -n '$=' filename

De plus, chat n'est pas nécessaire: wc -l filename suffit dans votre manière actuelle.

89
P.P.

Votre facteur de vitesse limite correspond à la vitesse d'E/S de votre périphérique de stockage. Il n'est donc pas utile de passer d'un programme de comptage de nouvelles lignes à un modèle, car la différence de vitesse d'exécution entre ces programmes est susceptible d'être supprimée par le biais d'un disque/stockage plus lent. tout ce que vous avez.

Mais si vous avez le même fichier copié sur des disques/périphériques, ou si le fichier est distribué entre ces disques, vous pouvez certainement effectuer l'opération en parallèle. Je ne connais pas précisément ce Hadoop, mais en supposant que vous puissiez lire un fichier de 10 Go à partir de 4 emplacements différents, vous pouvez exécuter 4 processus de comptage de lignes différents, chacun dans une partie du fichier, et résumer leurs résultats:

$ dd bs=4k count=655360 if=/path/to/copy/on/disk/1/file | wc -l &
$ dd bs=4k skip=655360 count=655360 if=/path/to/copy/on/disk/2/file | wc -l &
$ dd bs=4k skip=1310720 count=655360 if=/path/to/copy/on/disk/3/file | wc -l &
$ dd bs=4k skip=1966080 if=/path/to/copy/on/disk/4/file | wc -l &

Notez le & sur chaque ligne de commande, ainsi tout sera exécuté en parallèle; dd fonctionne comme ici cat, mais nous permet de spécifier combien d'octets à lire (count * bs octets) et combien à ignorer au début de l'entrée (skip * bs octets). Cela fonctionne par blocs, d'où la nécessité de spécifier bs comme taille de bloc. Dans cet exemple, j'ai partitionné le fichier de 10 Go en 4 morceaux égaux de 4 Ko * 655360 = 2684354560 octets = 2,5 Go, un pour chaque travail. Vous souhaiterez peut-être configurer un script pour le faire pour vous en fonction de la taille du fichier. fichier et le nombre de travaux parallèles que vous allez exécuter. Vous devez également résumer le résultat des exécutions, ce que je n’ai pas fait pour mon manque de capacité de script Shell.

Si votre système de fichiers est suffisamment intelligent pour fractionner de gros fichiers entre de nombreux périphériques, comme un système RAID, un système de fichiers distribué ou quelque chose du genre, et paralléliser automatiquement les demandes d'E/S pouvant être paralellisées, vous pouvez effectuer ce fractionnement en exécutant de nombreux travaux en parallèle, le même chemin de fichier, et vous pouvez toujours avoir un gain de vitesse.

EDIT: .__ Une autre idée qui m'est venue à l'esprit est que si les lignes à l'intérieur du fichier ont la même taille, vous pouvez obtenir le nombre exact de lignes en divisant la taille du fichier par la taille de la ligne, les deux en octets. Vous pouvez le faire presque instantanément dans un seul travail. Si vous avez la taille moyenne et ne vous souciez pas du nombre de lignes, mais souhaitez une estimation, vous pouvez effectuer la même opération et obtenir un résultat satisfaisant bien plus rapidement que l'opération exacte.

11
lvella

Sur un serveur multicœur, utilisez GNU parallel pour compter les lignes de fichier en parallèle. Une fois que chaque compte de ligne de fichiers est imprimé, bc additionne tous les comptes de ligne.

find . -name '*.txt' | parallel 'wc -l {}' 2>/dev/null | paste -sd+ - | bc

Pour économiser de l'espace, vous pouvez même garder tous les fichiers compressés. La ligne suivante décompresse chaque fichier et compte ses lignes en parallèle, puis additionne tous les comptes.

find . -name '*.xz' | parallel 'xzcat {} | wc -l' 2>/dev/null | paste -sd+ - | bc
8
Nicholas Sushkin

Si vos données résident sur HDFS, l’approche la plus rapide consiste peut-être à utiliser le streaming hadoop. COUNT UDF d’Apache Pig fonctionne sur un sac et utilise donc un seul réducteur pour calculer le nombre de lignes. Au lieu de cela, vous pouvez définir manuellement le nombre de réducteurs dans un script de streaming hadoop simple comme suit:

$HADOOP_HOME/bin/hadoop jar $HADOOP_HOME/hadoop-streaming.jar -Dmapred.reduce.tasks=100 -input <input_path> -output <output_path> -mapper /bin/cat -reducer "wc -l"

Notez que je règle manuellement le nombre de réducteurs sur 100, mais vous pouvez régler ce paramètre. Une fois le travail de réduction de carte terminé, le résultat de chaque réducteur est stocké dans un fichier séparé. Le nombre final de lignes est la somme des nombres renvoyés par tous les réducteurs. vous pouvez obtenir le nombre final de lignes comme suit:

$HADOOP_HOME/bin/hadoop fs -cat <output_path>/* | paste -sd+ | bc
6
Pirooz

Selon mon test, je peux vérifier que Spark-Shell (basé sur Scala) est bien plus rapide que les autres outils (GREP, SED, AWK, Perl, WC). Voici le résultat du test que j'ai exécuté sur un fichier de 23782409 lignes

time grep -c $ my_file.txt;

réel 0m44,96s utilisateur 0m41.59s sys 0m3.09s

time wc -l my_file.txt;

réel 0m37.57s utilisateur 0m33.48s sys 0m3.97s

time sed -n '$=' my_file.txt;

réel 0m38.22s utilisateur 0m28.05s sys 0m10.14s

time Perl -ne 'END { $_=$.;if(!/^[0-9]+$/){$_=0;};print "$_" }' my_file.txt;

0m23.38s réel utilisateur 0m20.19s sys 0m3.11s réel

time awk 'END { print NR }' my_file.txt;

0m19.90s réel utilisateur 0m16.76s sys 0m3.12s

spark-Shell
import org.joda.time._
val t_start = DateTime.now()
sc.textFile("file://my_file.txt").count()
val t_end = DateTime.now()
new Period(t_start, t_end).toStandardSeconds()

res1: org.joda.time.Seconds = PT15S

5
Pramod Tiwari

Hadoop fournit essentiellement un mécanisme permettant d’obtenir une performance similaire à celle suggérée par @Ivella. 

Le système de fichiers distribués (HDFS) de Hadoop va enregistrer votre fichier de 20 Go dans le cluster, par blocs de taille fixe. Disons que vous configurez une taille de bloc de 128 Mo, le fichier serait divisé en blocs de 20 x 8 x 128 Mo.

Vous exécuteriez ensuite un programme de réduction de la mappe sur ces données, en comptant essentiellement les lignes pour chaque bloc (au stade de la mappe), puis en réduisant le nombre de lignes en bloc pour obtenir un nombre final de lignes pour le fichier entier.

En ce qui concerne les performances, en règle générale, plus votre cluster est grand, meilleures sont les performances (plus de wc s'exécutant en parallèle sur des disques plus indépendants), mais il y a une surcharge dans l'orchestration des travaux qui signifie que l'exécution du travail sur des fichiers plus petits ne produira pas plus rapidement débit que l'exécution d'un wc local

3
Chris White

Je ne suis pas sûr que python soit plus rapide:

[root@myserver scripts]# time python -c "print len(open('mybigfile.txt').read().split('\n'))"

644306


real    0m0.310s
user    0m0.176s
sys     0m0.132s

[root@myserver scripts]# time  cat mybigfile.txt  | wc -l

644305


real    0m0.048s
user    0m0.017s
sys     0m0.074s
2
eugene

Si votre goulot d'étranglement est le disque, il importe de savoir comment vous le lisez dd if=filename bs=128M | wc -l est un lot plus rapide que wc -l filename ou cat filename | wc -l pour ma machine qui a un disque dur, un processeur rapide et de la RAM. Vous pouvez jouer avec la taille du bloc et voir ce que dd indique comme débit. Je l'ai monté à 1GiB.

Remarque: Il y a un débat quant à savoir si cat ou dd est plus rapide. Tout ce que je prétends, c'est que dd peut être plus rapide, selon le système, et que c'est pour moi. Essayez par vous-même.

2
sudo

Si votre ordinateur est équipé de python, vous pouvez essayer ceci depuis le shell:

python -c "print len(open('test.txt').read().split('\n'))"

Ceci utilise python -c pour passer une commande, qui consiste essentiellement à lire le fichier et à la scinder par la "nouvelle ligne" pour obtenir le nombre de nouvelles lignes ou la longueur totale du fichier.

@ BlueMoon's :

bash-3.2$ sed -n '$=' test.txt
519

En utilisant ce qui précède:

bash-3.2$ python -c "print len(open('test.txt').read().split('\n'))"
519
1
ZenOfPython

trouver -type f -name "filepattern_2015_07 _ *. txt" -exec ls -1 {} \; | chat | awk '// {print $ 0, system ("cat" $ 0 "|" "wc -l")}'

Sortie:

Supposons:

  • Votre système de fichiers est distribué
  • Votre système de fichiers peut facilement remplir la connexion réseau à un seul nœud
  • Vous accédez à vos fichiers comme des fichiers normaux

alors vous voulez vraiment couper les fichiers en parties, compter les parties en parallèle sur plusieurs nœuds et résumer les résultats à partir de là (c'est essentiellement l'idée de @Chris White).

Voici comment procéder avec GNU Parallel (version> 20161222). Vous devez répertorier les nœuds dans ~/.parallel/my_cluster_hosts et vous devez avoir un accès ssh à chacun d'entre eux:

parwc() {
    # Usage:
    #   parwc -l file                                                                

    # Give one chunck per Host                                                     
    chunks=$(cat ~/.parallel/my_cluster_hosts|wc -l)
    # Build commands that take a chunk each and do 'wc' on that                    
    # ("map")                                                                      
    parallel -j $chunks --block -1 --pipepart -a "$2" -vv --dryrun wc "$1" |
        # For each command                                                         
        #   log into a cluster Host                                                
        #   cd to current working dir                                              
        #   execute the command                                                    
        parallel -j0 --slf my_cluster_hosts --wd . |
        # Sum up the number of lines                                               
        # ("reduce")                                                               
        Perl -ne '$sum += $_; END { print $sum,"\n" }'
}

Utilisé comme:

parwc -l myfile
parwc -w myfile
parwc -c myfile
0
Ole Tange