web-dev-qa-db-ja.com

ExcelおよびTextEditを開くときのUTF8 CSVファイルのエンコードの問題

最近、データベース(Postgres)からサーバー(Ruby on Rails)の配列からデータを取得し、クライアント側でCSVファイル(Javascript、HTML5)に変換するCSVダウンロードボタンを追加しました。現在、CSVファイルをテストしていますが、エンコードの問題に遭遇しています。

「less」でCSVファイルを表示すると、ファイルは正常に表示されます。しかし、Excelでファイルを開くと、OR TextEdit、

—、â€、“

テキストに表示されます。基本的に、ここで説明されている文字が表示されます。 http://digwp.com/2011/07/clean-up-weird-characters-in-database/

この種の問題は、データベースのエンコード設定が間違った設定に設定されている場合に発生する可能性があることを読みました。しかし、私が使用しているデータベースはUTF8エンコードを使用するように設定されています。また、CSVファイルを作成するJSコードを介してデバッグすると、テキストは正常に表示されます。 (これはChrome能力であり、より少ない能力である可能性があります)

私がオンライン検索から学んでいるのは、エンコードが機能しない多くの理由があり、どの部分に問題があるのか​​わからないということですので、イライラしています、そして私が試みたものは何も私の問題に新しい光を投げかけていません。

参考までに、CSVファイルを作成するJavaScriptスニペットを次に示します。

$(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);
});
23
Ji Mun

@jlarsonがMacが最大の犯人であるという情報で更新したので、もう少し詳しく説明するかもしれません。 Mac版Officeは、少なくとも2011年以降、ファイルのインポート時にUnicode形式を読み取るためのサポートがかなり不十分です。

UTF-8のサポートは、ほとんど存在しないように思われますが、UTF-8の動作に関するごく少数のコメントを読んでいますが、大多数はそうではないと言っています。残念ながら、テストするMacがありません。繰り返しになりますが、ファイル自体はUTF-8で問題ありませんが、インポートはプロセスを停止します。

BOMなどの有無にかかわらず、エスケープされたUTF-16のリトルエンディアンとビッグエンディアンの割合をエクスポートするためのJavascriptの簡単なテストを作成しました。

コードはおそらくリファクタリングされるべきですが、テストには問題ないはずです。 UTF-8よりもうまく機能する可能性があります。もちろん、これは通常、グリフが2バイトまたは4バイトであるため、より大きなデータ転送を意味します。

ここでフィドルを見つけることができます:

nicode export sample Fiddle

CSVは特定の方法でnot処理しないことに注意してください。これは主に、UTF-8、UTF-16ビッグ/リトルエンディアン、および+/- BOMを持つデータURLへの純粋な変換を目的としています。 fiddleにはコンマをタブに置き換えるオプションが1つありますが、それが機能する場合はかなりハック的で脆弱なソリューションになると考えられます。


通常は次のように使用します:

_// 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();
_

オブジェクトには2つの結果プロパティがあります:

1。)_encoder.lead_

これは、データURLのMIMEタイプ、文字セットなどです。初期化子に渡されたオプションから構築されます。または、.config({ ... new conf ...}).intro()と言って再構築することもできます。

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

base64を指定できますが、base64変換はありません(少なくともここまで)。

2。)_encoder.buf_

これは、エスケープされたデータの割合を示す文字列です。

.pay()関数は、単に1.)と2.)を1つとして返します。


メインコード:


_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;
    }
};
_

前の答え:


私はあなたのものを複製するためのセットアップを持っていませんが、あなたのケースが@jlarsonと同じ場合、結果のファイルは正しいはずです。

この答えはやや長くなりました、(あなたが言う楽しいトピック?)、しかし、質問に関するさまざまな側面、何が起こっている可能性があり、何が起こっているのかを実際に確認する方法について議論しますさまざまな方法で。

TL; DR:

テキストは、UTF-8ではなく、ISO-8859-1、Windows-1252などとしてインポートされる可能性があります。インポートまたはその他の方法を使用して、アプリケーションにファイルをUTF-8として強制的に読み取らせます。


PS:The UniSearcher はこの旅で利用できる素敵なツールです。

長い道のり

"easiest"100%を確認する方法は、結果に16進エディターを使用することです。または、コマンドラインからhexdumpxxdなどを使用してファイルを表示します。この場合、バイトシーケンスは、スクリプトから配信されるUTF-8のバイトシーケンスである必要があります。

