2009-11-13 8 views
9

Y a-t-il une façon plus courte (peut-être plus pythonique) d'ouvrir un fichier texte et de lire les lignes qui commencent par un caractère de commentaire?Plus de façon pythonique de sauter les lignes d'en-tête

En d'autres termes, d'une manière plus propre de le faire

fin = open("data.txt") 
line = fin.readline() 
while line.startswith("#"): 
    line = fin.readline() 
+0

http://stackoverflow.com/questions/1706198/python-how-to-ignore-comment-lines-when-reading-in-a-file/1706204#1706204 – ghostdog74

+11

"Shorter" n'est pas nécessairement "Pythonic" . Ce que vous avez est très net, clair et évident. Le fait de l'écraser dans un doublage obscur ne favorise pas toujours la Pythonicité. Tout comme j'aime les outils, parfois son approche fonctionnelle me fait arrêter de me gratter la tête. Le code Pythonic devrait nécessiter peu ou pas de grattage de la tête. Si je devais voter pour une forme alternative et l'appeler plus Pythonic, ce serait la solution de compréhension de liste de Jim Dennis. – PaulMcG

Répondre

16

A ce stade de mon arc d'apprendre Python, je trouve cela très Pythonic:

def iscomment(s): 
    return s.startswith('#') 

from itertools import dropwhile 
with open(filename, 'r') as f: 
    for line in dropwhile(iscomment, f): 
     # do something with line 

pour ignorer toutes les lignes en haut du fichier commençant par #. Pour sauter toutes les lignes commençant par #:

from itertools import ifilterfalse 
with open(filename, 'r') as f: 
    for line in ifilterfalse(iscomment, f): 
     # do something with line 

qui est presque tout au sujet de la lisibilité pour moi; fonctionnellement il n'y a presque pas de différence entre:

for line in ifilterfalse(iscomment, f)) 

et

for line in (x for x in f if not x.startswith('#')) 

Briser le test dans sa propre fonction fait l'objet du code un peu plus clair; cela signifie aussi que si votre définition d'un commentaire change, vous avez un endroit pour le changer.

+0

ceux 'while's devraient être' with's, oui? – Autoplectic

+0

Yikes. Fixe, merci. –

+0

Cela fonctionne pour Python 2, pour Python 3, vous devriez utiliser 'filterfalse' au lieu de' ifilterfalse'. – nix

14
for line in open('data.txt'): 
    if line.startswith('#'): 
     continue 
    # work with line 

bien sûr, si vos lignes ne sont commentées au début du fichier, vous pouvez utiliser quelques optimisations.

+0

+1 Clair et explicite. S'il y a plus de conditions pour filtrer les lignes, il suffit d'ajouter la prochaine vérification comme ceci et cela reste clair. Contrairement aux filtres d'empilage. –

6

Si vous voulez filtrer tous les lignes de commentaires (pas seulement celles qui sont au début du fichier):

for line in file("data.txt"): 
    if not line.startswith("#"): 
    # process line 

Si vous voulez seulement ignorer ceux au début puis voir la réponse de ephemient en utilisant itertools.dropwhile

4

Vous pouvez faire un générateur qui passe en boucle sur le fichier qui saute ces lignes:

fin = open("data.txt") 
fileiter = (l for l in fin if not l.startswith('#')) 

for line in fileiter: 
    ... 
5

Vous pouvez utiliser une fonction de générateur

def readlines(filename): 
    fin = open(filename) 
    for line in fin: 
     if not line.startswith("#"): 
      yield line 

et de l'utiliser comme

for line in readlines("data.txt"): 
    # do things 
    pass 

Selon exactement où les fichiers viennent, vous pouvez également strip() les lignes avant la vérification startswith(). Une fois, je devais déboguer un script comme mois après avoir été écrit parce que quelqu'un a mis en quelques caractères d'espace avant le « # »

+1

Ceci filtre toutes les lignes qui commencent par '#', pas seulement celles au début ("head") du fichier - OP n'est pas complètement clair sur le comportement désiré. – ephemient

+0

En outre, vous pouvez utiliser une expression de générateur: 'pour line in (ligne pour line in open ('data.txt') sinon line.startswith ('#')):' – ephemient

+0

Voir ma réponse pour une version de ceci ne supprime que les lignes '#' du début du fichier, et non du fichier entier. – steveha

10
from itertools import dropwhile 
for line in dropwhile(lambda line: line.startswith('#'), file('data.txt')): 
    pass 
2

Vous pouvez faire quelque chose comme

def drop(n, seq): 
    for i, x in enumerate(seq): 
     if i >= n: 
      yield x 

Et puis disent

for line in drop(1, file(filename)): 
    # whatever 
2

J'aime l'idée de la fonction de générateur @ iWerner. Un petit changement à son code et il fait ce que la question a demandé.

def readlines(filename): 
    f = open(filename) 
    # discard first lines that start with '#' 
    for line in f: 
     if not line.lstrip().startswith("#"): 
      break 
    yield line 

    for line in f: 
     yield line 

et de l'utiliser comme

for line in readlines("data.txt"): 
    # do things 
    pass 

