web-dev-qa-db-fra.com

Problèmes d'encodage du fichier CSV UTF8 lors de l'ouverture d'Excel et de TextEdit

J'ai récemment ajouté un bouton de téléchargement CSV qui prend les données de la base de données (Postgres) d'un tableau du serveur (Ruby on Rails) et les convertit en un fichier CSV côté client (Javascript, HTML5). Je teste actuellement le fichier CSV et je rencontre des problèmes d'encodage. 

Lorsque je visualise le fichier CSV via "less", le fichier semble très bien. Mais lorsque j'ouvre le fichier dans Excel OR TextEdit, je commence à voir des caractères étranges comme 

â € ”, â €“

apparaissent dans le texte. En gros, je vois les caractères décrits ici: http://digwp.com/2011/07/clean-up-weird-characters-in-database/

J'ai lu que ce type de problème peut survenir lorsque le paramètre de codage de la base de données est défini sur le mauvais paramètre. MAIS, la base de données que j'utilise est configurée pour utiliser le codage UTF8. Et lorsque je débogue par le biais des codes JS qui créent le fichier CSV, le texte semble normal. (Cela pourrait être une capacité de Chrome, et moins de capacité) 

Je me sens frustré parce que la seule chose que j'apprends de ma recherche en ligne est qu'il peut y avoir plusieurs raisons pour lesquelles l'encodage ne fonctionne pas. Je ne sais pas quelle partie est en faute (excusez-moi, je commence par baliser de nombreuses choses) et rien de ce que j'ai essayé n'a jeté un nouvel éclairage sur mon problème.

Pour référence, voici l'extrait de code JavaScript qui crée le fichier CSV! 

$(document).ready(function() {
var csvData = <%= raw to_csv(@view_scope, clicks_post).as_json %>;
var csvContent = "data:text/csv;charset=utf-8,";
csvData.forEach(function(infoArray, index){
  var dataString = infoArray.join(",");
  csvContent += dataString+ "\n";
}); 
var encodedUri = encodeURI(csvContent);
var button = $('<a>');
button.text('Download CSV');
button.addClass("button right");
button.attr('href', encodedUri);
button.attr('target','_blank');
button.attr('download','<%=title%>_25_posts.csv');
$("#<%=title%>_download_action").append(button);
});
22
Ji Mun

Comme @jlarson a mis à jour avec l'information que Mac était le plus grand coupable, nous pourrions en avoir d'autres. Office pour Mac a, au moins 2011 et au moins, une prise en charge plutôt médiocre de la lecture de formats Unicode lors de l'importation de fichiers.

Le support pour UTF-8 semble être presque inexistant, ont lu quelques commentaires à ce sujet, mais la majorité dit que ce n’est pas le cas. Malheureusement, je n'ai pas de Mac à tester. Encore une fois: les fichiers eux-mêmes devraient être OK comme UTF-8, mais l'importation interrompt le processus.

Nous avons rédigé un test rapide en Javascript pour exporter le pourcentage d'échappées UTF-16 petit et grand endian, avec/sans nomenclature, etc.

Le code devrait probablement être refactorisé mais devrait pouvoir être testé. Cela pourrait mieux fonctionner que UTF-8. Bien entendu, cela signifie également que les transferts de données sont plus importants, car tout glyphe est composé de deux ou quatre octets.

Vous pouvez trouver un violon ici:

nicode export sample Fiddle

Notez que cela n'est pas gérer CSV de manière particulière. Il est principalement destiné à la conversion pure en URL de données ayant UTF-8, UTF-16 big/little endian et +/- BOM. Il existe une option dans le violon qui permet de remplacer les virgules par des tabulations, mais croyez que ce serait une solution plutôt piratée et fragile si cela fonctionnait.


À utiliser typiquement comme:

// Initiate
encoder = new DataEnc({
    mime   : 'text/csv',
    charset: 'UTF-16BE',
    bom    : true
});

// Convert data to percent escaped text
encoder.enc(data);

// Get result
var result = encoder.pay();

Il existe deux propriétés de résultat de l'objet:

1.) encoder.lead

Il s'agit du type mime, du jeu de caractères, etc. pour l'URL de données. Construit à partir des options passées à l'initialiseur, ou on peut aussi dire .config({ ... new conf ...}).intro() pour reconstruire.

data:[<MIME-type>][;charset=<encoding>][;base64]

Vous pouvez spécifier base64 , mais il n'y a pas de conversion base64 (du moins, pas si loin).