例として、jlarsonのスクリプトを使用する場合、dataArrayを使用します。

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

これは文字列にマージされます:

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

unicodeによって次のように変換されます。

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

UTF-8はベースとしてASCIIを使用するため(最上位ビットnotが設定されたバイトはASCIIと同じ)テストで唯一の特別なシーケンスデータは「Ԁסกၔ」であり、次のとおりです。

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

ダウンロードしたファイルの16進ダンプを見る:

_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.
_

2行目で、上記と一致する_d480 d7a1 e0b8 81e1 8194_を見つけます。

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

他の文字もマングルされていません。

必要に応じて、同様のテストを実行します。結果は同様になるはずです。


サンプル提供__—, â€, “_

また、質問で提供されているサンプルを見ることができます。テキストは、コードページ1252によってExcel/TextEditで表されると仮定する可能性があります。

Windows-1252でウィキペディアを引用するには:

Windows-1252またはCP-1252はラテンアルファベットの文字エンコーディングであり、Microsoft Windowsのレガシーコンポーネントで英語および他の一部の西欧言語でデフォルトで使用されます。これは、Windowsコードページのグループ内の1つのバージョンです。 LaTeXパッケージでは、「ansinew」と呼ばれます。

元のバイトを取得する

元の形式に戻すために、 コードページレイアウト を見ることができます。

_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
_
  • UUnicodeの略です
  • TTranslatedの略です

例えば:

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

_9d_のような特殊なケースには、CP-1252に対応するコードポイントがありません。これらは直接コピーするだけです。

注:テキストをファイルにコピーして16進ダンプを実行することでマングル文字列を見る場合は、例えばUTF-16エンコードでファイルを保存して、表に示されているUnicode値を取得します。例えば。 Vimの場合:

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

バイトからUTF-8

次に、結果の_T.Hex_行をUTF-8に結合します。 UTF-8シーケンスでは、バイトは 後続バイトがグリフを作成する数を示す先頭バイト で表されます。たとえば、バイトにバイナリ値_110x xxxx_がある場合、このバイトと次のバイトが1つのコードポイントを表していることがわかります。合計2つ。 _1110 xxxx_は、それが3などであることを示しています。 ASCII値は上位ビットが設定されていないため、_0xxx xxxx_に一致するバイトはスタンドアロンです。合計1バイトです。

0xe2 = 1110 0010置き場 => 3バイト=> 0xe28094(em-dash)— 
 0x2c = 0010 1100置き場 => 1バイト=> 0x2c(コンマ)、
 0x2c = 0010 0000置き場 => 1バイト=> 0x20(スペース)
 0xe2 = 1110 0010置き場 => 3バイト=> 0xe2809d(right-dq)” 
 0x2c = 0010 1100置き場 => 1バイト=> 0x2c(コンマ)、
 0x2c = 0010 0000置き場 => 1バイト=> 0x20(スペース)
 0xe2 = 1110 0010置き場 => 3バイト=> 0xe2809c(left-dq)“ 

結論; 元のUTF-8文字列は:

_—, ”, “
_

マングルバック

逆もできます。バイトとしての元の文字列:

_UTF-8: e2 80 94 2c 20 e2 80 9d 2c 20 e2 80 9c
_

cp-1252 の対応する値:

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

など、結果:

_—, â€, “
_

MS Excelへのインポート

言い換えると、当面の問題は、UTF-8テキストファイルをMS Excelおよびその他のアプリケーションにインポートする方法である可能性があります。 Excelでは、これはさまざまな方法で実行できます。

  • 方法1:

_.csv_や_.txt_など、アプリケーションで認識される拡張子でファイルを保存しないでください。ただし、完全に省略するか、何かを構成してください。

例として、拡張子なしで_"testfile"_としてファイルを保存します。次に、Excelでファイルを開き、実際にこのファイルを開くことを確認します。voilàエンコードオプションが提供されます。 UTF-8を選択すると、ファイルが正しく読み込まれます。

  • 方法2:

開いているファイルの代わりにインポートデータを使用します。何かのようなもの:

_Data -> Import External Data -> Import Data
_

エンコードを選択して続行します。

Excelと選択したフォントが実際にグリフをサポートしていることを確認してください

