90 votes

Méthode efficace d'analyse de fichiers à largeur fixe en Python

J'essaie de trouver un moyen efficace d'analyser des fichiers contenant des lignes de largeur fixe. Exemple: les 20 premiers caractères représentent une colonne, à partir de 21h30 une autre, etc. Supposons que la ligne contienne 100 caractères. Quel serait un moyen efficace d’analyser une ligne en plusieurs composants?

Je pourrais utiliser le découpage de chaîne par ligne, mais c'est un peu moche si la ligne est grosse ... d'autres méthodes rapides?

80voto

martineau Points 21665

À l'aide de l'Python standard librarystructmodule serait assez facile et extrêmement rapide puisqu'il est écrit en C.

Voici comment il pourrait être utilisé pour faire ce que vous voulez. Il permet également de colonnes de caractères pour être ignorés par la spécification des valeurs négatives pour le nombre de caractères dans le champ.

import struct

fieldwidths = (2, -10, 24)  # negative widths represent ignored padding fields
fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's')
                        for fw in fieldwidths)
fieldstruct = struct.Struct(fmtstring)
parse = fieldstruct.unpack_from
print('fmtstring: {!r}, recsize: {} chars'.format(fmtstring, fieldstruct.size))

line = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n'
fields = parse(line)
print('fields: {}'.format(fields))

Sortie:

fmtstring: '2s 10x 24s', recsize: 36 chars
fields: ('AB', 'MNOPQRSTUVWXYZ0123456789')

Mise à jour 1:

La suite de modifications permettraient d'adapter le travail en Python 2 ou 3 (et la poignée de saisie Unicode):

import sys

fieldstruct = struct.Struct(fmtstring)
if sys.version_info[0] < 3:
    parse = fieldstruct.unpack_from
else:
    # converts unicode input to byte string and results back to unicode string
    unpack = fieldstruct.unpack_from
    parse = lambda line: tuple(s.decode() for s in unpack(line.encode()))

Mise à jour 2:

Voici un moyen de le faire avec de la ficelle tranches, que vous considérez, mais qu'il pourrait obtenir trop laid. Bonne chose à propos de ça — en plus de ne pas être tout ce que laid, c'est qu'il fonctionne inchangé dans les deux Python 2 et 3, ainsi que d'être capable de gérer des chaînes Unicode. Je n'ai pas comparé, mais le suspect, il peut être compétitif avec l'structversion de module speedwise. Il pourrait être accéléré-les légèrement en retrait de la capacité de remplissage des champs.

try:
    from itertools import izip_longest  # added in Py 2.6
except ImportError:
    from itertools import zip_longest as izip_longest  # name change in Py 3.x

try:
    from itertools import accumulate  # added in Py 3.2
except ImportError:
    def accumulate(iterable):
        'Return running totals (simplified version).'
        total = next(iterable)
        yield total
        for value in iterable:
            total += value
            yield total

def make_parser(fieldwidths):
    cuts = tuple(cut for cut in accumulate(abs(fw) for fw in fieldwidths))
    pads = tuple(fw < 0 for fw in fieldwidths) # bool values for padding fields
    flds = tuple(izip_longest(pads, (0,)+cuts, cuts))[:-1]  # ignore final one
    parse = lambda line: tuple(line[i:j] for pad, i, j in flds if not pad)
    # optional informational function attributes
    parse.size = sum(abs(fw) for fw in fieldwidths)
    parse.fmtstring = ' '.join('{}{}'.format(abs(fw), 'x' if fw < 0 else 's')
                                                for fw in fieldwidths)
    return parse

line = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n'
fieldwidths = (2, -10, 24)  # negative widths represent ignored padding fields
parse = make_parser(fieldwidths)
fields = parse(line)
print('format: {!r}, rec size: {} chars'.format(parse.fmtstring, parse.size))
print('fields: {}'.format(fields))

Sortie:

format: '2s 10x 24s', rec size: 36 chars
fields: ('AB', 'MNOPQRSTUVWXYZ0123456789')

74voto

Reiner Gerecke Points 5332

Je ne sais pas vraiment si c'est efficace, mais il doit être lisible (par opposition à faire le découpage manuellement). J'ai défini une fonction slices qui reçoit une chaîne de caractères et les longueurs de colonnes, et renvoie la chaˆ ıne. J'ai fait un générateur, donc pour vraiment long des lignes, de ne pas construire une temporaire de la liste de sous-chaînes.

def slices(s, *args):
    position = 0
    for length in args:
        yield s[position:position + length]
        position += length

Exemple