2.) encoder.buf

C'est une chaîne avec le pourcentage de données échappées.

La fonction .pay() renvoie simplement 1.) et 2.) comme un.


Code principal:


function DataEnc(a) {
    this.config(a);
    this.intro();
}
/*
* http://www.iana.org/assignments/character-sets/character-sets.xhtml
* */
DataEnc._enctype = {
        u8    : ['u8', 'utf8'],
        // RFC-2781, Big endian should be presumed if none given
        u16be : ['u16', 'u16be', 'utf16', 'utf16be', 'ucs2', 'ucs2be'],
        u16le : ['u16le', 'utf16le', 'ucs2le']
};
DataEnc._BOM = {
        'none'     : '',
        'UTF-8'    : '%ef%bb%bf', // Discouraged
        'UTF-16BE' : '%fe%ff',
        'UTF-16LE' : '%ff%fe'
};
DataEnc.prototype = {
    // Basic setup
    config : function(a) {
        var opt = {
            charset: 'u8',
            mime   : 'text/csv',
            base64 : 0,
            bom    : 0
        };
        a = a || {};
        this.charset = typeof a.charset !== 'undefined' ?
                        a.charset : opt.charset;
        this.base64 = typeof a.base64 !== 'undefined' ? a.base64 : opt.base64;
        this.mime = typeof a.mime !== 'undefined' ? a.mime : opt.mime;
        this.bom = typeof a.bom !== 'undefined' ? a.bom : opt.bom;

        this.enc = this.utf8;
        this.buf = '';
        this.lead = '';
        return this;
    },
    // Create lead based on config
    // data:[<MIME-type>][;charset=<encoding>][;base64],<data>
    intro : function() {
        var
            g = [],
            c = this.charset || '',
            b = 'none'
        ;
        if (this.mime && this.mime !== '')
            g.Push(this.mime);
        if (c !== '') {
            c = c.replace(/[-\s]/g, '').toLowerCase();
            if (DataEnc._enctype.u8.indexOf(c) > -1) {
                c = 'UTF-8';
                if (this.bom)
                    b = c;
                this.enc = this.utf8;
            } else if (DataEnc._enctype.u16be.indexOf(c) > -1) {
                c = 'UTF-16BE';
                if (this.bom)
                    b = c;
                this.enc = this.utf16be;
            } else if (DataEnc._enctype.u16le.indexOf(c) > -1) {
                c = 'UTF-16LE';
                if (this.bom)
                    b = c;
                this.enc = this.utf16le;
            } else {
                if (c === 'copy')
                    c = '';
                this.enc = this.copy;
            }
        }
        if (c !== '')
            g.Push('charset=' + c);
        if (this.base64)
            g.Push('base64');
        this.lead = 'data:' + g.join(';') + ',' + DataEnc._BOM[b];
        return this;
    },
    // Deliver
    pay : function() {
        return this.lead + this.buf;
    },
    // UTF-16BE
    utf16be : function(t) { // U+0500 => %05%00
        var i, c, buf = [];
        for (i = 0; i < t.length; ++i) {
            if ((c = t.charCodeAt(i)) > 0xff) {
                buf.Push(('00' + (c >> 0x08).toString(16)).substr(-2));
                buf.Push(('00' + (c  & 0xff).toString(16)).substr(-2));
            } else {
                buf.Push('00');
                buf.Push(('00' + (c  & 0xff).toString(16)).substr(-2));
            }
        }
        this.buf += '%' + buf.join('%');
        // Note the hex array is returned, not string with '%'
        // Might be useful if one want to loop over the data.
        return buf;
    },
    // UTF-16LE
    utf16le : function(t) { // U+0500 => %00%05
        var i, c, buf = [];
        for (i = 0; i < t.length; ++i) {
            if ((c = t.charCodeAt(i)) > 0xff) {
                buf.Push(('00' + (c  & 0xff).toString(16)).substr(-2));
                buf.Push(('00' + (c >> 0x08).toString(16)).substr(-2));
            } else {
                buf.Push(('00' + (c  & 0xff).toString(16)).substr(-2));
                buf.Push('00');
            }
        }
        this.buf += '%' + buf.join('%');
        // Note the hex array is returned, not string with '%'
        // Might be useful if one want to loop over the data.
        return buf;
    },
    // UTF-8
    utf8 : function(t) {
        this.buf += encodeURIComponent(t);
        return this;
    },
    // Direct copy
    copy : function(t) {
        this.buf += t;
        return this;
    }
};