Mais voici une approche différente. C'est presque très simple. L'idée est que nous ouvrons le fichier et obtenons un objet fichier, que nous pouvons utiliser comme un itérateur. Ensuite, nous tirons les lignes que nous ne voulons pas de l'itérateur, et retournons simplement l'itérateur. Ce serait idéal si nous savions toujours combien de lignes sauter. Le problème ici est que nous ne savons pas combien de lignes nous devons sauter; nous avons juste besoin de tirer les lignes et de les regarder. Et il n'y a aucun moyen de remettre une ligne dans l'itérateur, une fois que nous l'avons tiré. Donc: ouvrir l'itérateur, tirer les lignes et compter combien de ont le caractère '#' en tête; puis utilisez la méthode .seek() pour rembobiner le fichier, tirez à nouveau le bon numéro et renvoyez l'itérateur. Une chose que j'aime à ce sujet: vous récupérez l'objet fichier réel, avec toutes ses méthodes; vous pouvez simplement utiliser ceci au lieu de open() et cela fonctionnera dans tous les cas. J'ai renommé la fonction à open_my_text() pour refléter cela.

def open_my_text(filename): 
    f = open(filename, "rt") 
    # count number of lines that start with '#' 
    count = 0 
    for line in f: 
     if not line.lstrip().startswith("#"): 
      break 
     count += 1 

    # rewind file, and discard lines counted above 
    f.seek(0) 
    for _ in range(count): 
     f.readline() 

    # return file object with comment lines pre-skipped 
    return f 

Au lieu de f.readline() je aurais pu utiliser f.next() (pour Python 2.x) ou next(f) (pour Python 3.x) mais je voulais écrire il était portable à tout Python.

EDIT: D'accord, je sais que personne ne se soucie et je "ne reçois aucun upvotes pour cela, mais je l'ai re-écrit ma réponse une dernière fois pour le rendre plus élégant

Vous ne pouvez pas mettre un. retournez dans un itérateur, mais vous pouvez ouvrir un fichier deux fois et obtenir deux itérateurs, étant donné la façon dont fonctionne la mise en cache des fichiers, le deuxième itérateur est presque libre. , cette version serait grandement surperformer la version précédente qui appelle f.seek(0).

def open_my_text(filename): 
    # open the same file twice to get two file objects 
    # (We are opening the file read-only so this is safe.) 
    ftemp = open(filename, "rt") 
    f = open(filename, "rt") 

    # use ftemp to look at lines, then discard from f 
    for line in ftemp: 
     if not line.lstrip().startswith("#"): 
      break 
     f.readline() 

    # return file object with comment lines pre-skipped 
    return f 

cette version est beaucoup mieux que la version précédente, et encore des retours un objet fichier complet avec toutes ses méthodes.

+1

Au lieu de compter, pourquoi ne pas utiliser 'f.tell()' dans votre boucle pour enregistrer l'endroit actuel dans le fichier? Remplacez 'count = 0' par' loc = 0', 'count + = 1' par' loc = f.tell() ', et' f.seek (0) 'par' f.seek (loc) 'et supprimez votre boucle 'for_ in range (count)' tout à fait. – PaulMcG

+0

J'aime la suggestion, mais je l'ai juste essayé et ça ne marche pas. La méthode '.tell()' ne suit pas l'itérateur; mon court fichier de test a été complètement inséré et '.tell()' a renvoyé la fin de fichier chaque fois que je l'ai appelé. Si '.tell()' a fait un suivi avec l'itérateur, je le ferais absolument à votre façon; c'est plus propre. Mon code est plus salissant, mais a l'avantage de fonctionner réellement ... :-) – steveha

5

En pratique, si je savais que je traitais des fichiers texte de taille raisonnable (tout ce qui se intègrera confortablement dans la mémoire) puis j'aller problème avec quelque chose comme:

f = open("data.txt") 
lines = [ x for x in f.readlines() if x[0] != "#" ] 

... à snarf dans le fichier entier et filtrer toutes les lignes qui commencent par l'octothorpe.

Comme d'autres l'ont souligné qu'on pourrait vouloir ignorer les espaces au lieu avant le si dièse comme:

lines = [ x for x in f.readlines() if not x.lstrip().startswith("#") ] 

J'aime ce pour sa brièveté. Cela suppose que nous voulons supprimer toutes les lignes de commentaire.

On peut aussi « couper » les derniers caractères (presque toujours) au large de la nouvelles lignes fin de chaque utilisation de:

lines = [ x[:-1] for x in ... ] 

... en supposant que nous ne sommes pas préoccupés par la question tristement célèbre obscure d'un manque nouvelle ligne finale sur la dernière ligne du fichier.(La seule fois où une ligne de la .readlines() ou des méthodes d'objet de type fichier similaires ne se termine pas dans un retour à la ligne est à EOF).

Dans raisonnablement versions récentes d'un Python peut « Chomp » (uniquement des sauts de ligne) les extrémités des lignes en utilisant une expression conditionnelle comme ceci:

lines = [ x[:-1] if x[-1]=='\n' else x for x in ... ] 

... qui est à peu près aussi compliqué que je J'y vais avec une compréhension de la liste pour des raisons de lisibilité. Si nous étions inquiets de la possibilité d'un fichier trop volumineux (ou de faibles contraintes de mémoire) ayant un impact sur nos performances ou notre stabilité, nous utilisons une version de Python suffisamment récente pour supporter les expressions génératrices (qui sont des ajouts plus récents). à la langue que la liste compréhensions Je me sers ici), nous pourrions utiliser:

for line in (x[:-1] if x[-1]=='\n' else x for x in 
    f.readlines() if x.lstrip().startswith('#')): 

    # do stuff with each line 

... est à la limite de ce que j'attends quelqu'un d'autre à analyser dans une ligne un an après le code a été enregistré po

Si l'intention est seulement d'ignorer les lignes "en-tête" alors je pense que la meilleure approche serait être:

f = open('data.txt') 
for line in f: 
    if line.lstrip().startswith('#'): 
     continue 

... et d'en finir.

Questions connexes