当代码运行时,为什么在'±'前面会打印出'Â'?

8

我正在尝试编写一个非常简单的输出语句,将其输出到csv文件中。它仅陈述数据的偏差边际,因此我使用'±'符号,使其读起来类似于“5 ft/s^2 ±2.4%”。

我正在使用Python3开发。我尝试了三种不同的方法来使用'±'符号:ASCII、Unicode和直接复制粘贴该字符到编辑器中。请参见以下内容。

val1 = 3.2
val2 = 2.4

s1 = val1 + "ft/sec^2 " + chr(241) + val2 + "%"
s2 = val1 + "ft/sec^2 " +  u'\u00B1' + val2 + "%"
s3 = val1 + "ft/sec^2 ±" + val2 + "%"

然而,对于这三种方法,我的输出始终都是相同的...

3.2ft/sec^2 ±2.4%

这个 'Â' 字符一直出现。我对编码和类似的东西完全没有经验。我搜索了一些似乎与我的情况相关的情况,但是不够理解以找到特定情况下的解决方案。

我使用 pandas DataFrame 收集数据,然后使用 .to_csv() 方法创建 csv 文件。文档说明它默认使用 'utf-8' 编码。

以下是能够重现同样问题的 7 行代码。

import pandas as pd 

df = pd.DataFrame(columns=['TestCol'])
df['TestCol'] = ["Test1: " + chr(241),
    "Test2: " + u'\u00B1',
    "Test3: " + "±"]
df.to_csv('TestExample.csv', index=False, encoding='utf-8')

在我的CSV文件中,有一列看起来像这样:
TestCol
Test1: ñ
Test2: ±
Test3: ±

非常感谢您提供的帮助、解释和知识!


1
第二个对我来说很好用,第一个输出的是ñ而不是±。你能编辑你的帖子并包含你用来写入CSV的代码吗? - Michael Kolber
1
UTF-8编码中Unicode代码点U+00A0到U+00BF的第二个字节恰好与代码点本身重合。您的UTF-8编码值被解释为ISO-8859。 - chepner
1
具有代码点241(十进制)的字符确实是 ñ - mzjn
@mzjn,这个问题有点出乎意料。当我在Python终端中输入chr(241)时,它输出的是ñ,但是在创建CSV文件的代码中,同样的操作却给了我±,这让我感到困惑。 - the_pied_shadow
@MichaelKolber 我更新了帖子,包括我使用的创建CSV的方法。 - the_pied_shadow
3个回答

14
Excel打开.csv文件时,假定使用Windows编码。这种编码取决于语言/国家,在英语和西欧国家中,它是cp-1252,非常类似于ISO-8859-1(也称为“Latin1”)。
此编码每个字符使用一个字节。这意味着它最多允许256个不同的字符(实际上它们少于256,因为有些代码保留给控制和不可打印字符)。
Python3使用Unicode表示字符串。Unicode没有“只有256”符号的限制,因为它内部使用约20位。实际上,Unicode可以表示世界上任何语言的任何字符(甚至包括一些超出这个世界的语言)。
问题在于,当Unicode必须写入文件(或通过网络传输)时,它必须被“编码”为一系列字节。其中一种方法,也是当前许多领域的标准,是“UTF-8”。
UTF-8编码每个字符使用可变数量的字节。它被设计为与ASCII兼容,因此ASCII表中的任何符号都用一个字节表示(与其ASCII代码相一致)。但是,任何不在ASCII中的字符都需要多于1个字节来表示。特别地,当字符±(代码点U+00B1或177)在UTF-8中编码时,需要两个十六进制值c2和b1的字节。
当Excel读取这些字节时,由于它假定cp-1252编码使用每个字符一个字节,它将解码序列c2、b1作为两个单独的字符。第一个字符被解码为Â,第二个字符非正式地被解码为±。
注意:顺便提一下,Unicodeñ(代码点U+00F1或241)也被编码为两个字节,值为c3、b1,当以cp-1252解码时,显示为Ãñ。请注意,第一个字符现在为Ã而不是Â,但第二个字符再次是(再次非正式)±。
解决方案是指示pandas在写文件时应使用cp-1252编码。
df.to_csv("file.csv", encoding="cp1252")

