简短回答
iconv
会使用您指定的任何输入/输出编码,而不管文件内容是什么。如果您指定了错误的输入编码,则输出将变得混乱。
- 您可以尝试使用
file
命令检测文件的类型/编码。
file
只猜测文件的编码,可能是错误的(特别是在大文件中晚期才出现特殊字符的情况下)。
- 即使运行了
iconv
,由于file
尝试猜测编码的方式有限,可能不会报告任何更改。有关具体示例,请参见我的长答案。
- 您可以使用
hexdump
查看非7位ASCII文本的字节,并与常见编码的代码表(ISO 8859- *,UTF-8)进行比较,以自行决定编码。
- 7位ASCII(也称为US ASCII)在字节级别上与UTF-8和8位ASCII扩展(ISO 8859- *)完全相同。因此,如果您的文件只有7位字符,则可以将其称为UTF-8、ISO 8859- *或US ASCII,因为在字节级别上它们都是相同的。只有当您的文件具有7位ASCII范围之外的字符时,才有意义谈论UTF-8和其他编码(在此上下文中)。
长答案
今天我遇到了这个问题,并看到了你的问题。也许我可以添加一些信息,以帮助其他遇到这个问题的人。
ASCII
首先,术语ASCII是多义的,这导致了混淆。
7位ASCII仅包括128个字符(00-7F或0-127十进制)。有时也将7位ASCII称为US-ASCII。
ASCII
UTF-8
UTF-8编码使用与7位ASCII相同的编码来表示其前128个字符。因此,只包含该范围内的前128个字符的文本文件在使用UTF-8或7位ASCII编码时,在字节级别上将完全相同。
Codepage layout
ISO 8859-*和其他ASCII扩展
术语
扩展 ASCII (或
高 ASCII) 指包含标准七位 ASCII 字符加上其他字符的八位或更大字符编码。
扩展ASCII
ISO 8859-1(也称为"ISO Latin 1")是一个特定的8位ASCII扩展标准,涵盖了大部分西欧字符。还有其他针对东欧语言和西里尔语言的ISO标准。ISO 8859-1 包括德语和西班牙语中的 Ö,é,ñ 和 ß 等字符编码(UTF-8 也支持这些字符,但底层编码不同)。
"扩展"意味着 ISO 8859-1 包括7位 ASCII 标准,并使用第8位添加字符。因此,对于前128个字符,ISO 8859-1 在字节级别上等效于 ASCII 和 UTF-8 编码文件。然而,当您开始处理第128个字符之后的字符时,您就不再在字节级别上等效于 UTF-8,如果您想要将您的"扩展 ASCII"编码文件转换为 UTF-8 编码,则必须进行转换。
ISO 8859和专有适配
在ISO 8位ASCII扩展标准(ISO 8859-*
)发布之前,IBM、DEC、HP、Apple等公司都有许多专有的8位代码页(将字节映射到字符)。
ISO字符集与代码页不同的一个显著方式是,ISO标准中128至159的字符位置对应于带高位设置的ASCII控制字符是未使用和未定义的,尽管它们经常被用于专有代码页中的可打印字符
即在所有ISO 8位扩展中,字符128-159(80
-9F
)未被使用,而在以前的专有代码页中,这些字符用于ASCII控制字符(已存在于7位ASCII),但第8位被设置了。
上述关于未使用/定义
80
-
9F
的说法并不完全正确。 显然在 ISO / IEC 标准中,该范围为控制字符而被定义,但在同名的 IANA 字符集中,该范围未被定义。 我从一些存档的讨论中得到了这个信息,这些讨论出现在令人困惑且误导性的维基百科页面 windows-1252 上...但由于 ISO 标准需要付费才能验证,因此无法验证。
windows-1252
...进一步混淆事情。
在ISO 8位扩展发布后,微软发布了一个新的代码页windows-1252
,它是ISO-8859-1
的超集*,使用未使用的ISO字符范围128-159(80
-9F
)来表示智能引号等内容。如果您不理解,请比较代码表的第8x行和第9x行(iso-8859-1 windows-1252)。
Superset指的是,如果你将
ISO-8859-1
渲染为
windows-1252
,它看起来很好(因为在
windows-1252
中有相同编码的所有可打印字符也存在于
ISO-8859-1
中)...但是,如果你尝试将
windows-1252
渲染为
ISO-8859-1
,并且渲染的数据恰好包含128-159范围内的字节,则这些字符将无法正确显示。
常见的错误是将Windows-1252文本与字符集标签ISO-8859-1混淆。一个常见的结果是,在非Windows操作系统上,由文字处理软件产生的引号和撇号(通过“智能引号”生成)被替换为问号或方框,使文本难以阅读。大多数现代Web浏览器和电子邮件客户端将媒体类型字符集ISO-8859-1视为Windows-1252,以适应这种错误标记。这现在是HTML5规范的标准行为,该规范要求将广告作为ISO-8859-1的文档实际上使用Windows-1252编码进行解析。
在html5标准中,没有名为
ISO-8859-1
的编码,而是
iso-8859-1
是编码
windows-1252
的多个标签之一。
windows-1252
html5 encodings
*
- 注意,它并不是ISO/IEC 8859-1标准的技术上集合,因为该标准定义了
80
-
9F
范围内的控制字符,而
windows-1252
定义了该范围内的不同字符。但是,IANA字符集8859-1没有定义该范围内的字符,因此从技术上讲,它是IANA字符集的超集,但不是ISO/IEC标准的超集?(这就是为什么标准应该是开放的,以便我们可以检查这些内容。)
使用
file
检测编码
今天我学到的一课是,我们不能总是信任
file
来正确解释文件的字符编码。
file (command)
该命令只告诉文件看起来像什么,而不是它实际上是什么(在文件内容与其不匹配的情况下)。通过将一个魔术数字放入内容与之不匹配的文件中,很容易欺骗程序。因此,该命令除了特定情况外,不能用作安全工具。
file
查找文件中暗示类型的魔术数字,但这些可能是错误的,并没有保证正确性。
file
还尝试通过查看文件中的字节来猜测字符编码。基本上,
file
有一系列测试,帮助它猜测文件类型和编码。
我的文件是一个大型CSV文件。
file
报告此文件为US ASCII编码,这是
错误的。
$ ls -lh
total 850832
-rw-r--r-- 1 mattp staff 415M Mar 14 16:38 source-file
$ file -b --mime-type source-file
text/plain
$ file -b --mime-encoding source-file
us-ascii
我的文件中有umlauts(即Ö)。直到文件的100k行之后,第一个非7位ascii才出现。我怀疑这就是为什么file
没有意识到文件编码不是US-ASCII的原因。
$ pcregrep -no '[^\x00-\x7F]' source-file | head -n1
102321:�
我使用
PCRE's 的
grep
,因为我在 Mac 上。如果你使用 GNU grep,可以使用
-P
选项。另外,在 Mac 上,你可以安装
coreutils(通过
Homebrew 或其他方式),以获取 GNU grep。
我没有深入研究
file
的源代码,而且 man 页面也没有详细讨论文本编码检测,但是我猜测
file
在猜测编码之前不会查看整个文件。
无论我的文件编码是什么,这些非 7 位 ASCII 字符都会破坏东西。我的德语 CSV 文件是以
;
分隔的,提取单个列不起作用。
$ cut -d";" -f1 source-file > tmp
cut: stdin: Illegal byte sequence
$ wc -l *
3081673 source-file
102320 tmp
3183993 total
请注意
cut
错误,我的“tmp”文件只有102320行,第一个特殊字符在第102321行。
让我们看看这些非ASCII字符是如何编码的。我将第一个非7位ASCII字符转储到
hexdump
中,进行一些格式化,删除换行符(
0a
)并仅取前几个字符。
$ pcregrep -o '[^\x00-\x7F]' source-file | head -n1 | hexdump -v -e '1/1 "%02x\n"'
d6
0a
另一种方式。我知道第一个非7位ASCII字符在第102321行的第85个位置。我获取该行并告诉hexdump
从第85个位置开始获取两个字节。您可以看到特殊(非7位ASCII)字符用"."表示,下一个字节是"M"...因此这是单字节字符编码。
$ tail -n +102321 source-file | head -n1 | hexdump -C -s85 -n2
00000055 d6 4d |.M|
00000057
在这两种情况中,我们可以看到特殊字符由
d6
表示。由于该字符是德语字母Ö,因此我猜测ISO 8859-1应该包括它。果然,你可以看到"d6"是匹配的(
ISO/IEC 8859-1)。
重要问题是...如果我不能确定文件编码,如何知道这个字符是Ö? 答案是上下文。我打开了文件,读取了文本,然后确定它应该是什么字符。如果我在
Vim中打开它,它会显示为Ö,因为Vim比
file
更好地
猜测字符编码(在这种情况下)。
所以,我的文件似乎是ISO 8859-1编码。理论上,我应该检查其余的非7位ASCII字符,以确保ISO 8859-1适合...除了良好的礼仪外,没有任何强制要求程序在将文件写入磁盘时只使用单个编码。
我将跳过检查并继续进行转换步骤。
$ iconv -f iso-8859-1 -t utf8 source-file > output-file
$ file -b --mime-encoding output-file
us-ascii
嗯,即使转换后,file
仍然告诉我这个文件是美国 ASCII 编码。让我们再次使用 hexdump
进行检查。
$ tail -n +102321 output-file | head -n1 | hexdump -C -s85 -n2
00000055 c3 96 |..|
00000057
明显有所改变。请注意,我们有两个非7位ASCII字符(右侧由"."表示),这两个字节的十六进制代码现在是c3 96
。如果我们仔细看,似乎我们现在使用的是UTF-8(c3 96
是UTF-8中Ö
的编码)UTF-8编码表和Unicode字符
但是,file
仍然报告我们的文件为us-ascii
?嗯,我认为这归结于关于file
未查看整个文件以及第一个非7位ASCII字符直到文件末尾才出现的观点。
我将使用sed
在文件开头插入一个Ö
,看看会发生什么。
$ sed '1s/^/Ö\'$'\n/' source-file > test-file
$ head -n1 test-file
Ö
$ head -n1 test-file | hexdump -C
00000000 c3 96 0a |...|
00000003
很酷,我们有一个带重音符号。注意编码是c3 96 (UTF-8)。嗯。
再次检查同一文件中的其他带重音符号:
$ tail -n +102322 test-file | head -n1 | hexdump -C -s85 -n2
00000055 d6 4d |.M|
00000057
ISO 8859-1。哎呀!这只是表明了编码容易混淆。为了清楚起见,我已经成功地在同一文件中创建了UTF-8和ISO 8859-1编码的混合体。
让我们尝试转换我们混乱(混合编码)的测试文件,并查看会发生什么,其中还包含着一个umlaut字符(Ö)。
$ iconv -f iso-8859-1 -t utf8 test-file > test-file-converted
$ head -n1 test-file-converted | hexdump -C
00000000 c3 83 c2 96 0a |.....|
00000005
$ tail -n +102322 test-file-converted | head -n1 | hexdump -C -s85 -n2
00000055 c3 96 |..|
00000057
第一个umlaut被解释为ISO 8859-1,因为这是我们告诉iconv的...虽然这不是我们想要的,但这是我们告诉iconf要做的。第二个umlaut从d6(ISO 8859-1)正确转换为c3 96(UTF-8)。
我会再试一次,但这次我将使用Vim插入Ö而不是sed。 Vim似乎更好地检测到编码(作为“latin1”即ISO 8859-1),因此它可能会以一致的编码插入新的Ö。
$ vim source-file
$ head -n1 test-file-2
�
$ head -n1 test-file-2 | hexdump -C
00000000 d6 0d 0a |...|
00000003
$ tail -n +102322 test-file-2 | head -n1 | hexdump -C -s85 -n2
00000055 d6 4d |.M|
00000057
实际上,当在文件开头插入字符时,Vim使用了正确/一致的ISO编码。
现在进行测试:file是否能更好地识别文件开头的特殊字符编码?
$ file -b --mime-encoding test-file-2
iso-8859-1
$ iconv -f iso-8859-1 -t utf8 test-file-2 > test-file-2-converted
$ file -b --mime-encoding test-file-2-converted
utf-8
是的,确实如此!故事的寓意是:不要相信
file
总是能正确猜测你的编码。在同一文件中混合编码很容易发生。如果不确定,请查看十六进制代码。
解决
file
处理大文件时的特定限制的一个技巧是缩短文件,以确保特殊(非 ASCII)字符尽早出现在文件中,这样
file
更有可能找到它们。
$ first_special=$(pcregrep -o1 -n '()[^\x00-\x7F]' source-file | head -n1 | cut -d":" -f1)
$ tail -n +$first_special source-file > /tmp/source-file-shorter
$ file -b --mime-encoding /tmp/source-file-shorter
iso-8859-1
您可以使用(可能正确的)检测到的编码作为
iconv
的输入,以确保正确转换。
更新
Christos Zoulas更新了file
,使得查看的字节数可配置。功能请求完成只用了一天时间,太棒了!
http://bugs.gw.com/view.php?id=533
允许从命令行更改分析文件时要读取的字节数
该功能在file
版本5.26中发布。
在猜测编码之前查看更多的大文件需要时间。但是,在特定用例中更好的猜测可能会抵消额外的时间和I/O,因此拥有此选项很不错。
请使用以下选项:
−P, −−parameter name=value
Set various parameter limits.
Name Default Explanation
bytes 1048576 max number of bytes to read from file
类似于...
file_to_check="myfile"
bytes_to_scan=$(wc -c < $file_to_check)
file -b --mime-encoding -P bytes=$bytes_to_scan $file_to_check
如果你想强制file
在猜测之前查看整个文件,那么这应该可以解决问题。当然,只有在你拥有file
5.26或更新版本时才有效。
更新2023-02-06
感谢@theprivileges指出,自file
5.44起,参数行为已经更改。现在有一个额外的encoding
参数,用于指定file
读取的字节中应使用多少字节进行编码确定。
例如:
file_to_check="myfile"
bytes_to_scan=$(wc -c < $file_to_check)
file -b --mime-encoding -P bytes=$bytes_to_scan -P encoding=$bytes_to_scan file_to_check="myfile"
注意!根据这个更改,用于确定编码的文件字节现在被限制为最大64k。因此,在非常大的文件中,如果特殊字符只出现在文件的后面,您可能需要使用不同的解决方法(例如将特殊字符移到文件的前面以进行正确检测)。
强制file
显示UTF-8而不是US-ASCII
其他答案似乎着重于尝试使file
显示UTF-8,即使文件仅包含普通的7位ASCII。如果您深思熟虑,您可能永远不想这样做。
如果一个文件仅包含7位ascii,但是
file
命令显示该文件为UTF-8,那么这意味着该文件包含了一些具有UTF-8特定编码的字符。如果这不是真的,可能会在后续过程中导致混淆或问题。如果
file
在文件只包含7位ascii字符时显示为UTF-8,则这是
file
程序的一个错误。
任何需要UTF-8格式输入文件的软件都不应该有任何问题使用普通的7位ascii,因为它们在字节级别上与UTF-8相同。如果有软件在接受文件之前使用
file
命令输出,并且除非"看到"UTF-8,否则无法处理文件...那就是设计得相当糟糕。我认为这是该程序的一个错误。
如果你一定要将纯7位ASCII文件转换为UTF-8,只需在文件中插入一个带有该字符的UTF-8编码的单个非7位ASCII字符即可完成。但我无法想象出需要这样做的用例。最简单的UTF-8字符用于此操作是字节顺序标记(BOM),它是一种特殊的不可打印字符,提示文件是非ASCII文件。这可能是最好的选择,因为它通常不会对文件内容产生视觉影响,因为它通常会被忽略。
Microsoft编译器和解释器以及许多Microsoft Windows上的软件(例如记事本)将BOM视为必需的幻数,而不是使用启发式方法。这些工具在将文本保存为UTF-8时添加BOM,并且除非存在BOM或文件仅包含ASCII,否则无法解释UTF-8。
这是关键:
或文件仅包含ASCII
一些 Windows 工具在读取 UTF-8 文件时可能会出现问题,除非存在 BOM 字符。然而,这不影响纯 7 位 ASCII 文件。也就是说,不能通过添加 BOM 字符来强制将纯 7 位 ASCII 文件转换为 UTF-8。
如果您仍然想这样做,以下是方法。在 UTF-8 中,BOM 由十六进制序列
0xEF,0xBB,0xBF
表示,因此我们可以将此字符轻松添加到纯 7 位 ASCII 文件的开头。通过向文件中添加一个非 7 位 ASCII 字符,文件不再只是 7 位 ASCII。请注意,我们没有修改或转换原始的 7 位 ASCII 内容。我们只是在文件开头添加了一个非 7 位 ASCII 字符,因此该文件不再完全由 7 位 ASCII 字符组成。
这里有更多关于在不需要时使用 BOM 可能会出现的问题的讨论(对于某些 Microsoft 应用程序消耗的实际 UTF-8 文件是需要的)。
https://dev59.com/enE95IYBdhLWcg3wn_f2#13398447
$ printf '\xEF\xBB\xBF' > bom.txt # put a UTF-8 BOM char in new file
$ file bom.txt
bom.txt: UTF-8 Unicode text, with no line terminators
$ file plain-ascii.txt # our pure 7-bit ascii file
plain-ascii.txt: ASCII text
$ cat bom.txt plain-ascii.txt > plain-ascii-with-utf8-bom.txt # put them together into one new file with the BOM first
$ file plain-ascii-with-utf8-bom.txt
plain-ascii-with-utf8-bom.txt: UTF-8 Unicode (with BOM) text
utf8_encode
、utf8_decode
等等...或者更深入一些: http://www.toptal.com/php/a-utf-8-primer-for-php-and-mysql https://dev59.com/13VC5IYBdhLWcg3wfhGL - eightyfive