In [32]: list(slices('abcdefghijklmnopqrstuvwxyz0123456789', 2))
Out[32]: ['ab']

In [33]: list(slices('abcdefghijklmnopqrstuvwxyz0123456789', 2, 10, 50))
Out[33]: ['ab', 'cdefghijkl', 'mnopqrstuvwxyz0123456789']

In [51]: d,c,h = slices('dogcathouse', 3, 3, 5)
In [52]: d,c,h
Out[52]: ('dog', 'cat', 'house')

Mais je pense que l'avantage d'un générateur est perdu si vous avez besoin de toutes les colonnes à la fois. Où l'on pourrait bénéficier d', c'est quand vous voulez processus de colonnes, une par une, dire dans une boucle.

33voto

Tom M Points 309

deux autres options plus faciles et plus jolies que les solutions déjà mentionnées

le premier utilise des pandas

 import pandas as pd

path = 'filename.txt'

#using pandas with a column specification   
col_specification =[(0, 20), (21, 30), (31, 50), (51, 100)]
data = pd.read_fwf(path, colspecs=col_specification)
 

et la deuxième option en utilisant numpy.loadtxt

 import numpy as np

#using numpy and letting it figure it out automagically
data_also = np.loadtxt(path)
 

Cela dépend vraiment de la manière dont vous voulez utiliser vos données.

15voto

John Machin Points 39706

Le code ci-dessous donne une esquisse de ce que vous pourriez faire si vous avez quelques graves fixe la largeur de la colonne fichier manipulation à faire.

"Grave" = plusieurs types d'enregistrements dans chacun de plusieurs types de fichier, enregistre jusqu'à 1000 octets, la mise en page-définisseur et de "s'opposer" producteur/consommateur est un ministère du gouvernement avec l'attitude, la mise en page la suite des changements dans inutilisés colonnes, jusqu'à un million d'enregistrements dans un fichier, ...

Caractéristiques: Précompilation la structure formats. Ignore des colonnes. Convertit les chaînes d'entrée pour les types de données requis (esquisse omet d'erreur de manipulation). Convertit des dossiers aux instances de l'objet (ou les dicts ou nommé de tuples si vous préférez).

Code:

import struct, datetime, cStringIO, pprint

# functions for converting input fields to usable data
cnv_text = lambda s: s.rstrip()
cnv_int = lambda s: int(s)
cnv_date_dmy = lambda s: datetime.datetime.strptime(s, "%d%m%Y") # ddmmyyyy
# etc

# field specs (field name, start pos (1-relative), len, converter func)
fieldspecs = [
    ('surname', 11, 20, cnv_text),
    ('given_names', 31, 20, cnv_text),
    ('birth_date', 51, 8, cnv_date_dmy),
    ('start_date', 71, 8, cnv_date_dmy),
    ]

fieldspecs.sort(key=lambda x: x[1]) # just in case

# build the format for struct.unpack
unpack_len = 0
unpack_fmt = ""
for fieldspec in fieldspecs:
    start = fieldspec[1] - 1
    end = start + fieldspec[2]
    if start > unpack_len:
        unpack_fmt += str(start - unpack_len) + "x"
    unpack_fmt += str(end - start) + "s"
    unpack_len = end
field_indices = range(len(fieldspecs))
print unpack_len, unpack_fmt
unpacker = struct.Struct(unpack_fmt).unpack_from

class Record(object):
    pass
    # or use named tuples

raw_data = """\
....v....1....v....2....v....3....v....4....v....5....v....6....v....7....v....8
          Featherstonehaugh   Algernon Marmaduke  31121969            01012005XX
"""

f = cStringIO.StringIO(raw_data)
headings = f.next()
for line in f:
    # The guts of this loop would of course be hidden away in a function/method
    # and could be made less ugly
    raw_fields = unpacker(line)
    r = Record()
    for x in field_indices:
        setattr(r, fieldspecs[x][0], fieldspecs[x][3](raw_fields[x]))
    pprint.pprint(r.__dict__)
    print "Customer name:", r.given_names, r.surname

Sortie:

78 10x20s20s8s12x8s
{'birth_date': datetime.datetime(1969, 12, 31, 0, 0),
 'given_names': 'Algernon Marmaduke',
 'start_date': datetime.datetime(2005, 1, 1, 0, 0),
 'surname': 'Featherstonehaugh'}
Customer name: Algernon Marmaduke Featherstonehaugh

4voto

user1019129 Points 261
> str = '1234567890'
> w = [0,2,5,7,10]
> [ str[ w[i-1] : w[i] ] for i in range(1,len(w)) ]
['12', '345', '67', '890']

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X