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

我最近添加了一个CSV下载button,从数据库(Postgres)从服务器(Ruby on Rails)中获取数据,并将其转换为客户端(Javascript,HTML5)上的CSV文件。 我目前正在testing的CSV文件,我遇到了一些编码问题。

当我通过“less”查看CSV文件时,文件显示正常。 但是当我在Excel或TextEdit中打开文件时,我开始看到奇怪的字符

“,”

出现在文本中。 基本上,我看到这里描述的字符: http : //digwp.com/2011/07/clean-up-weird-characters-in-database/

我知道当数据库编码设置被设置为错误的时候会出现这样的问题。 但是,我使用的数据库被设置为使用UTF8编码。 当我通过创buildCSV文件的JS代码进行debugging时,文本显示正常。 (这可能是一个Chrome的能力,而且能力较低)

我感到沮丧,因为我从我的在线search中学到的唯一的东西是,编码不能工作的原因可能有很多,我不确定哪个部分是错的(所以请原谅我最初标记了很多东西) ,而我所尝试的一切都为我的问题揭开了新的一页。

作为参考,这里是创buildCSV文件的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); }); 

正如@jlarson更新的信息,Mac是最大的罪魁祸首,我们可能会进一步。 对于Mac来说,Office至less有2011年的版本,而且在导入文件时读取Unicode格式的支持还不够。

对于UTF-8的支持似乎是不存在的,对于它的工作看得很less,但大多数人认为它没有。 不幸的是,我没有任何Mactesting。 所以再次说明:文件本身应该是UTF-8,但导入会暂停进程。

在Javascript中写了一个快速testing,用于导出百分比转义的UTF-16小字节和大字节,带/不带BOM等。

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

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

Unicode导出示例Fiddle

请注意,它不以任何特定方式处理CSV。 它主要是用来纯粹转换成UTF-8,UTF-16大/小端和+/- BOM 数据的URL 。 在小提琴中有一个选项可以用制表符replace逗号,但是如果它能够工作的话,相信这将是一个相当黑客和脆弱的解决scheme。


通常使用像:

 // 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的MIMEtypes,字符集等。 从传递给初始化程序的选项构build,或者也可以说.config({ ... new conf ...}).intro()来重新构build。

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

你可以指定base64 ,但没有base64转换(至less不是这么远)。

2.) encoder.buf

这是一个包含百分比转义数据的string。

.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: UniSearcher是一个很好的工具,可以在这个旅程中使用。

漫长的路程

100%确定我们所看到的“最简单”的方法是在结果上使用hex编辑器。 或者使用hexdumpxxd或类似命令来查看文件。 在这种情况下,字节序列应该是从脚本传送的UTF-8的字节序列。

作为一个例子,如果我们把jlarson的脚本取出data Array

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

这一个被合并到string中:

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

它通过Unicode转换为:

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

由于UTF-8使用ASCII作为基础( 设置最高位的字节与ASCII相同),testing数据中唯一的特殊序列是“Ԁסกwhich which”

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

看下载的文件的hex转储:

 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 | | | | | | | | | | | | | | +-+-+ +-+-+ +--+--+ +--+--+ | | | | | | | | | | | | | | | | Ԁ ס ก ၔ , seatt 

其他angular色也没有被破坏。

如果你想做类似的testing。 结果应该是相似的。


通过样品提供—, â€, “

我们也可以看看问题中提供的示例。 很可能会假定文本是由代码页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的缩写
  • TTranslated的缩写

例如:

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

9d这样的特殊情况在CP-1252中没有相应的代码点,我们直接复制。

注意:如果通过将文本复制到文件并执行hex转储来查看受损的string,请使用例如UTF-16编码保存该文件,以获取表中所示的Unicode值。 例如在Vim:

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

