打开Excel和TextEdit时UTF8 CSV文件的编码问题

gorkyyrv  于 12个月前  发布在  其他
关注(0)|答案(7)|浏览(95)

我最近添加了一个CSV下载按钮,它从数据库(Postgres)中获取数据,从服务器(Ruby on Rails)中获取数组,并将其转换为客户端(JavaScript,HTML5)的CSV文件。我目前正在测试CSV文件,我遇到了一些编码问题。
当我通过'less'查看CSV文件时,文件看起来很好。但是当我在Excel或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);
});

字符串

thtygnil

thtygnil1#

随着@jlarson更新了Mac是最大罪魁祸首的信息,我们可能会得到一些进一步的信息。Office for Mac至少在2011年和2012年,在导入文件时对阅读Unicode格式的支持相当差。
对UTF-8的支持似乎几乎不存在,我读过一些关于它工作的评论,而大多数人说它不工作。不幸的是,我没有任何Mac可以测试。所以再次:文件本身应该是UTF-8,但导入会停止该过程。
用JavaScript写了一个快速测试,用于导出百分比转义的UTF-16 little和big endian,带- /不带BOM等。
代码可能需要重构,但测试应该没问题。它可能比UTF-8工作得更好。当然,这通常也意味着更大的数据传输,因为任何UTF-8都是两个或四个字节。
你可以在这里找到一个小提琴:

请注意,它并不以任何特定的方式处理CSV。它主要用于纯转换为具有UTF-8,UTF-16大/小字节序和+/- BOM的 * 数据URL*。在 fiddle 中有一个选项可以将逗号替换为制表符,-但如果它工作的话,相信这将是相当黑客和脆弱的解决方案。

  • 通常使用如下方式:*
// 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();

字符串

  • 对象有两个结果属性:*
  • 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:

文本可能是以ISO-8859-1、Windows-1252等格式导入的,而不是以UTF-8格式导入的。使用导入或其他方式强制应用程序以UTF-8格式读取文件。
PS:* The UniSearcher是一个很好的工具,可以在这个旅程中使用。

漫长的路途

100%确定我们正在查看的内容的 “最简单” 方法是在结果上使用十六进制编辑器。或者从命令行使用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


查看已下载文件的十六进制转储:

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.


在第二行,我们找到d480 d7a1 e0b8 81e1 8194,它与上面的匹配:

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

其他角色也没有被破坏。
如果你愿意,做类似的测试。结果应该是相似的。

根据提供的样本—, â€, “

我们也可以看看问题中提供的示例。它可能假设文本在Excel / TextEdit中由代码页1252表示。
引用Windows-1252上的维基百科:

  • Windows-1252或CP-1252是拉丁字母的字符编码,默认情况下用于英语和其他一些西方语言的Microsoft Windows的旧组件中。它是Windows代码页组中的一个版本。在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 的缩写
  • T是 * 翻译 * 的缩写

举例来说:

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

9d这样的特殊情况在CP-1252中没有对应的代码点,我们只是直接复制。
注意事项:如果通过将文本复制到文件并执行十六进制转储来查看损坏的字符串,则使用例如UTF-16编码来保存文件以获得表中表示的Unicode值。例如,在Vim中:

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

转换为UTF-8

然后,我们将结果(T.Hex线)合并组合,在UTF-8序列中,字节由一个前导字节表示,它告诉我们有多少个后续字节组成一个字节。例如,如果一个字节的二进制值为110x xxxx,我们知道这个字节和下一个字节代表一个代码-点。总共两个。1110 xxxx告诉我们它是三个等等。ASCII值没有设置高位,因此任何匹配0xxx xxxx的字节都是独立的。总共一个字节。

0xe2 = 1110 0010bin => 3 bytes => 0xe28094 (em-dash)  —
0x2c = 0010 1100bin => 1 byte  => 0x2c     (comma)    ,
0x2c = 0010 0000bin => 1 byte  => 0x20     (space)   
0xe2 = 1110 0010bin => 3 bytes => 0xe2809d (right-dq) ”
0x2c = 0010 1100bin => 1 byte  => 0x2c     (comma)    ,
0x2c = 0010 0000bin => 1 byte  => 0x20     (space)   
0xe2 = 1110 0010bin => 3 bytes => 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中,可以通过多种方式完成。

    • 方法一:*

