【问题标题】:Encoding issues for UTF8 CSV file when opening Excel and TextEdit打开 Excel 和 TextEdit 时 UTF8 CSV 文件的编码问题
【发布时间】:2014-02-16 00:38:37
【问题描述】:

我最近添加了一个 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 sn-p!

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

【问题讨论】:

  • 这是一个 JSBin,它复制了一些变体:jsbin.com/wuxeceza/4
  • 请注意,操作系统、浏览器和 Excel 版本之间的行为可能会有所不同,因此如果您想要赏金,请在回答中考虑这些因素!
  • @jlarson:你改了吗?第一个文件有 UTF-8 BOM 标记...
  • @user13500 UTF-8 BOM 标记似乎在任何地方都没有帮助,因此已将其删除。
  • @jlarson:嗯,不,但是在测试时检查代码库是否发生变化有点困难...... :)

标签: javascript excel csv encoding utf-8


【解决方案1】:

在编写多字节 CSV 文件时应该应用这三个规则,以便它可以在不同操作系统平台(Windows、Linux、MacOS)的 Excel 上读取

  1. 制表符\t 用于分隔字段而不是逗号 (,)
  2. 内容必须以 UTF-16 little endian (UTF16-LE) 编码
  3. 内容必须以UTF16-LE字节顺序标记(BOM)为前缀,即0xFEFF

这里是an article,它展示了如何重现编码问题并介绍了解决方案。 NodeJS 用于创建 CSV 文件。

附带说明,在使用 NodeJS fs 模块写入文件时,必须明确设置 UTF16-LE BOM。请参阅此github issue 以获得更详细的讨论。