Réponse précédente:


Je n'ai pas de configuration pour répliquer le vôtre, mais si votre cas est identique à @jlarson, le fichier résultant devrait être correct.

Cette réponse est devenue un peu longue (sujet amusant, dites-vous?) , mais abordez divers aspects de la question, que se passe-t-il (probablement) et comment vérifier réellement ce qui se passe de diverses façons.

TL; DR:

Le texte est probablement importé au format ISO-8859-1, Windows-1252 ou similaire, et non au format UTF-8. Forcer l'application à lire le fichier au format UTF-8 à l'aide de l'importation ou par un autre moyen.


PS: The UniSearcher est un outil de Nice à utiliser pour ce voyage.

Le long chemin autour

Le moyen "le plus simple" pour être sûr à 100% est d'utiliser un éditeur hexadécimal pour le résultat. Vous pouvez également utiliser hexdumpname__, xxdou similaire à partir de la ligne de commande pour afficher le fichier. Dans ce cas, la séquence d'octets doit être celle de UTF-8 telle que fournie par le script.

Comme exemple si nous prenons le script de jlarson , il prend le data Array :

data = ['name', 'city', 'state'],
       ['\u0500\u05E1\u0E01\u1054', 'seattle', 'washington']

Celui-ci est fusionné dans la chaîne:

 name,city,state<newline>
 \u0500\u05E1\u0E01\u1054,seattle,washington<newline>

qui se traduit par Unicode en:

 name,city,state<newline>
 Ԁסกၔ,seattle,washington<newline>

Comme UTF-8 utilise ASCII comme base (les octets avec le bit le plus fort et non sont identiques à ceux en ASCII), la seule séquence spéciale dans les données de test est " ס ก ၔ "qui à son tour est:

Code-point  Glyph      UTF-8
----------------------------
    U+0500    Ԁ        d4 80
    U+05E1    ס        d7 a1
    U+0E01    ก     e0 b8 81
    U+1054    ၔ     e1 81 94

En regardant le vidage hexadécimal du fichier téléchargé:

0000000: 6e61 6d65 2c63 6974 792c 7374 6174 650a  name,city,state.
0000010: d480 d7a1 e0b8 81e1 8194 2c73 6561 7474  ..........,seatt
0000020: 6c65 2c77 6173 6869 6e67 746f 6e0a       le,washington.

Sur la deuxième ligne, nous trouvons d480 d7a1 e0b8 81e1 8194 qui correspond à ce qui précède:

0000010: d480  d7a1  e0b8 81  e1 8194 2c73 6561 7474  ..........,seatt
         |   | |   | |     |  |     |  | |  | |  | |
         +-+-+ +-+-+ +--+--+  +--+--+  | |  | |  | |
           |     |      |        |     | |  | |  | |
           Ԁ     ס      ก        ၔ     , s  e a  t t

Aucun des autres personnages n'est mutilé non plus.

Faites des tests similaires si vous voulez. Le résultat devrait être similaire.


Par exemple fourni —, â€, “

Nous pouvons également consulter l'échantillon fourni dans la question. Il est probable que le texte est représenté dans Excel/TextEdit par la page de codes 1252.

Pour citer Wikipedia sur Windows-1252:

Windows-1252 ou CP-1252 est un codage de caractères de l'alphabet latin, utilisé par défaut dans les composants hérités de Microsoft Windows en anglais et dans certaines autres langues occidentales. Il s’agit d’une version du groupe de pages de code Windows. Dans les paquets LaTeX, cela s'appelle "ansinew".

Récupérer les octets d'origine

Pour le traduire dans sa forme originale, nous pouvons regarder le mise en page de code , d'où nous obtenons:

Character:   <â>  <€>  <”>  <,>  < >  <â>  <€>  < >  <,>  < >  <â>  <€>  <œ>
U.Hex    :    e2 20ac 201d   2c   20   e2 20ac   9d   2c   20   e2 20ac  153
T.Hex    :    e2   80   94   2c   20   e2   80   9d*  2c   20   e2   80   9c
  • Uest l'abréviation de Unicode
  • Test l'abréviation de traduit

Par exemple:

â => Unicode 0xe2   => CP-1252 0xe2
” => Unicode 0x201d => CP-1252 0x94
€ => Unicode 0x20ac => CP-1252 0x80

Des cas spéciaux comme 9d n'ont pas de point de code correspondant dans le CP-1252, nous les copions simplement.

