8
votes

Comment conserver les valeurs nulles lors de l'écriture en CSV

J'écris les données du serveur sql dans un fichier csv en utilisant le module csv de Python, puis je télécharge le fichier csv dans une base de données postgres en utilisant la commande copy. Le problème est que l'écrivain csv de Python convertit automatiquement les valeurs Nulls en une chaîne vide "" et qu'il échoue lorsque la colonne est un type de données int ou float et qu'il essaie d'insérer ce "" alors qu'il devrait être une valeur None ou null.

Pour rendre l'interface aussi simple que possible avec des modules implémenter l'API DB, la valeur None est écrite sous forme de chaîne vide.

https://docs.python.org/ 3.4 / library / csv.html? Highlight = csv # csv.writer

Quelle est la meilleure façon de conserver la valeur nulle? Existe-t-il un meilleur moyen d'écrire des csvs en Python? Je suis ouvert à toutes les suggestions.

Exemple:

J'ai des valeurs lat et long:

42.313270000,-71.116240000
42.377010000,-71.064770000
"",""

Lors de l'écriture en csv il convertit les valeurs nulles en "":

with file_path.open(mode='w', newline='') as outfile:
    csv_writer = csv.writer(outfile, delimiter=',', quoting=csv.QUOTE_NONNUMERIC)
    if include_headers:
        csv_writer.writerow(col[0] for col in self.cursor.description)
    for row in self.cursor:
        csv_writer.writerow(row)

.

42.313270000    -71.116240000
42.377010000    -71.064770000
NULL    NULL

NULL

Spécifie la chaîne qui représente une valeur nulle. La valeur par défaut est \ N (barre oblique inverse-N) au format texte, et une chaîne vide sans guillemets en CSV format. Vous pourriez préférer une chaîne vide même au format texte pour les cas où vous ne voulez pas distinguer les valeurs nulles des chaînes vides. Ce l'option n'est pas autorisée lors de l'utilisation du format binaire.

https://www.postgresql.org/docs/9.2/sql- copy.html

ANSWER:[

Ce qui a résolu le problème pour moi a été de changer le devis en csv.QUOTE_MINIMAL.

csv.QUOTE_MINIMAL Demande aux objets d'écriture de ne citer que ces champs qui contiennent des caractères spéciaux tels que délimiteur, quotechar ou tout autre des caractères dans lineterminator.

Questions connexes: - Postgresql COPY chaîne vide comme NULL ne fonctionne pas p >


1 commentaires

pouvez-vous partager un exemple? parce que l'écrivain csv peut écrire des entiers (sous forme de chaînes) et des flottants (sous forme de chaînes). Que voulez-vous écrire à la place de None / "Null"?


5 Réponses :


2
votes

votre code

for row in self.cursor:
    csv_writer.writerow("null" if x is None else x for x in row)

utilise writer tel quel, mais vous n'êtes pas obligé de le faire. Vous pouvez filtrer les valeurs pour changer certaines valeurs particulières avec une compréhension du générateur et une expression ternaire

for row in self.cursor:
    csv_writer.writerow(row)


2 commentaires

Bien que cela ne résout pas l'impossibilité d'insérer des valeurs varchar dans une colonne int ou float, je pense que cela peut aider.


désolé je ne peux pas aider plus sans plus d'entrée. Je ne sais pas pourquoi int ou float ne fonctionne pas comme il se doit avec csv. Et je ne sais pas ce qu'est varchar.



2
votes

Vous demandez csv.QUOTE_NONNUMERIC . Cela transformera tout ce qui n'est pas un nombre en une chaîne. Vous devriez envisager d'utiliser csv.QUOTE_MINIMAL car il pourrait être plus ce que vous recherchez:

Code de test:

,0,,data

Résultats: h3 >

test1.csv:

"",0,"","data"

test2.csv:

import csv

test_data = (None, 0, '', 'data')
for name, quotes in (('test1.csv', csv.QUOTE_NONNUMERIC),
                     ('test2.csv', csv.QUOTE_MINIMAL)):

    with open(name, mode='w') as outfile:
        csv_writer = csv.writer(outfile, delimiter=',', quoting=quotes)
        csv_writer.writerow(test_data))


0 commentaires

-1
votes

J'utiliserais pandas, psycopg2 et sqlalchemy. Assurez-vous que sont installés. Provenant de votre flux de travail actuel et évitant d'écrire en csv