【讨论】:

    【解决方案2】:
    button.href = 'data:' + mimeType + ';charset=UTF-8,%ef%bb%bf' + encodedUri;
    

    这应该可以解决问题

    【讨论】:

      【解决方案3】:

      对于从 Sharepoint 列表中提取到 Javascript 中的数据,我遇到了类似的问题。结果是一个叫做"Zero Width Space" 的字符,当它被带入Excel 时,它被显示为-。显然,Sharepoint 有时会在用户点击“退格”时插入这些内容。

      我用这个快速修复替换了它们:

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

      看起来你可能还有其他隐藏的字符。通过查看 Chrome 检查器中的输出字符串,我找到了我的零宽度字符的代码点。检查员无法渲染字符,因此将其替换为红点。当您将鼠标悬停在该红点上时,它会为您提供代码点(例如 \u200B),您只需将各种代码点添加到不可见字符中并以这种方式删除它们。

      【讨论】:

      • 这可能不是问题的理想解决方案,但它非常适合解决 SharePoint 零宽度空间问题
      【解决方案4】:

      我昨天正好遇到了这个。我正在开发一个按钮,将 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】:

        随着@jlarson 更新有关 Mac 是最大罪魁祸首的信息,我们可能会进一步了解。 Office for Mac 至少在 2011 年及以后版本对导入文件时读取 Unicode 格式的支持相当差。

        对 UTF-8 的支持似乎几乎不存在,读过一些关于它的工作的 cmets,而大多数人说它没有。不幸的是,我没有任何 Mac 可以测试。再说一遍:文件本身应该是 UTF-8,但导入会停止进程。

        用 Javascript 编写了一个快速测试,用于导出百分比转义的 UTF-16 小端和大端、带/不带 BOM 等。

        代码可能应该被重构,但应该可以用于测试。它可能比 UTF-8 更好。当然,这通常也意味着更大的数据传输,因为任何字形都是两个或四个字节。

        你可以在这里找到一个小提琴:

        Unicode export sample Fiddle

        请注意,它以任何特定方式处理 CSV。它主要用于纯转换为具有 UTF-8、UTF-16 大/小端和 +/- BOM 的 数据 URLfiddle 中有一个选项可以用制表符替换逗号,但相信如果可行的话,这将是一个相当老套和脆弱的解决方案。


        通常使用如下:

        // 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.) 作为一个返回。


        主要代码:


        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 的脚本为例,它采用 data Array:

        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”。

        检索原始字节

        要将其翻译回原来的形式,我们可以查看code page layout,从中我们可以得到:

        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中没有对应的code-point,我们直接复制就好了。

        注意:如果通过将文本复制到文件并执行十六进制转储来查看损坏的字符串,请使用例如 UTF-16 编码保存文件以获得表中所示的 Unicode 值。例如。在 Vim 中:

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

        字节到 UTF-8

        然后我们将结果(T.Hex 行)组合成 UTF-8。在 UTF-8 序列中,字节由 leading byte telling us how many subsequent bytes make the glyph 表示。例如,如果一个字节具有二进制值110x xxxx,我们知道这个字节和下一个字节代表一个代码点。一共两个。 1110 xxxx 告诉我们它是三个,依此类推。 ASCII 值没有设置高位,因此任何匹配 0xxx xxxx 的字节都是独立的。一共1个字节。

        0xe2 = 1110 0010bin => 3 个字节 => 0xe28094 (em-dash) —
        0x2c = 0010 1100bin => 1 字节 => 0x2c(逗号),
        0x2c = 0010 0000bin => 1 字节 => 0x20(空格)
        0xe2 = 1110 0010bin => 3 字节 => 0xe2809d (right-dq)”
        0x2c = 0010 1100bin => 1 字节 => 0x2c(逗号),
        0x2c = 0010 0000bin => 1 字节 => 0x20(空格)
        0xe2 = 1110 0010bin => 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 中,这可以通过多种方式完成。

        • 方法一:

        不要使用应用程序识别的扩展名保存文件,例如 .csv.txt,而应完全省略或编造一些内容。

        例如,将文件保存为"testfile",不带扩展名。然后在 Excel 中打开文件,确认我们确实要打开这个文件,然后 voilà 我们会得到编码选项。选择UTF-8,应该可以正确读取文件。

        • 方法二:

        使用导入数据而不是打开文件。比如:

        Data -> Import External Data -> Import Data
        

        选择编码并继续。

        检查 Excel 和所选字体是否确实支持字形

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

        如果存在对代码点的支持,则文本应该可以正常呈现。


        Linux

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


        为什么有效(或应该)

        来自规范状态的encodeURI,(另请阅读sec-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. 预期输入与错位输出的示例(复制粘贴)。
        2. 原始数据与结果文件的十六进制转储示例。

        【讨论】:

        • +1:详尽的解释,即使对OP没有帮助,但对以后的参考很有帮助。
        • 我觉得我现在应该能够解决各种编码错误了!感谢您的详细解释和建议。
        • @JiMun:希望至少它可以对这项任务有所了解。它变得有点长,并且可以更好地构建(当然还有很多主题可以包括在内)。另请注意,如果您不完全了解,JS 使用 16 位代码和代理对来表示 16 位以上的值。 (如果您决定在 JS 中使用字节。)
        • 好的,这并不能解决问题,但有一些非常好的研究,所以谢谢你——赏金是你的@user13500
        • 谢谢!你是摇滚明星。刚刚用这个拯救了我的身后!
        【解决方案6】:

        Excel 喜欢 UTF-16 LE 和 BOM 编码中的 Unicode。输出正确的BOM (FF FE),然后将所有数据从 UTF-8 转换为 UTF-16 LE。

        Windows 在内部使用 UTF-16 LE,因此某些应用程序使用 UTF-16 比使用 UTF-8 效果更好。

        我没有尝试在 JS 中这样做,但是网络上有各种脚本可以将 UTF-8 转换为 UTF-16。 UTF 变体之间的转换非常简单,只需要十几行。

        【讨论】:

          【解决方案7】:

          这可能是您的服务器编码有问题。

          如果您正在运行 Linux,您可以尝试(假设语言环境为美国英语):

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

          【讨论】:

          • 我们正在尝试从客户端执行此操作。 “在客户端将其转换为 CSV 文件(Javascript、HTML5)”
          • 我理解,但这并不一定意味着服务器发送给客户端的内容是正确编码的(即使它通过服务器在数据库中是utf-8)
          • @jlarson 您可以使用github.com/SheetJS/js-codepage 之类的东西在 UTF8 和代码页 1252 之间进行转换
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-08-14
          • 2015-08-30
          • 2013-03-02
          • 1970-01-01
          • 1970-01-01
          • 2021-08-18
          相关资源
          最近更新 更多