字节到UTF-8

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

  0xe2 = 1110 0010 bin => 3 bytes => 0xe28094(em-dash) -
 0x2c = 0010 1100 bin => 1 byte => 0x2c(逗号),
 0x2c = 0010 0000 bin => 1 byte => 0x20(空格)   
 0xe2 = 1110 0010 bin => 3 bytes => 0xe2809d(right-dq)“
 0x2c = 0010 1100 bin => 1 byte => 0x2c(逗号),
 0x2c = 0010 0000 bin => 1 byte => 0x20(空格)   
 0xe2 = 1110 0010 bin => 3 bytes => 0xe2809c(left-dq)“

结论; 原始的UTF-8string是:

 —, ”, “ 

把它弄回来

我们也可以做相反的事情。 原始string为字节:

 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 ,但完全忽略它或创build一些内容。

作为一个例子,将文件保存为"testfile" ,没有扩展名。 然后在Excel中打开文件,确认我们确实想要打开这个文件,然后我们得到了编码选项。 selectUTF-8,文件应该正确读取。

  • 方法二:

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

 Data -> Import External Data -> Import Data 

select编码并继续。

检查Excel和选定的字体实际上是否支持字形

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

  • 页面,代码点为U + 0E00至U + 0EFF

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


Linux的

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


为什么它有效(或应该)

encodeURI来自规范状态,(也读sec-15.1.3 ):

encodeURI函数计算一个URI的新版本,其中某些字符的每个实例都被代表该字符的UTF-8编码的一个,两个,三个或四个转义序列replace。

我们可以简单地在我们的控制台中进行testing,例如说:

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

正如我们注册的转义序列等于上面的hex转储中的转义序列:

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

或者testing一个4字节的代码:

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

如果这不符合

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

  1. 预期input与输出错误的示例(复制粘贴)。
  2. 原始数据与结果文件的hex转储示例

我昨天碰到了这个。 我正在开发一个button,导出一个HTML表格的内容作为CSV下载。 button本身的function与您的function几乎完全相同 – 单击时,我从表格中读取文本,并使用CSV内容创build数据URI。

当我试图在Excel中打开生成的文件时,显然“£”符号被错误地读取。 2字节的UTF-8表示正在作为ASCII处理,导致不需要的垃圾字符。 一些谷歌search表明这是Excel的一个已知问题。

我试图在string的开头添加字节顺序标记–Excel只是将它解释为ASCII数据。 然后,我尝试了各种各样的东西,把UTF-8string转换成ASCII(比如csvData.replace('\u00a3', '\xa3') ),但是我发现任何时候数据被强制转换为一个JavaScriptstring,都会变成UTF -8再次。 诀窍是将其转换为二进制,然后Base64对其进行编码,而不会一路转换回string。

我已经在我的应用程序中使用了CryptoJS (用于针对REST API的HMAC身份validation),并且可以使用它从原始string创buildASCII编码的字节序列,然后Base64将其编码并创build数据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是您的CSVstring。

没有CryptoJS,如果你不想引进这个库,也许有办法做同样的事情,但是这至less表明它是可能的。

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变化之间的转换非常简单,只需要十几行。

我从一个Sharepoint列表中拖入JavaScript的数据中遇到了类似的问题。 它变成了一个叫做“零宽度空间”的字符,它被显示为“当它被带入Excel中”。 显然,Sharepoint在用户点击“退格”时会插入这些内容。

我用这个quickfixreplace了它们:

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

看起来你可能在那里还有其他隐藏的angular色。 我通过查看Chrome检查器中的输出stringfind了我的零宽度字符的代码点。 检查员无法渲染字符,所以用红点代替。 当您将鼠标hover在红点上时,它将为您提供代码点(例如\ u200B),您可以将各个代码点分别隐藏到不可见字符中,然后将其删除。

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

如果您正在运行Linux,则可以尝试(假设本地语言为英语):

 sudo locale-gen en_US en_US.UTF-8 dpkg-reconfigure locales 
 button.href = 'data:' + mimeType + ';charset=UTF-8,%ef%bb%bf' + encodedUri; 

这应该做的伎俩