不要保存带有应用程序识别的扩展名的文件,如.csv.txt,而是完全忽略它或编造一些东西。
作为一个例子,保存文件为"testfile",没有扩展名。然后在Excel中打开文件,确认我们实际上想要打开这个文件,* 瞧 * 我们得到了编码选项。选择UTF-8,文件应该被正确读取。

    • 方法二:*

使用导入数据而不是打开文件。类似于:

Data -> Import External Data -> Import Data

选择编码并继续。

检查Excel和所选字体是否真正支持

我们还可以使用更友好的剪贴板来测试Unicode字符的字体支持。例如,将此页面中的文本复制到Excel中:

如果存在对代码点的支持,则文本应该呈现良好。

Linux

在Linux上,这在用户态中主要是UTF-8,这不应该是一个问题。使用Libre Office Calc,Vim等显示正确渲染的文件。

为什么它有效(或应该)

来自规范状态的encodeURI,(另请阅读第15.1.3节):
encodeURI函数计算URI的新版本,其中某些字符的每个示例都被表示字符的UTF-8编码的一个、两个、三个或四个转义序列替换。
我们可以在控制台中简单地测试这一点,例如:

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

当我们注册时,转义序列等于上面十六进制转储中的转义序列:

%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.预期输入与损坏输出的示例(复制粘贴)。
1.原始数据与结果文件的十六进制转储示例。

sc4hvdpw

sc4hvdpw2#

我昨天正好遇到了这个问题。我正在开发一个按钮,可以将HTML表的内容导出为CSV下载。按钮本身的功能几乎与您的相同-单击时,我从表中读取文本并使用CSV内容创建数据URI。
当我试图在Excel中打开结果文件时,很明显 "£" 符号被错误地读取。2字节的UTF-8表示被处理为ASCII,导致不需要的垃圾字符。一些Google搜索表明这是Excel的已知问题。
我尝试在字符串的开头添加字节顺序标记- Excel只是将其解释为ASCII数据。(例如csvData.replace('\u00a3', '\xa3')),但我发现,任何时候数据被强制转换为JavaScript字符串时,它都会变成UTF-技巧是将其转换为二进制,然后进行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库的话,可能有办法在没有CryptoJS的情况下做同样的事情,但这至少表明这是可能的。

py49o6xq

py49o6xq3#

Excel喜欢使用UTF-16 LE和BOM编码的Unicode。输出正确的BOMFF FE),然后将所有数据从UTF-8转换为UTF-16 LE。
Windows在内部使用UTF-16 LE,因此某些应用程序使用UTF-16比使用UTF-8更好。
我还没有尝试过在JS中这样做,但是网上有各种各样的脚本可以将UTF-8转换为UTF-16。UTF变体之间的转换非常容易,只需要十几行。

1zmg4dgp

1zmg4dgp4#

我有一个类似的问题,从Sharepoint列表中拉入JavaScript的数据。它原来是一个叫做"Zero Width Space"的字符,当它被带入Excel时,它被显示为“”。显然,Sharepoint有时会在用户点击“退格键”时插入这些字符。
我用这个快速修复替换了它们:

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

字符串
看起来你可能有其他隐藏的字符在那里。我找到了代码点的零宽度字符在我的通过在Chrome检查器中查看输出字符串。检查器无法呈现字符,所以它用一个红点代替它。当你把鼠标悬停在那个红点上,它给你一个代码点(例如\u200B),你可以在各种代码点中添加不可见的字符,然后以这种方式删除它们。

llew8vvj

llew8vvj5#

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

字符串
这个应该可以

a8jjtwal

a8jjtwal6#

可能是服务器编码有问题。
如果您运行的是Linux,您可以尝试(假设本地环境为英语US):

sudo locale-gen en_US en_US.UTF-8
dpkg-reconfigure locales

字符串

li9yvcax

li9yvcax7#

在编写多字节CSV文件时,应应用这三条规则,以便在不同的操作系统平台(Windows、Linux、MacOS)上的Excel上都可以读取该文件。
1.制表符\t用于分隔字段,而不是逗号(,
1.内容必须以UTF-16小端字节序(UTF 16-LE)编码
1.内容必须以UTF 16-LE字节顺序标记(BOM)为前缀,即0xFEFF
顺便说一下,在使用NodeJS fs模块编写文件时,必须显式设置UTF 16-LE BOM。请参考此github issue以了解更多详细讨论。

相关问题