Remarque: Si vous regardez une chaîne mutilée en copiant le texte dans un fichier et en effectuant un vidage hexadécimal, enregistrez-le avec par exemple le codage UTF-16 pour obtenir les valeurs Unicode représentées dans le tableau. Par exemple. à Vim:

set fenc=utf-16
# Or
set fenc=ucs-2

Octets en UTF-8

Nous combinons ensuite le résultat, la ligne T.Hex, en UTF-8. Dans les séquences UTF-8, les octets sont représentés par un octet de tête nous indiquant le nombre d’octets suivants constituant le glyphe . Par exemple, si un octet a la valeur binaire 110x xxxx, nous savons que cet octet et le suivant représentent un point de code. Un total de deux. 1110 xxxx nous dit qu'il est trois et ainsi de suite. ASCII le bit de poids fort n'est pas défini pour les valeurs, de sorte que tout octet correspondant à 0xxx xxxx est autonome. Un total d'un octet.

0xe2 = 1110 0010poubelle => 3 octets => 0xe28094 (tiret) - 
 0x2c = 0010 1100poubelle => 1 octet => 0x2c (virgule), 
 0x2c = 0010 0000poubelle => 1 octet => 0x20 (espace) 
 0xe2 = 1110 0010poubelle => 3 octets => 0xe2809d (right-dq) ”
 0x2c = 0010 1100poubelle => 1 octet => 0x2c (virgule), 
 0x2c = 0010 0000poubelle => 1 octet => 0x20 (espace) 
 0xe2 = 1110 0010poubelle => 3 octets => 0xe2809c (left-dq) “

Conclusion; La chaîne UTF-8 d'origine était:

—, ”, “

Le ramener

Nous pouvons aussi faire l'inverse. La chaîne d'origine en octets:

UTF-8: e2 80 94 2c 20 e2 80 9d 2c 20 e2 80 9c

Valeurs correspondantes dans cp-1252 :

e2 => â
80 => €
94 => ”
2c => ,
20 => <space>
...

et ainsi de suite, résultat:

—, â€, “

Importation vers MS Excel

En d'autres termes: le problème en cause pourrait être de savoir comment importer des fichiers texte UTF-8 dans MS Excel et dans d'autres applications. Dans Excel, cela peut être fait de différentes manières.

  • Première méthode:

Ne sauvegardez pas le fichier avec une extension reconnue par l'application, telle que .csv ou .txt, mais omettez-le complètement ou créez quelque chose.

Par exemple, enregistrez le fichier sous le nom "testfile", sans extension. Ensuite, dans Excel, ouvrez le fichier, confirmez que nous voulons réellement ouvrir ce fichier et voilà , nous recevons l’option d’encodage. Sélectionnez UTF-8 et le fichier doit être lu correctement.

  • Méthode deux:

Utilisez les données d'importation au lieu du fichier ouvert. Quelque chose comme:

Data -> Import External Data -> Import Data

Sélectionnez le codage et continuez.

Vérifiez que Excel et la police sélectionnée prennent en charge le glyphe

Nous pouvons également tester la prise en charge des polices pour les caractères Unicode en utilisant le presse-papiers parfois plus convivial. Par exemple, copiez le texte de cette page dans Excel:

S'il existe un support pour les points de code, le texte devrait être bien rendu.


Linux

Sous Linux, qui est principalement UTF-8 dans le monde utilisateur, cela ne devrait pas poser de problème. Utiliser Libre Office Calc, Vim, etc. affiche les fichiers correctement restitués.


Pourquoi ça marche (ou devrait)

encodeURI parmi les états de spécification, (également lu sec-15.1. ):

La fonction encodeURI calcule une nouvelle version d'un URI dans laquelle chaque instance de certains caractères est remplacée par une, deux, trois ou quatre séquences d'échappement représentant le codage UTF-8 du caractère.

Nous pouvons simplement tester cela dans notre console en disant par exemple:

>> encodeURI('Ԁסกၔ,seattle,washington')
<< "%D4%80%D7%A1%E0%B8%81%E1%81%94,seattle,washington"

Au fur et à mesure que nous enregistrons, les séquences d'échappement sont égales à celles du vidage hexadécimal ci-dessus:

%D4%80%D7%A1%E0%B8%81%E1%81%94 (encodeURI in log)
 d4 80 d7 a1 e0 b8 81 e1 81 94 (hex-dump of file)

ou, tester un code de 4 octets:

>> encodeURI('????')
<< "%F3%B1%80%81"

