web-dev-qa-db-fra.com

Lisez les données du fichier CSV et transformez la chaîne en un type de données correct, y compris une colonne de liste d'entiers

Lorsque je relis des données à partir d'un fichier CSV, chaque cellule est interprétée comme une chaîne.

  • Comment puis-je convertir automatiquement les données que j'ai lues dans le bon type?
  • Ou mieux: comment dire au lecteur csv le type de données correct de chaque colonne?

(J'ai écrit une liste bidimensionnelle, où chaque colonne est d'un type différent (bool, str, int, liste d'entiers), vers un fichier CSV.)

Exemples de données (dans un fichier CSV):

IsActive,Type,Price,States
True,Cellphone,34,"[1, 2]"
,FlatTv,3.5,[2]
False,Screen,100.23,"[5, 1]"
True,Notebook, 50,[1]
23
wewa

Comme le les documents expliquent , le lecteur CSV n'effectue pas la conversion automatique des données. Vous avez l'option de format QUOTE_NONNUMERIC, mais cela ne convertirait que tous les champs non entre guillemets en flottants. Il s'agit d'un comportement très similaire aux autres lecteurs csv.

Je ne pense pas que le module csv de Python serait d'une quelconque aide pour ce cas. Comme d'autres l'ont déjà souligné, literal_eval() est un bien meilleur choix.

Ce qui suit fonctionne et se convertit:

  • cordes
  • int
  • flotteurs
  • listes
  • dictionnaires

Vous pouvez également l'utiliser pour les booléens et NoneType, bien que ceux-ci doivent être formatés en conséquence pour que literal_eval() passe. LibreOffice Calc affiche les booléens en majuscules, quand en Python les booléens sont en majuscules. De plus, vous devrez remplacer les chaînes vides par None (sans guillemets)

J'écris un importateur pour mongodb qui fait tout cela. Ce qui suit fait partie du code que j'ai écrit jusqu'à présent.

[REMARQUE: Mon csv utilise tab comme délimiteur de champ. Vous voudrez peut-être également ajouter une gestion des exceptions]

def getFieldnames(csvFile):
    """
    Read the first row and store values in a Tuple
    """
    with open(csvFile) as csvfile:
        firstRow = csvfile.readlines(1)
        fieldnames = Tuple(firstRow[0].strip('\n').split("\t"))
    return fieldnames

def writeCursor(csvFile, fieldnames):
    """
    Convert csv rows into an array of dictionaries
    All data types are automatically checked and converted
    """
    cursor = []  # Placeholder for the dictionaries/documents
    with open(csvFile) as csvFile:
        for row in islice(csvFile, 1, None):
            values = list(row.strip('\n').split("\t"))
            for i, value in enumerate(values):
                nValue = ast.literal_eval(value)
                values[i] = nValue
            cursor.append(dict(Zip(fieldnames, values)))
    return cursor
14
cortopy

Vous devez mapper vos lignes:

data = """True,foo,1,2.3,baz
False,bar,7,9.8,qux"""

reader = csv.reader(StringIO.StringIO(data), delimiter=",")
parsed = (({'True':True}.get(row[0], False),
           row[1],
           int(row[2]),
           float(row[3]),
           row[4])
          for row in reader)
for row in parsed:
    print row

résulte en

(True, 'foo', 1, 2.3, 'baz')
(False, 'bar', 7, 9.8, 'qux')
7
user647772

Je sais que c'est une question assez ancienne, étiquetée python-2.5 , mais voici la réponse qui fonctionne avec Python 3.6+ qui pourrait intéresser les gens qui utilisent plus d'up-) versions à jour de la langue.

Il s'appuie sur le intégré typing.NamedTuple classe qui a été ajoutée dans Python 3.5. Ce qui peut ne pas être évident dans la documentation est que le "type" de chaque champ peut être une fonction.

L'exemple de code d'utilisation utilise également des soi-disant f-string littéraux qui n'ont pas été ajoutés avant Python 3.6, mais leur utilisation n'est pas requise pour faire les données de base transformations de type.

#!/usr/bin/env python3.6
import ast
import csv
from typing import NamedTuple


class Record(NamedTuple):
    """ Define the fields and their types in a record. """
    IsActive : bool
    Type: str
    Price: float
    States: ast.literal_eval  # Handles string represenation of literals.

    @classmethod
    def _transform(cls: 'Record', dct: dict) -> dict:
        """ Convert string values in given dictionary to corresponding Record
            field type.
        """
        return {field: cls._field_types[field](value)
                    for field, value in dct.items()}


filename = 'test_transform.csv'

with open(filename, newline='') as file:
    for i, row in enumerate(csv.DictReader(file)):
        row = Record._transform(row)
        print(f'row {i}: {row}')

Production:

row 0: {'IsActive': True, 'Type': 'Cellphone', 'Price': 34.0, 'States': [1, 2]}
row 1: {'IsActive': False, 'Type': 'FlatTv', 'Price': 3.5, 'States': [2]}
row 2: {'IsActive': True, 'Type': 'Screen', 'Price': 100.23, 'States': [5, 1]}
row 3: {'IsActive': True, 'Type': 'Notebook', 'Price': 50.0, 'States': [1]}

Généraliser cela en créant une classe de base avec juste la méthode de classe générique n'est pas simple à cause de la façon dont typing.NamedTuple est implémenté.

Pour éviter ce problème, dans Python 3.7+, un dataclasses.dataclass pourrait être utilisé à la place car ils n'ont pas de problème d'héritage - donc créer une classe de base générique qui peut être réutilisée est simple:

#!/usr/bin/env python3.7
import ast
import csv
from dataclasses import dataclass, fields
from typing import Type, TypeVar

T = TypeVar('T', bound='GenericRecord')

class GenericRecord:
    """ Generic base class for transforming dataclasses. """
    @classmethod
    def _transform(cls: Type[T], dict_: dict) -> dict:
        """ Convert string values in given dictionary to corresponding type. """
        return {field.name: field.type(dict_[field.name])
                    for field in fields(cls)}


@dataclass
class CSV_Record(GenericRecord):
    """ Define the fields and their types in a record.
        Field names must match column names in CSV file header.
    """
    IsActive : bool
    Type: str
    Price: float
    States: ast.literal_eval  # Handles string represenation of literals.


filename = 'test_transform.csv'

with open(filename, newline='') as file:
    for i, row in enumerate(csv.DictReader(file)):
        row = CSV_Record._transform(row)
        print(f'row {i}: {row}')

Dans un sens, il n'est pas vraiment très important de choisir celle que vous utilisez, car une instance de la classe n'a jamais été créée - en utiliser une est juste un moyen propre de spécifier et de conserver une définition des noms de champ et de leur type dans une structure de données d'enregistrement.

6
martineau

Accessoires à Jon Clements et cortopy pour m'avoir enseigné ast.literal_eval! Voici ce que j'ai fini par faire (Python 2; les changements pour 3 devraient être triviaux):

from ast import literal_eval
from csv import DictReader
import csv


def csv_data(filepath, **col_conversions):
    """Yield rows from the CSV file as dicts, with column headers as the keys.

    Values in the CSV rows are converted to Python values when possible,
    and are kept as strings otherwise.

    Specific conversion functions for columns may be specified via
    `col_conversions`: if a column's header is a key in this dict, its
    value will be applied as a function to the CSV data. Specify
    `ColumnHeader=str` if all values in the column should be interpreted
    as unquoted strings, but might be valid Python literals (`True`,
    `None`, `1`, etc.).

    Example usage:

    >>> csv_data(filepath,
    ...          VariousWordsIncludingTrueAndFalse=str,
    ...          NumbersOfVaryingPrecision=float,
    ...          FloatsThatShouldBeRounded=round,
    ...          **{'Column Header With Spaces': arbitrary_function})
    """

    def parse_value(key, value):
        if key in col_conversions:
            return col_conversions[key](value)
        try:
            # Interpret the string as a Python literal
            return literal_eval(value)
        except Exception:
            # If that doesn't work, assume it's an unquoted string
            return value

    with open(filepath) as f:
        # QUOTE_NONE: don't process quote characters, to avoid the value
        # `"2"` becoming the int `2`, rather than the string `'2'`.
        for row in DictReader(f, quoting=csv.QUOTE_NONE):
            yield {k: parse_value(k, v) for k, v in row.iteritems()}

(Je suis un peu méfiant à l'idée d'avoir peut-être manqué quelques cas de coin impliquant des citations. Veuillez commenter si vous voyez des problèmes!)

2
doctaphred

J'ai trop aimé l'approche de @ martineau et j'ai été particulièrement intrigué par son commentaire selon lequel l'essence de son code était une correspondance claire entre les champs et les types. Cela m'a suggéré qu'un dictionnaire fonctionnerait également. D'où la variation de son thème ci-dessous. Cela a bien fonctionné pour moi.

De toute évidence, le champ de valeur dans le dictionnaire est vraiment juste un appelable et pourrait donc être utilisé pour fournir un crochet pour le massage des données ainsi que la transtypage si vous le souhaitez.

import ast
import csv

fix_type = {'IsActive': bool, 'Type': str, 'Price': float, 'States': ast.literal_eval}

filename = 'test_transform.csv'

with open(filename, newline='') as file:
    for i, row in enumerate(csv.DictReader(file)):
        row = {k: fix_type[k](v) for k, v in row.items()}
        print(f'row {i}: {row}')

Production

row 0: {'IsActive': True, 'Type': 'Cellphone', 'Price': 34.0, 'States': [1, 2]}
row 1: {'IsActive': False, 'Type': 'FlatTv', 'Price': 3.5, 'States': [2]}
row 2: {'IsActive': True, 'Type': 'Screen', 'Price': 100.23, 'States': [5, 1]}
row 3: {'IsActive': True, 'Type': 'Notebook', 'Price': 50.0, 'States': [1]}
1
fredmb

Une alternative (bien que cela semble un peu extrême) au lieu d'utiliser ast.literal_eval est le module pyparsing disponible sur PyPi - et voyez si le http://pyparsing.wikispaces.com/file/view/parsePythonValue.py exemple de code est approprié pour quoi vous avez besoin, ou peut être facilement adapté.

0
Jon Clements

J'adore @ martinea réponse. C'est très propre.

Une chose dont j'avais besoin était de convertir seulement quelques valeurs et de laisser tous les autres champs sous forme de chaînes, comme avoir des chaînes par défaut et simplement mettre à jour le type de clés spécifiques.

Pour ce faire, remplacez simplement cette ligne:

row = CSV_Record._transform(row)

par celui-ci:

row.update(CSV_Record._transform(row))

La fonction ' update ' met à jour la ligne de variable directement, en fusionnant le brut les données de l'extrait csv avec les valeurs converties au type correct par la méthode ' _ transform '.

Notez qu'il n'y a pas de ligne ' =' dans la version mise à jour.

J'espère que cela vous aidera au cas où quelqu'un aurait une exigence similaire.

(PS: je suis assez nouveau pour publier sur stackoverflow, alors s'il vous plaît laissez-moi savoir si ce qui précède n'est pas clair)

0
Adrien C.