当然,这也存在一个潜在问题。由于“cp-1252”最多只能表示256个符号,而Unicode可以表示超过1M个符号,因此你的数据框架中的一些字符串数据可能使用了“cp-1252”无法表示的任何字符。在这种情况下,你将会得到一个编码错误。
另外,在使用Pandas读取这个.csv文件时,你需要指定编码方式,因为Pandas默认是UTF-8编码。
关于“utf-8-sig”的更新:
其他答案和一些评论提到了“utf-8-sig”编码,这可能是另一个有效(也许更好)的解决方案。我稍微解释一下它是什么。
UTF8并不是将Unicode转换为字节序列的唯一方式,虽然它被几个标准推荐。另一种流行的选择是(曾经是?)UTF-16。在这种编码方式下,所有Unicode字符都被编码为16位值(一些字符无法用这种方式表示,但是可以通过使用两个16位值扩展集合来解决)。
使用每个字符16位而不是8位的问题在于,这时字节序成为了相关因素。由于16位不是内存、网络和磁盘操作的基本单位,因此当您将16位值写入或发送到内存、网络或磁盘时,实际上会发送两个字节。这些字节发送的顺序取决于架构。例如,假设您需要在磁盘中写入16位数字66ff(以十六进制表示)。您必须将其“分解”为66ff,并决定先写哪一个。磁盘中的顺序可以是66ff(这被称为大端序),或ff66(这被称为小端序)。
如果您处于小端架构(例如英特尔),则磁盘中字节的默认顺序将与大端架构不同。当然,问题在于当您尝试在与创建文件的机器架构不同的机器上读取文件时。您可能会错误地组装这些字节为ff66,这将是不同的Unicode字符。
因此,必须有一种方法在文件中包含关于创建时使用的字节序的信息。这就是所谓的BOM(字节顺序标记)的作用。它由Unicode字符FEFF组成。如果将此字符写为文件中的第一个字符,则在读回文件时,如果您的软件发现FEFF作为第一个字符,它将知道读取文件时使用的字节序与编写文件时使用的字节序相同。但是,如果它找到FFFE(顺序被交换),则它将知道存在字节序不匹配,然后在读取时交换每对字节以获得正确的Unicode字符。
顺便说一下,Unicode标准没有其代码为FFFE的字符,以避免读取BOM时产生混淆。如果在开头找到FFFE,则意味着字节序错误,必须交换字节。
所有这些都与UTF-8无关,因为此编码使用字节(而不是16位)作为信息的基本单位,因此免疫字节序问题。尽管如此,您仍然可以在UTF-8中编码FEFF(它将导致一个由值为EF、BB和BF的3个字节序列),并将其写为文件中的第一个字符。这就是Python在指定utf-8-sig编码时所做的。
在这种情况下,它的目的不是帮助确定字节序,而是作为一种“指纹”,帮助读取文件的软件猜测使用的编码是UTF-8。如果软件在文件中找到前3个字节是“幻数”EFBBBF,它可以推断该文件以UTF-8存储。这三个字节被丢弃,其余部分从UTF-8解码。

特别地,Microsoft Windows在其大多数软件中使用此技术。显然,在Excel的情况下也适用,因此总结如下:

  • 您使用df.to_csv("file.csv", encoding="utf-8-sig")编写csv文件
  • Excel读取文件并在开头找到EFBBBF。因此,它会丢弃这些字节,并假定文件的其余部分为utf-8。
  • 稍后,序列c2b1出现在文件中,它将正确解码为UTF-8,生成±

这具有在任何Windows计算机上工作的优点,无论它使用的代码页是什么(cp1252用于西欧,其他国家可能使用其他代码页,但Unicode和UTF-8是通用的)。

潜在问题是,如果您尝试在非Windows机器上读取此csv文件,则可能会出现问题。软件可能无法识别前三个“魔术字节”EF、BB、BF的含义。因此,您可能会在文件开头遇到“虚假”的字符,这可能会导致问题。如果读取文件的软件假定UTF-8编码,则这三个字节将被解码为Unicode字符FFFE,但它们不会被丢弃。该字符是不可见的,宽度为零,因此任何编辑器都无法“看到”它,但它仍然存在。如果读取文件的软件假定其他任何编码,例如“latin1”,则这三个字节将被错误地解码为,并且它们将在文件开头可见。
如果您使用Python读回此文件,则必须再次指定utf-8-sig编码,以使Python丢弃这三个初始字节。

太棒了,感谢您的解释和解决方案。不过我需要注意一下,“cp-1252”在我的情况下会抛出错误,显然当传递到这个方法时需要写成“cp1252”。再次感谢! - the_pied_shadow
@the_pied_shadow,我已在答案中修复了这个问题。感谢您注意到它。 - JLDiaz
@JLDiaz 如果使用utf-8-sig作为编码方式,Excel可以读取UTF-8格式的文件。这种编码方式会在文件开头添加一个Excel能够识别的UTF-8签名。 - Mark Tolonen
@MarkTolonen 谢谢您指出这个问题。我扩展了答案,以解释这种编码的工作原理和一些注意事项。 - JLDiaz

3
您正在将文件写入UTF-8编码,但您使用的查看器将其视为Latin-1(或类似的Windows cp1252)。您可以尝试使用encoding = 'utf-8-sig'打开您要写入的文件,在文件开头加上BOM,这样应用程序就可以将其识别为UTF-8。或者您可以告诉您的查看器程序将其解释为UTF-8。我强烈建议不要像写入latin-1或类似编码,否则文本将无法在其他区域设置的系统上移植,除非明确告知如何解码它。

谢谢回复。我没有打开我要写入的文件,而是使用.to_csv()方法从Pandas数据框创建一个文件。查看文档,它默认使用utf-8进行编码,但我尝试明确说明它并没有改变任何东西。代码完成后,我使用Excel打开文件进行查看。 - the_pied_shadow
@the_pied_shadow:这就是为什么我特别建议使用utf-8-sig,而不仅仅是utf-8。写入UTF-8 BOM告诉Windows程序将字节解释为UTF-8,而不是cp1252(或其他可怕的特定于区域的编码)。如果您没有使用open,您仍然应该能够使用用于编写文件的任何内容来指定编码;在选择cp1252之前,请尝试一下;也就是说,“会烧掉使用您数据的西欧语言国家以外的任何人或非Windows系统上的任何人的编解码器”。 - ShadowRanger

1

s3 包含一个UTF8编码的值,其中±(U+00B1)的UTF8编码为\xc2\xb1。然而,您的终端将这些字节解释为ISO-8859编码的文本,而不是UTF-8编码的文本。在ISO-8859中,代码点C2是(您可能已经猜到)“”,代码点B1是“±”。实际上,在U+00A0和U+00BF之间的所有Unicode值的UTF-8编码的第二个字节与它们的Unicode代码点重合。此外,ISO-8859-1与代码点00-FF的Unicode相对应。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接