web-dev-qa-db-fra.com

Écrire les résultats d'une requête SQL en CSV et éviter les sauts de ligne supplémentaires

Je dois extraire des données de plusieurs moteurs de base de données différents. Une fois ces données exportées, j'envoie les données à AWS S3 et les copie vers Redshift à l'aide d'une commande COPY. Certaines tables contiennent beaucoup de texte, avec des sauts de ligne et d'autres caractères présents dans les champs de la colonne. Quand je lance le code suivant:

cursor.execute('''SELECT * FROM some_schema.some_message_log''')
rows = cursor.fetchall()
with open('data.csv', 'w', newline='') as fp:
    a = csv.writer(fp, delimiter='|', quoting=csv.QUOTE_ALL, quotechar='"', doublequote=True, lineterminator='\n')
    a.writerows(rows)

Certaines des colonnes ayant des retours à la ligne/des sauts de ligne créeront de nouvelles lignes:

"2017-01-05 17:06:32.802700"|"SampleJob"|""|"Date"|"error"|"Job.py"|"syntax error at or near ""from"" LINE 34: select *, SYSDATE, from staging_tops.tkabsences;
                                      ^
-<class 'psycopg2.ProgrammingError'>"

ce qui entraîne l'échec du processus d'importation. Je peux contourner ce problème en codant en dur pour les exceptions:

cursor.execute('''SELECT * FROM some_schema.some_message_log''')
rows = cursor.fetchall()
with open('data.csv', 'w', newline='') as fp:
    a = csv.writer(fp, delimiter='|', quoting=csv.QUOTE_ALL, quotechar='"', doublequote=True, lineterminator='\n')

for row in rows:
    list_of_rows = []
    for c in row:
        if isinstance(c, str):
            c = c.replace("\n", "\\n")
            c = c.replace("|", "\|")
            c = c.replace("\\", "\\\\")
            list_of_rows.append(c)
        else:
            list_of_rows.append(c)
    a.writerow([x.encode('utf-8') if isinstance(x, str) else x for x in list_of_rows])

Mais cela prend beaucoup de temps pour traiter des fichiers plus volumineux et semble être une mauvaise pratique en général. Existe-t-il un moyen plus rapide d’exporter des données d’un curseur SQL vers un fichier CSV qui ne se cassera pas face aux colonnes de texte contenant des retours à la ligne/des sauts de ligne?

8
user2752159

Je suppose que le problème est aussi simple que de vérifier que la bibliothèque d’exportation Python CSV et l’importation COPY de Redshift parlent d’une interface commune. En bref, vérifiez vos délimiteurs et vos guillemets et assurez-vous que la sortie Python et la commande Redshift COPY concordent.

Avec un peu plus de détails: les pilotes de base de données auront déjà fait le travail difficile d’obtenir Python sous une forme bien comprise. En d'autres termes, chaque ligne de la base de données est une liste (ou un nuplet, un générateur, etc.) et chaque cellule est accessible individuellement. Et au moment où vous avez une structure semblable à une liste, l'exportateur CSV de Python peut effectuer le reste du travail et, surtout, Redshift pourra copier à partir de la sortie, des nouvelles lignes intégrées, etc. En particulier, vous ne devriez pas avoir besoin de vous échapper manuellement; les fonctions .writerow() ou .writerows() devraient suffire.

