Comment passer à une ligne particulière dans un énorme fichier texte?

107

Existe-t-il des alternatives au code ci-dessous:

startFromLine = 141978 # or whatever line I need to jump to

urlsfile = open(filename, "rb", 0)

linesCounter = 1

for line in urlsfile:
    if linesCounter > startFromLine:
        DoSomethingWithThisLine(line)

    linesCounter += 1

Si je traite un énorme fichier texte (~15MB)avec des lignes de longueur inconnue mais différente, et que je dois passer à une ligne particulière, quel numéro je connais à l'avance? Je me sens mal en les traitant un par un alors que je sais que je pourrais ignorer au moins la première moitié du fichier. Vous recherchez une solution plus élégante s'il y en a.

user63503
la source
Comment savez-vous que la première moitié du fichier n'est pas un groupe de "\ n" alors que la seconde moitié est une seule ligne? Pourquoi vous sentez-vous mal à ce sujet?
Andrew Dalke
7
Je pense que le titre est trompeur - tbh 15MB n'est pas vraiment un "fichier texte énorme", c'est le moins qu'on puisse dire ...
pms

Réponses:

30

linecache :

Le linecachemodule permet d'obtenir n'importe quelle ligne d'un fichier source Python, tout en essayant d'optimiser en interne, en utilisant un cache, le cas courant où de nombreuses lignes sont lues à partir d'un seul fichier. Ceci est utilisé par le tracebackmodule pour récupérer les lignes source à inclure dans le traçage formaté ...

John Ellinwood
la source
165
Je viens de vérifier le code source de ce module: tout le fichier est lu en mémoire! J'écarterais donc définitivement cette réponse dans le but d'accéder rapidement à une ligne donnée dans un fichier.
MiniQuark
MiniQuark, je l'ai essayé, ça marche vraiment, et très vite. J'aurai besoin de voir ce qui se passe si je travaille sur une douzaine de fichiers en même temps de cette façon, savoir à quel moment mon système meurt.
user63503
5
Le gestionnaire de mémoire virtuelle de votre système d'exploitation aide un peu, donc la lecture de gros fichiers en mémoire ne sera peut-être pas lente si vous ne générez pas beaucoup de défauts de pages :) Au contraire, faites-le de manière "stupide" et allouez beaucoup et beaucoup de mémoire peut être extrêmement rapide. J'ai apprécié l'article du développeur danois de FreeBSD Poul-Henning Kamp à ce sujet: queue.acm.org/detail.cfm?id=1814327
Morten Jensen
13
essayez le fichier 100G, ça craint. je dois utiliser f.tell (), f.seek (), f.readline ()
whi
114

Vous ne pouvez pas avancer sans lire le fichier au moins une fois, car vous ne savez pas où se trouvent les sauts de ligne. Vous pouvez faire quelque chose comme:

# Read in the file once and build a list of line offsets
line_offset = []
offset = 0
for line in file:
    line_offset.append(offset)
    offset += len(line)
file.seek(0)

# Now, to skip to line n (with the first line being line 0), just do
file.seek(line_offset[n])
Adam Rosenfield
la source
2
+1, mais sachez que cela n'est utile que s'il va sauter sur plusieurs lignes aléatoires! mais s'il ne saute que sur une ligne, alors c'est du gaspillage
aen
3
+1: De plus, si le fichier ne change pas, l'index des numéros de ligne peut être décapé et réutilisé, ce qui amortit davantage le coût initial de l'analyse du fichier.
S.Lott
OK, après avoir sauté là-bas, comment traiterais-je alors ligne par ligne à partir de cette position?
user63503
8
Une chose à noter (notamment sous windows): veillez à ouvrir le fichier en mode binaire, ou bien utilisez offset = file.tell (). En mode texte sous Windows, la ligne sera un octet plus courte que sa longueur brute sur le disque (\ r \ n remplacé par \ n)
Brian
2
@photographer: Utilisez read () ou readline (), ils partent de la position actuelle définie par seek.
S.Lott
22

Vous n'avez pas vraiment beaucoup d'options si les lignes sont de longueur différente ... vous devez malheureusement traiter les caractères de fin de ligne pour savoir quand vous êtes passé à la ligne suivante.

Cependant, vous pouvez accélérer considérablement cette opération ET réduire l'utilisation de la mémoire en modifiant le dernier paramètre sur «ouvrir» par quelque chose qui n'est pas 0.