また、使いやすいクリップボードを使用して、Unicode文字のフォントサポートをテストすることもできます。たとえば、このページのテキストをExcelにコピーします。

コードポイントのサポートが存在する場合、テキストは正常にレンダリングされるはずです。


Linux

ユーザーランドでは主にUTF-8であるLinuxでは、これは問題になりません。 Libre Office Calc、Vimなどを使用すると、ファイルが正しくレンダリングされます。


なぜ機能する(またはする必要がある)

encodeURI 仕様の状態から( sec-15.1. も読みます):

EncodeURI関数は、特定の文字の各インスタンスが、文字のUTF-8エンコードを表す1、2、3、または4つのエスケープシーケンスで置き換えられたURIの新しいバージョンを計算します。

コンソールでこれをテストするには、たとえば次のようにします。

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

登録すると、エスケープシーケンスは上記の16進ダンプのエスケープシーケンスと同じになります。

_%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)
_

または、4バイトコードのテスト:

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

これが遵守されない場合

このいずれにも当てはまらない場合は、追加した場合に役立ちます

  1. 予想される入力とマングルされた出力のサンプル(コピーペースト)。
  2. 元のデータと結果ファイルのサンプル16進ダンプ。
37
user13500

昨日、まさにこれに遭遇しました。 HTMLテーブルの内容をCSVダウンロードとしてエクスポートするボタンを開発していました。ボタン自体の機能はほぼ同じです。クリックすると、テーブルからテキストを読み取り、CSVコンテンツでデータURIを作成します。

結果のファイルをExcelで開こうとすると、 "£"シンボルが正しく読み取られないことが明らかになりました。 2バイトのUTF-8表現はASCIIとして処理され、不要な文字化けが発生しました。一部のグーグルは、これがExcelの既知の問題であることを示しました。

文字列の先頭にバイトオーダーマークを追加しようとしました。ExcelはそれをASCIIデータとして解釈しました。その後、UTF-8文字列をASCII(csvData.replace('\u00a3', '\xa3')など)が、データがJavaScript文字列に強制変換されると、UTF-8になることがわかった。これをバイナリに変換してからBase64エンコードする途中で文字列に戻すことなく。

私のアプリにはすでに CryptoJS があり(REST APIに対するHMAC認証に使用)、それを使用してASCII元の文字列からエンコードされたバイトシーケンスは、Base64でエンコードされ、データURIを作成しますこれは機能し、Excelで開かれた結果のファイルには不要な文字が表示されません。

変換を行う重要なコードは次のとおりです。

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はCSV文字列です。

そのライブラリを持ち込みたくない場合、CryptoJSなしで同じことを行う方法はおそらくありますが、少なくともこれが可能であることを示しています。

5
Rob Fletcher

Excelは、BOM付きUTF-16 LEエンコーディングのUnicodeが好きです。正しい出力 [〜#〜] bom [〜#〜]FF FE)、すべてのデータをUTF-8からUTF-16 LEに変換します。

Windowsは内部でUTF-16 LEを使用するため、一部のアプリケーションはUTF-8よりもUTF-16でより適切に動作します。

私はJSでそれをやろうとしませんでしたが、UTF-8をUTF-16に変換するためのさまざまなスクリプトがウェブ上にあります。 UTFバリエーション間の変換は非常に簡単で、数十行しかかかりません。

3
Athari

SharepointリストからJavascriptに取り込まれたデータで同様の問題が発生していました。 "Zero Width Space" 文字と呼ばれるものであることが判明し、Excelに取り込まれたときにâ€として表示されていました。どうやら、ユーザーが「バックスペース」を押すと、Sharepointがこれらを挿入することがあります。

これらをこのクイックフィックスに置き換えました。

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

他の隠されたキャラクターがそこにいるようです。私は、Chromeインスペクターで出力文字列を見て、ゼロ幅文字のコードポイントを見つけました。インスペクターは文字をレンダリングできなかったため、赤い点で置き換えました。マウスをその赤い点の上に置くと、コードポイント(例:\ u200B)が表示され、さまざまなコードポイントで非表示の文字をサブスクライブして、それらを削除できます。

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

これはトリックを行う必要があります

0
Alon Kogan

サーバーのエンコードに問題がある可能性があります。

Linuxを実行している場合は、試してみることができます(ロケールは英語(US)を想定しています)。

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