L’implémentation COPY de Redshift comprend le dialecte le plus courant du CSV par défaut, à savoir:

  • délimiter les cellules par une virgule (,),
  • guillemets entre guillemets ("),
  • et pour éviter les guillemets doubles imbriqués (""").

Pour sauvegarder cela avec la documentation de Redshift FORMAT AS CSV :

... Le caractère de citation par défaut est un guillemet double ("). Lorsque le caractère de citation est utilisé dans un champ, remplacez-le par un caractère de citation supplémentaire. ...

Toutefois, votre code d'exportation Python CSV utilise un canal (|) en tant que delimiter et définit le quotechar sur double guillemet ("). Cela aussi peut fonctionner, mais pourquoi s’écarter de les valeurs par défaut ? Suggérez d'utiliser l'homonyme de CSV et de simplifier votre code:

cursor.execute('''SELECT * FROM some_schema.some_message_log''')
rows = cursor.fetchall()
with open('data.csv', 'w') as fp:
    csvw = csv.writer( fp )
    csvw.writerows(rows)

À partir de là, indiquez à COPY d'utiliser le format CSV (là encore, sans nécessiter de spécifications autres que celles par défaut):

COPY  your_table  FROM  your_csv_file  auth_code  FORMAT AS CSV;

Ça devrait le faire.

2
hunteke

Si vous utilisez SELECT * FROM table sans clause WHERE, vous pouvez utiliser COPY table TO STDOUT à la place, avec les bonnes options:

copy_command = """COPY some_schema.some_message_log TO STDOUT
        CSV QUOTE '"' DELIMITER '|' FORCE QUOTE *"""

with open('data.csv', 'w', newline='') as fp:
    cursor.copy_expert(copy_command)

Ceci, lors de mes tests, a pour résultat un\n littéral 'au lieu de nouvelles lignes, où l'écriture dans l'écrivain CSV donne des lignes brisées.

Si vous avez besoin d'une clause WHERE en production, vous pouvez créer une table temporaire et la copier:

cursor.execute("""CREATE TEMPORARY TABLE copy_me AS
        SELECT this, that, the_other FROM table_name WHERE conditions""")

(edit) En regardant à nouveau votre question, je vois que vous mentionnez "tous les moteurs de base de données différents". Ce qui précède fonctionne avec psyopg2 et postgresql mais pourrait probablement être adapté pour d’autres bases de données ou bibliothèques.

3
Nathan Vērzemnieks

Pourquoi écrire dans la base de données après chaque ligne?

cursor.execute('''SELECT * FROM some_schema.some_message_log''')
rows = cursor.fetchall()
with open('data.csv', 'w', newline='') as fp:
    a = csv.writer(fp, delimiter='|', quoting=csv.QUOTE_ALL, quotechar='"', doublequote=True, lineterminator='\n')

list_of_rows = []
for row in rows:
    for c in row:
        if isinstance(c, basestring):
            c = c.replace("\n", "\\n")
            c = c.replace("|", "\|")
            c = c.replace("\\", "\\\\")
    list_of_rows.append(row)
a.writerows([x.encode('utf-8') if isinstance(x, str) else x for x in list_of_rows])
0
Batman

Le problème est que vous utilisez la commande Redshift COPY avec ses paramètres par défaut, qui utilisent un tuyau comme délimiteur (voir ici et ici ) et requièrent l'échappement des nouvelles lignes et des tuyaux dans le texte. champs (voir ici et ici ). Cependant, l’écrivain csv de Python ne sait faire que ce qui est standard avec les nouvelles lignes incorporées, c’est-à-dire de les laisser telles quelles, dans une chaîne entre guillemets.

Heureusement, la commande Redshift COPY peut également utiliser le format CSV standard. Ajout de l'option CSV à votre commande COPYvous donne ce comportement :

Permet l'utilisation du format CSV dans les données d'entrée. Pour échapper automatiquement aux délimiteurs, aux caractères de nouvelle ligne et aux retours à la ligne, insérez le champ dans le caractère spécifié par le paramètre QUOTE. Le caractère de citation par défaut est un guillemet double ("). Lorsque le caractère de citation est utilisé dans un champ, remplacez-le par un caractère de citation supplémentaire."

C'est exactement l'approche utilisée par le rédacteur Python CSV, elle devrait donc prendre en charge vos problèmes. Donc, mon conseil serait de créer un fichier csv standard en utilisant un code comme celui-ci:

cursor.execute('''SELECT * FROM some_schema.some_message_log''')
rows = cursor.fetchall()
with open('data.csv', 'w', newline='') as fp:
    a = csv.writer(fp)  # no need for special settings
    a.writerows(rows)

Ensuite, dans Redshift, modifiez votre commande COPY en quelque chose comme ceci (notez la balise CSV ajoutée):

COPY logdata
FROM 's3://mybucket/data/data.csv' 
iam_role 'arn:aws:iam::0123456789012:role/MyRedshiftRole' 
CSV;

Vous pouvez également continuer à convertir manuellement vos champs pour qu'ils correspondent aux paramètres par défaut de la commande COPY de Redshift. Le csv.writer de Python ne le fera pas pour vous tout seul, mais vous pourrez peut-être accélérer un peu votre code, en particulier pour les gros fichiers, comme ceci:

cursor.execute('''SELECT * FROM some_schema.some_message_log''')
rows = cursor.fetchall()
with open('data.csv', 'w', newline='') as fp:
    a = csv.writer(
        fp, 
        delimiter='|', quoting=csv.QUOTE_ALL, 
        quotechar='"', doublequote=True, lineterminator='\n'
    )
    a.writerows(
        c.replace("\\", "\\\\").replace("\n", "\\\n").replace("|", "\\|").encode('utf-8')
        if isinstance(c, str)
        else c
        for row in rows
        for c in row
    )

Une autre solution consiste à importer les données de la requête dans un pandas DataFrame avec .from_sql, en effectuant les remplacements dans le DataFrame (une colonne entière à la fois), puis en écrivant la table avec .to_csv. Les pandas ont un code csv incroyablement rapide, ce qui peut vous donner une accélération significative.

Mise à jour: Je viens de remarquer que, finalement, j'ai dupliqué la réponse de @ hunteke. Le point clé (que j'ai manqué la première fois) est que vous n'avez probablement pas utilisé l'argument CSV dans votre commande Redshift COPY actuelle; si vous ajoutez cela, cela devrait devenir facile. 

0
Matthias Fripp