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

 Henry-lau洋 发布于 2023-01-31 14:56

我最近添加了一个CSV下载按钮,它从服务器(Ruby on Rails)获取数据库(Postgres)中的数据,并将其转换为客户端的CSV文件(Javascript,HTML5).我目前正在测试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 = $('');
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);
});

user13500.. 37

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

对UTF-8的支持似乎几乎不存在,已经阅读了一些关于它工作的评论,而大多数人说它没有.不幸的是我没有任何Mac可以测试.所以:文件本身应该是正确的UTF-8,但导入会暂停进程.

在Javascript中编写了一个快速测试,用于导出转义百分比转义的UTF-16小端和大端,有/无BOM等.

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

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

Unicode export sample Fiddle

请注意,它以任何特定的方式处理CSV.它主要用于纯转换为具有UTF-8,UTF-16大/小端和+/- BOM的数据URL.小提琴中有一个选项可以用标签替换逗号,但是如果它有效的话,相信这将是一个相当hackish和脆弱的解决方案.


通常使用如下:

// 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-type,charset等.根据传递给初始化程序的选项构建,或者也可以说.config({ ... new conf ...}).intro()重新构建.

data:[][;charset=][;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:[][;charset=][;base64],
    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:UniSearcher是一个很好的工具,可以在这个旅程中使用.

很长的路要走

"最简单"的方式为100%,确保了我们所看到的是使用在结果的十六进制编辑器.或者hexdump,xxd从命令行使用等来查看文件.在这种情况下,字节序列应该是从脚本传递的UTF-8的字节序列.

作为一个例子,如果我们采用jlarson的脚本,它需要data 数组:

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

这个被合并到字符串中:

 name,city,state
 \u0500\u05E1\u0E01\u1054,seattle,washington

通过Unicode转换为:

 name,city,state
 ????,seattle,washington

由于UTF-8使用ASCII作为基础(设置最高位的字节与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

其他任何角色都没有被破坏.

如果你想要做类似的测试.结果应该是类似的.


提供样品 —, â€, “

我们还可以查看问题中提供的示例.可能假设文本在代码页1252的Excel/TextEdit中表示.

在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

9dCP-1252中没有相应代码点的特殊情况,我们只需直接复制.

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

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

字节为UTF-8

然后我们将结果和T.Hex线组合成UTF-8.在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 => 
...

等等,结果:

—, â€, “

导入到MS Excel

换句话说:手头的问题可能是如何将UTF-8文本文件导入MS Excel和其他一些应用程序.在Excel中,这可以通过各种方式完成.

方法一:

不要使用应用程序识别的扩展名保存文件,例如.csv,或者.txt,但是完全省略它或者做一些事情.

例如,将文件保存为"testfile",没有扩展名.然后在Excel中打开该文件,确认我们其实是想打开这个文件,我们获得与编码选项服务.选择UTF-8,应正确读取文件.

方法二:

使用导入数据而不是打开文件.就像是:

Data -> Import External Data -> Import Data

选择编码并继续.

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

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

代码点U + 0E00到U + 0EFF的页面

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


Linux的

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


为什么它有效(或应该)

来自spec状态的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"

如果这不符合

如果没有这个适用,如果你添加它可能会有所帮助

    预期输入与损坏输出的样本,(复制粘贴).

    示例原始数据与结果文件的十六进制转储.


Rob Fletcher.. 5

我昨天碰到了这个.我正在开发一个按钮,将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的情况下执行相同的操作但这至少表明它是可能的.

撰写答案
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有