Si ce n'est pas conforme

Si rien de tout cela ne s'applique, cela pourrait aider si vous ajoutiez

  1. Échantillon de l'entrée attendue par rapport à la sortie réduite (copier/coller).
  2. Exemple de vidage hexadécimal des données d'origine par rapport au fichier de résultat.
36
user13500

J'ai rencontré exactement cela hier. Je développais un bouton qui exporte le contenu d'un tableau HTML sous forme de téléchargement au format CSV. La fonctionnalité du bouton lui-même est presque identique à la vôtre. En cliquant, je lis le texte du tableau et crée un URI de données avec le contenu CSV.

Lorsque j'ai essayé d'ouvrir le fichier résultant dans Excel, il était clair que le symbole "£" était mal lu. La représentation UTF-8 sur 2 octets était en cours de traitement en tant que ASCII, générant ainsi un caractère indésirable indésirable. Certains utilisateurs ont indiqué qu'il s'agissait d'un problème connu d'Excel.

J'ai essayé d'ajouter la marque d'ordre des octets au début de la chaîne - Excel l'a simplement interprétée comme des données ASCII. J'ai ensuite essayé diverses choses pour convertir la chaîne UTF-8 en ASCII (telle que csvData.replace('\u00a3', '\xa3')), mais j'ai constaté que chaque fois que les données sont forcées sur une chaîne JavaScript, elles redeviendront UTF-8. L'astuce consiste à le convertir en binaire, puis en Base64, sans le reconvertir en chaîne le long du chemin.

J'avais déjà CryptoJS dans mon application (utilisé pour l'authentification HMAC par rapport à une API REST) et j'ai pu l'utiliser pour créer une séquence d'octets ASCII à partir de la chaîne d'origine, puis encoder en Base64 et créez un URI de données. Cela a fonctionné et le fichier résultant lorsqu'il est ouvert dans Excel n'affiche aucun caractère indésirable.

Le bit essentiel de code qui effectue la conversion est:

var csvHeader = 'data:text/csv;charset=iso-8859-1;base64,'
var encodedCsv =  CryptoJS.enc.Latin1.parse(csvData).toString(CryptoJS.enc.Base64)
var dataURI = csvHeader + encodedCsv

csvData est votre chaîne CSV.

Il y a probablement moyen de faire la même chose sans CryptoJS si vous ne voulez pas importer cette bibliothèque, mais cela montre au moins que c'est possible.

5
Rob Fletcher

Excel aime Unicode dans UTF-16 LE avec le codage BOM. Générez le bon BOM (FF FE), puis convertissez toutes vos données de UTF-8 à UTF-16 LE.

Windows utilise UTF-16 LE en interne, de sorte que certaines applications fonctionnent mieux avec UTF-16 qu'avec UTF-8.

Je n'ai pas essayé de faire cela dans JS, mais il existe différents scripts sur le Web pour convertir UTF-8 en UTF-16. La conversion entre les variations UTF est assez facile et prend seulement une douzaine de lignes.

3
Athari

J'avais un problème similaire avec les données qui ont été extraites en Javascript à partir d'une liste Sharepoint. Il s’est avéré qu’il s’agissait d’un caractère appelé "Caractère Zéro Largeur" ​​ et il s’affichait comme «lorsqu’il a été importé dans Excel. Apparemment, Sharepoint les insère parfois lorsqu'un utilisateur appuie sur "backspace".

Je les ai remplacés par ce correctif:

var mystring = myString.replace(/\u200B/g,'');

Il semble que vous ayez peut-être d'autres personnages cachés. J'ai trouvé le point de code pour le caractère de largeur nulle dans le mien en examinant la chaîne de sortie dans l'inspecteur Chrome. L'inspecteur n'a pas pu restituer le caractère, il l'a donc remplacé par un point rouge. Lorsque vous passez la souris sur ce point rouge, il vous donne le point de code (par exemple,\u200B) et vous pouvez simplement remplacer les différents points de code par les caractères invisibles et les supprimer ainsi.

2
Josh Abrams
button.href = 'data:' + mimeType + ';charset=UTF-8,%ef%bb%bf' + encodedUri;

cela devrait faire l'affaire

0
Alon Kogan

Cela pourrait être un problème dans l'encodage de votre serveur.

Vous pouvez essayer (en supposant que la langue anglaise est en anglais) si vous utilisez Linux:

Sudo locale-gen en_US en_US.UTF-8
dpkg-reconfigure locales
0
goten