0 signifie que l'opération de lecture de fichier est sans tampon, ce qui est très lent et gourmand en disque. 1 signifie que le fichier est mis en tampon en ligne, ce qui serait une amélioration. Tout ce qui est supérieur à 1 (disons 8k .. c'est-à-dire: 8096 ou plus) lit des morceaux du fichier en mémoire. Vous y accédez toujours via for line in open(etc):, mais python ne va que petit à petit, supprimant chaque morceau mis en mémoire tampon après son traitement.

Jarret Hardie
la source
6
8K est 8192, peut-être mieux d'écrire 8 << 10 pour être du bon côté. :)
détendez-vous
Savez-vous par hasard que la taille de la mémoire tampon est spécifiée sur les octets? Quel est le format approprié? Puis-je écrire «8k»? Ou cela devrait être «8096»?
user63503
1
HAHAHA ... ça doit être vendredi ... Je ne peux clairement pas faire de maths. La taille du tampon est en effet un entier exprimant des octets, alors écrivez 8192 (pas 8096 :-)), plutôt que 8
Jarret Hardie
Mon plaisir - j'espère que cela fonctionne. Sur un système moderne, vous pouvez probablement augmenter un peu la taille de la mémoire tampon. 8k est juste un souvenir dans ma mémoire pour une raison que je ne peux pas identifier.
Jarret Hardie
J'ai fait quelques tests ici, et le mettre à -1 (par défaut du système d'exploitation, souvent 8k, mais souvent difficile à dire), semble être à peu près aussi rapide que possible. Cela dit, cela peut être dû en partie au fait que je teste sur un serveur virtuel.
Oscar Smith du
12

Je suis probablement gâté par un bélier abondant, mais 15 M, ce n'est pas énorme. La lecture en mémoire avec readlines() est ce que je fais habituellement avec des fichiers de cette taille. Accéder à une ligne après cela est trivial.

SilentGhost
la source
Pourquoi j'hésitais légèrement à lire le fichier entier - je pourrais avoir plusieurs de ces processus en cours d'exécution, et si une douzaine de ceux-ci lisaient 12 fichiers de 15 Mo chacun, cela pourrait ne pas être bon. Mais j'ai besoin de le tester pour savoir s'il fonctionnera. Je vous remercie.
user63503
4
Hrm, et si c'est un fichier de 1 Go?
Noah
@photographer: même "plusieurs" processus lisant des fichiers de 15 Mo ne devraient pas avoir d'importance sur une machine moderne typique (en fonction, bien sûr, de ce que vous en faites exactement).
Jacob Gabrielson
Jacob, oui, je devrais juste essayer. Le (s) processus est / sont en cours d'exécution sur une machine virtuelle pendant des semaines si vm n'est pas en panne. Malheureusement, la dernière fois, il s'est écrasé après 6 jours. J'ai besoin de continuer là où ça s'est soudainement arrêté. Encore faut-il savoir comment trouver où il a été laissé.
user63503
@Noah: mais ce n'est pas le cas! Pourquoi n'allez-vous pas plus loin? Et si fichier 128 To? Que de nombreux systèmes d'exploitation ne pourraient pas le supporter. Pourquoi ne pas résoudre le problème au fur et à mesure?
SilentGhost
7

Je suis surpris que personne ne soit mentionné islice

line = next(itertools.islice(Fhandle,index_of_interest,index_of_interest+1),None) # just the one line

ou si vous voulez tout le reste du fichier

rest_of_file = itertools.islice(Fhandle,index_of_interest)
for line in rest_of_file:
    print line

ou si vous voulez toutes les autres lignes du fichier

rest_of_file = itertools.islice(Fhandle,index_of_interest,None,2)
for odd_line in rest_of_file:
    print odd_line
Joran Beasley
la source
5

Puisqu'il n'y a aucun moyen de déterminer la longueur de toutes les lignes sans les lire, vous n'avez pas d'autre choix que d'itérer sur toutes les lignes avant votre ligne de départ. Tout ce que vous pouvez faire est de lui donner une belle apparence. Si le fichier est vraiment énorme, vous pouvez utiliser une approche basée sur un générateur:

from itertools import dropwhile