#no need to import psycopg2
import pandas as pd
from sqlalchemy import create_engine


#create connection to postgres
engine = create_engine('postgres://.....')

#get column names from cursor.description
columns = [col[0] for col in self.cursor.description]

#convert data into dataframe
df = pd.DataFrame(cursor.fetchall(),columns=columns)

#send dataframe to postgres
df.to_sql('name_of_table',engine,if_exists='append',index=False)

#if you still need to write to csv
df.to_csv('your_file.csv')


4 commentaires

Pourquoi compliquer les choses avec un logiciel supplémentaire? DataFrame.to_csv () utilise toujours l'objet csv.writer () pour effectuer l'écriture réelle dans un fichier.


D'après ce que je comprends, la seule raison pour laquelle les Nulls sont un problème est que OP ne peut pas les insérer dans Postgres. OP a-t-il vraiment besoin de les enregistrer au format CSV? Ou est-ce qu'OP utilise simplement CSV comme étape intermédiaire. Quoi qu'il en soit, pandas est un bon outil à utiliser spécialement si OP doit transformer les données à l'avenir. Compliqué n'est pas toujours une mauvaise chose. La simplicité n'est pas toujours la meilleure solution.


Notez que to_sql () est assez lent , l'importation CSV est beaucoup plus rapide pour l'importation en masse. Ni l'un ni l'autre ne répond à la question de l'OP, et la réponse à cette question est assez simple: changer la configuration des citations.


Je reconnais que cela ne répond pas directement à la question du PO, mais je maintiens que c'est une alternative, moins performante mais plus flexible. De plus, je suis nerveux et ravi que vous ayez commenté ma réponse.



13
votes

Vous avez deux options ici: changez l'option de citation csv.writing en Python, ou dites à PostgreSQL d'accepter les chaînes entre guillemets comme possibles NULL (nécessite PostgreSQL 9.4 ou plus récent)

Python csv.writer () et citant

Du côté Python, vous dites à l'objet csv.writer () d'ajouter des guillemets, car vous l'avez configuré pour utiliser csv.QUOTE_NONNUMERIC : p>

Demande aux objets writer de citer tous les champs non numériques.

Aucune Les valeurs ne sont pas numériques, donc il en résulte l'écriture de "" .

Passer à l'utilisation de csv.QUOTE_MINIMAL ou csv.QUOTE_NONE :

csv.QUOTE_MINIMAL
Indique aux objets writer de ne citer que les champs qui contiennent des caractères spéciaux tels que délimiteur , quotechar ou l'un des caractères de lineterminator em>.

csv.QUOTE_NONE
Indique aux objets writer de ne jamais citer les champs. Lorsque le délimiteur actuel apparaît dans les données de sortie, il est précédé du caractère escapechar actuel.

Comme vous n'écrivez que des valeurs de longitude et de latitude, vous n'avez pas besoin de guillemets ici, il n'y a pas de délimiteurs ou de guillemets présents dans vos données.

Avec l'une ou l'autre des options, le La sortie CSV pour les valeurs Aucune est une simple chaîne vide:

COPY position (
    lon, 
    lat
) 
FROM "filename"
WITH (
    FORMAT csv,
    NULL '',
    DELIMITER ',',
    FORCE_NULL(lon, lat)
);

PostgreSQL 9.4 COPY FROM , NULL et FORCE_NULL

Depuis PostgreSQL 9.4, vous pouvez également forcer PostgreSQL à accepter les chaînes vides entre guillemets comme NULL s, lorsque vous utilisez le Option FORCE_NULL . À partir de la COPY FROM documentation : p>

FORCE_NULL

Faites correspondre les valeurs des colonnes spécifiées à la chaîne nulle, même si elle a été citée, et si une correspondance est trouvée, définissez la valeur sur NULL . Dans le cas par défaut où la chaîne nulle est vide, cela convertit une chaîne vide entre guillemets en NULL . Cette option n'est autorisée que dans COPY FROM , et uniquement lors de l'utilisation du format CSV.

Nommer les colonnes dans une option FORCE_NULL permet à PostgreSQL d'accepter à la fois la colonne vide et "" comme valeurs NULL pour celles-ci colonnes, par exemple:

>>> import csv
>>> from io import StringIO
>>> def test_csv_writing(rows, quoting):
...     outfile = StringIO()
...     csv_writer = csv.writer(outfile, delimiter=',', quoting=quoting)
...     csv_writer.writerows(rows)
...     return outfile.getvalue()
...
>>> rows = [
...     [42.313270000, -71.116240000],
...     [42.377010000, -71.064770000],
...     [None, None],
... ]
>>> print(test_csv_writing(rows, csv.QUOTE_NONNUMERIC))
42.31327,-71.11624
42.37701,-71.06477
"",""

>>> print(test_csv_writing(rows, csv.QUOTE_MINIMAL))
42.31327,-71.11624
42.37701,-71.06477
,

>>> print(test_csv_writing(rows, csv.QUOTE_NONE))
42.31327,-71.11624
42.37701,-71.06477
,

à quel point les options de citation que vous avez utilisées du côté Python n'ont plus d'importance.

Autres options à considérer

Pour les tâches simples de transformation de données à partir d'autres bases de données, n'utilisez pas Python

Si vous interrogez déjà des bases de données pour rassembler des données pour aller dans PostgreSQL, envisagez de les insérer directement dans Postgres em>. Si les données proviennent d'autres sources, utilisez le wrapper de données étrangères ( fdw ) module vous permet de supprimer les intermédiaires et d'extraire directement des données dans PostgreSQL à partir d'autres sources.

Données numpy? Pensez à utiliser COPY FROM comme binaire, directement à partir de Python

Les données Numpy peuvent être insérées plus efficacement via binaire COPY FROM ; la réponse liée augmente un tableau structuré numpy avec les métadonnées supplémentaires requises et l'ordre des octets, puis crée efficacement une copie binaire des données et l'insère dans PostgreSQL en utilisant COPY FROM STDIN WITH BINARY et psycopg2.copy_expert () méthode . Cela évite parfaitement les conversions de nombre -> texte -> nombre.

Des données persistantes pour gérer de grands ensembles de données dans un pipeline?

Ne réinventez pas les roues du pipeline de données. Pensez à utiliser des projets existants tels que Apache Spark , qui ont déjà résolu les problèmes d'efficacité. Spark vous permet de traiter les données comme un flux structuré a> et inclut l'infrastructure pour exécuter des étapes d'analyse de données en parallèle a >, et vous pouvez traiter les données structurées distribuées comme des dataframes Pandas .

Une autre option pourrait être de consulter Dask pour aider à partager des ensembles de données entre les tâches distribuées pour traiter de grandes quantités de données.

Même si la conversion d'un projet déjà en cours vers Spark peut être un pas trop loin, envisagez au moins d'utiliser Apache Arrow , la plate-forme d'échange de données Spark s'appuie sur. Le projet pyarrow vous permettrait d'échanger des données via des fichiers Parquet, ou échanger des données via IPC .

Les équipes Pandas et Numpy sont assez fortement investis dans la prise en charge des besoins d'Arrow et de Dask (il y a un chevauchement considérable des membres principaux entre ces projets) et travaillent activement pour rendre l'échange de données Python aussi efficace que possible, y compris étendre le module pickle de Python pour permettre des flux de données hors bande pour éviter toute copie inutile de la mémoire lorsque partager des données.


5 commentaires

J'ai fini par utiliser QUOTE_MINIMAL et cela a fonctionné pour la plupart des ensembles de données, mais cela a créé des colonnes supplémentaires pour d'autres. Je cherche toujours pourquoi cela se produit, mais la réponse de Jean-François et la spécification de la valeur nulle dans la commande de copie fonctionnent ... bien que très lentement. Il s'agit d'une étape intermédiaire pour stocker des ensembles de données plus volumineux qui ne peuvent pas tenir en mémoire dans un pipeline de données et je me demande si vous avez des suggestions en dehors de cette question. J'apprécie votre réponse malgré tout.


@JonathanPorter: il semble que vous essayiez de créer un pipeline de données à partir de zéro. Avez-vous envisagé d'utiliser Apache Spark à la place? Spark a une excellente support Python de première classe et vous permet de diffuser des ensembles de données, d'opérer sur des ensembles de données en parallèle ou vous permet d'accéder aux données via Apache Arrow , un format beaucoup plus efficace pour échanger de grandes quantités de colonnes Les données.


@JonathanPorter: La solution de Jean-François est lente car elle transforme chaque ligne à la main, et vous ne pouvez pas vraiment accélérer cette opération lorsque vous avez une source de données structurée en entrée (numpy, lignes de requête de base de données, etc.) car de telles sources rechignent à mélange de types numériques et chaînes. Vous feriez mieux de vous en tenir à QUOTE_NONNUMERIC et d'utiliser FORCE_NULL sur les colonnes numériques à la place. Non pas que je puisse vraiment imaginer ce que vous entendez par «créer des colonnes supplémentaires», cela ressemble à un nouveau problème ailleurs, probablement un bogue dans la façon dont vous écrivez le fichier CSV.


@JonathanPorter: Je pense que je peux penser à un scénario dans lequel vous pourriez voir des colonnes supplémentaires: lorsque vos données contiennent des délimiteurs ou des guillemets dans une valeur et que vous n'avez pas configuré COPY FROM ESCAPE < / code> ou QUOTE pour correspondre aux paramètres utilisés dans votre rédacteur.


Merci pour toutes ces suggestions. En effet, je n'ai pas configuré ces options, mais je vais les vérifier et examiner également Spark! Je ne manquerai pas de publier ce qui a fonctionné le mieux dans mon cas.



2
votes

J'écris les données du serveur sql dans un fichier csv à l'aide du module csv de Python, puis je télécharge le fichier csv dans une base de données postgres à l'aide de la commande copy.

Je pense que votre véritable exigence est que vous devez sauter les lignes de données dans le système de fichiers, et comme la phrase ci-dessus et le titre de la question le montrent clairement, vous le faites actuellement avec un fichier csv. Le problème est que le format csv offre une prise en charge médiocre de la notion SGBDR de NULL. Laissez-moi résoudre votre problème pour vous en modifiant légèrement la question. Je voudrais vous présenter le format parquet. Étant donné un ensemble de lignes de table en mémoire, cela vous permet de très rapidement les conserver dans un fichier binaire compressé, et de les récupérer, avec les métadonnées et les NULL intacts, sans tracas de citations de texte. Voici un exemple, utilisant le moteur de parquet pyarrow 0.12.1 :

      lat     lng
0  42.313 -71.116
1  42.377 -71.065
2     NaN     NaN

Sortie:

import pandas as pd
import pyarrow


def round_trip(fspec='/tmp/locations.parquet'):
    rows = [
        dict(lat=42.313, lng=-71.116),
        dict(lat=42.377, lng=-71.065),
        dict(lat=None, lng=None),
    ]

    df = pd.DataFrame(rows)
    df.to_parquet(fspec)
    del(df)

    df2 = pd.read_parquet(fspec)
    print(df2)


if __name__ == '__main__':
    round_trip()

Une fois que vous avez récupéré les lignes dans un dataframe, vous êtes libre d'appeler df2.to_sql () ou utilisez une autre technique préférée pour mettre des nombres et des NULL dans une table de base de données.

EDIT:

Si vous pouvez exécuter .to_sql () sur le serveur PG, ou sur le même LAN, puis faites-le. Sinon, votre technique préférée impliquera probablement .copy_expert () . Pourquoi? Le résumé est qu'avec psycopg2, "L'INSERT en masse est lent". Les couches intermédiaires telles que sqlalchemy et pandas, ainsi que les applications bien écrites qui se soucient des performances d'insertion, utiliseront .executemany () . L'idée est d'envoyer de nombreuses lignes en même temps, sans attendre l'état du résultat individuel, car nous ne sommes pas préoccupés par les violations d'index uniques. Ainsi, TCP obtient un tampon géant de texte SQL et l'envoie en même temps, saturant la bande passante du canal de bout en bout, tout comme copy_expert envoie une grande mémoire tampon à TCP pour atteindre une bande passante élevée.

En revanche, le pilote psycopg2 ne prend pas en charge les exécutables hautes performances. À partir de 2.7.4, il exécute simplement les éléments un par un, en envoyant une commande SQL sur le WAN et en attendant un temps d'aller-retour pour le résultat avant d'envoyer la commande suivante. Ping votre serveur; si les temps de ping suggèrent que vous pouvez obtenir une douzaine d'allers-retours par seconde, alors prévoyez d'insérer seulement une douzaine de lignes par seconde. La plupart du temps est passé à attendre un paquet de réponse, plutôt qu'à traiter les lignes de base de données. Ce serait bien si, à une date ultérieure, psycopg2 offrait un meilleur support pour cela.


1 commentaires

Notez que DataFrame.to_sql () est vraiment très lent. Les insertions groupées dans PostgreSQL sont toujours mieux faites avec COPY FROM .