def iterate_from_line(f, start_from_line):
    return (l for i, l in dropwhile(lambda x: x[0] < start_from_line, enumerate(f)))

for line in iterate_from_line(open(filename, "r", 0), 141978):
    DoSomethingWithThisLine(line)

Remarque: l'indice est basé sur zéro dans cette approche.


la source
4

Si vous ne souhaitez pas lire l'intégralité du fichier en mémoire ... vous devrez peut-être trouver un format autre que le texte brut.

bien sûr, tout dépend de ce que vous essayez de faire et de la fréquence à laquelle vous sauterez dans le fichier.

Par exemple, si vous allez sauter plusieurs fois aux lignes dans le même fichier et que vous savez que le fichier ne change pas pendant que vous travaillez avec, vous pouvez faire ceci:
Commencez par parcourir le fichier entier et enregistrez le " find-location "de certains numéros de ligne-clé (comme, jamais 1000 lignes),
puis si vous voulez la ligne 12005, sautez à la position de 12000 (que vous avez enregistrée) puis lisez 5 lignes et vous saurez que vous êtes à la ligne 12005 et ainsi de suite

hasen
la source
3

Si vous connaissez à l'avance la position dans le fichier (plutôt que le numéro de ligne), vous pouvez utiliser file.seek () pour aller à cette position.

Edit : vous pouvez utiliser la fonction linecache.getline (filename, lineno) , qui retournera le contenu de la ligne lineno, mais seulement après avoir lu le fichier entier en mémoire. Bon si vous accédez au hasard à des lignes à partir du fichier (comme Python lui-même voudra peut-être le faire pour imprimer un retraçage) mais pas bon pour un fichier de 15 Mo.

Noé
la source
Je n'utiliserais certainement pas linecache à cette fin, car il lit tout le fichier en mémoire avant de renvoyer la ligne demandée.
MiniQuark
Ouais, ça sonnait trop beau pour être vrai. Je souhaite toujours qu'il y ait un module pour faire cela efficacement, mais j'ai tendance à utiliser la méthode file.seek () à la place.
Noah
3

Qu'est-ce qui génère le fichier que vous souhaitez traiter? Si c'est quelque chose sous votre contrôle, vous pouvez générer un index (quelle ligne est à quelle position.) Au moment où le fichier est ajouté. Le fichier d'index peut être de taille de ligne fixe (espace avec espace ou 0 avec 0) et sera certainement plus petit. Et ainsi peut être lu et traité rapidement.

  • Quelle ligne voulez-vous ?.
  • Calculer le décalage d'octet du numéro de ligne correspondant dans le fichier d'index (possible car la taille de ligne du fichier d'index est constante).
  • Utilisez seek ou autre pour sauter directement pour obtenir la ligne du fichier d'index.
  • Analyser pour obtenir le décalage d'octet de la ligne correspondante du fichier réel.
kamathln
la source
3

J'ai eu le même problème (besoin de récupérer à partir d'une énorme ligne spécifique de fichier).

Sûrement, je peux à chaque fois parcourir tous les enregistrements du fichier et l'arrêter lorsque le compteur sera égal à la ligne cible, mais cela ne fonctionne pas efficacement dans un cas où vous souhaitez obtenir un nombre pluriel de lignes spécifiques. Cela a provoqué la résolution du problème principal - comment gérer directement à l'endroit nécessaire du fichier.

J'ai découvert la décision suivante: tout d'abord, j'ai terminé le dictionnaire avec la position de départ de chaque ligne (la clé est le numéro de ligne et la valeur - la longueur cumulée des lignes précédentes).

t = open(file,’r’)
dict_pos = {}

kolvo = 0
length = 0
for each in t:
    dict_pos[kolvo] = length
    length = length+len(each)
    kolvo = kolvo+1

finalement, fonction de visée:

def give_line(line_number):
    t.seek(dict_pos.get(line_number))
    line = t.readline()
    return line

t.seek (line_number) - commande qui exécute l'élagage du fichier jusqu'à la création de la ligne. Donc, si vous validez ensuite readline, vous obtenez votre ligne cible.

En utilisant une telle approche, j'ai gagné beaucoup de temps.

user3810114
la source
3

Vous pouvez utiliser mmap pour trouver le décalage des lignes. MMap semble être le moyen le plus rapide de traiter un fichier

exemple:

with open('input_file', "r+b") as f:
    mapped = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ)
    i = 1
    for line in iter(mapped.readline, ""):
        if i == Line_I_want_to_jump:
            offsets = mapped.tell()
        i+=1

puis utilisez f.seek (décalages) pour passer à la ligne dont vous avez besoin

George
la source
2

Les lignes elles-mêmes contiennent-elles des informations d'index? Si le contenu de chaque ligne était quelque chose comme " <line index>:Data", alors l' seek()approche pourrait être utilisée pour faire une recherche binaire dans le fichier, même si le montant de Dataest variable. Vous cherchiez au milieu du fichier, lisez une ligne, vérifiez si son index est supérieur ou inférieur à celui que vous souhaitez, etc.

Sinon, le mieux que vous puissiez faire est juste readlines(). Si vous ne voulez pas lire tous les 15 Mo, vous pouvez utiliser l' sizehintargument pour au moins remplacer un grand nombre de readline()s par un plus petit nombre d'appels à readlines().

DNS
la source
2

Si vous avez affaire à un fichier texte et basé sur un système Linux , vous pouvez utiliser les commandes linux.
Pour moi, cela a bien fonctionné!

import commands

def read_line(path, line=1):
    return commands.getoutput('head -%s %s | tail -1' % (line, path))

line_to_jump = 141978
read_line("path_to_large_text_file", line_to_jump)
HongKun Yoo
la source
bien sûr, il n'est pas compatible avec Windows ou certains types de shells Linux qui ne prennent pas en charge head / tail.
Wizmann
Est-ce plus rapide que de le faire en Python?
Shamoon
Cela peut-il avoir plusieurs lignes?
Shamoon
1

Voici un exemple utilisant 'readlines (sizehint)' pour lire un morceau de lignes à la fois. DNS a souligné cette solution. J'ai écrit cet exemple parce que les autres exemples ici sont orientés sur une seule ligne.

def getlineno(filename, lineno):
    if lineno < 1:
        raise TypeError("First line is line 1")
    f = open(filename)
    lines_read = 0
    while 1:
        lines = f.readlines(100000)
        if not lines:
            return None
        if lines_read + len(lines) >= lineno:
            return lines[lineno-lines_read-1]
        lines_read += len(lines)

print getlineno("nci_09425001_09450000.smi", 12000)
Andrew Dalke
la source
0

Aucune des réponses n'est particulièrement satisfaisante, voici donc un petit extrait pour vous aider.

class LineSeekableFile:
    def __init__(self, seekable):
        self.fin = seekable
        self.line_map = list() # Map from line index -> file position.
        self.line_map.append(0)
        while seekable.readline():
            self.line_map.append(seekable.tell())

    def __getitem__(self, index):
        # NOTE: This assumes that you're not reading the file sequentially.  
        # For that, just use 'for line in file'.
        self.fin.seek(self.line_map[index])
        return self.fin.readline()

Exemple d'utilisation:

In: !cat /tmp/test.txt

Out:
Line zero.
Line one!

Line three.
End of file, line four.

In:
with open("/tmp/test.txt", 'rt') as fin:
    seeker = LineSeekableFile(fin)    
    print(seeker[1])
Out:
Line one!

Cela implique de faire beaucoup de recherches de fichiers, mais est utile dans les cas où vous ne pouvez pas mettre le fichier entier en mémoire. Il effectue une lecture initiale pour obtenir les emplacements des lignes (il lit donc tout le fichier, mais ne le garde pas en mémoire), puis chaque accès effectue une recherche de fichier après le fait.

J'offre l'extrait ci-dessus sous la licence MIT ou Apache à la discrétion de l'utilisateur.

Joseph Catrambone
la source
-1

Peut utiliser cette fonction pour renvoyer la ligne n:

def skipton(infile, n):
    with open(infile,'r') as fi:
        for i in range(n-1):
            fi.next()
        return fi.next()
ksed
la source
Cette logique ne fonctionne pas s'il y a des lignes vides continues, fi.next () saute toutes les lignes vides à la fois, sinon c'est bien :)
Anvesh Yalamarthy
L'OP ne mentionne pas que les lignes ont des lignes avec des sauts de ligne non standard. Dans ce cas, vous devrez analyser chaque ligne avec au moins une instruction if pour les sauts de ligne partiels.
ksed le