简答
file
仅猜测文件编码并且可能是错误的(特别是在特殊字符仅在大文件中出现较晚的情况下)。
- 您可以使用
hexdump
查看非 7 位 ASCII 文本的字节,并与常见编码(ISO 8859-*、UTF-8)的代码表进行比较,以自行决定编码是什么。
iconv
无论文件的内容是什么,都将使用您指定的任何输入/输出编码。如果你指定了错误的输入编码,输出就会出现乱码。
- 即使在运行之后,由于尝试猜测编码的方式有限
iconv
,也可能不会报告任何更改。具体例子见我的长回答。file
file
- 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 编码对其前 128 个字符使用与 7 位 ASCII 相同的编码。因此,仅包含前 128 个字符范围内的字符的文本文件在字节级别上将是相同的,无论是使用 UTF-8 还是 7 位 ASCII 编码。
代码页布局
ISO 8859-* 和其他 ASCII 扩展
术语扩展 ASCII(或高位 ASCII)是指 8 位或更大的字符编码,包括标准的 7 位 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 和专有改编
检测编码file
我今天学到的一个教训是,我们不能相信file
总是对文件的字符编码给出正确的解释。
文件(命令)
该命令只告诉文件看起来像什么,而不是它是什么(在文件查看内容的情况下)。通过将幻数放入内容不匹配的文件中很容易欺骗程序。因此,除了在特定情况下,该命令不能用作安全工具。
file
在文件中寻找暗示类型的幻数,但这些可能是错误的,不能保证正确性。file
还尝试通过查看文件中的字节来猜测字符编码。基本上file
有一系列测试可以帮助它猜测文件类型和编码。
我的文件是一个大的 CSV 文件。file
将此文件报告为美国 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
我的文件中有变音符号(即 Ö)。第一个非 7 位 ascii 直到文件超过 100k 行才会显示。我怀疑这就是为什么file
没有意识到文件编码不是 US-ASCII 的原因。
$ pcregrep -no '[^\x00-\x7F]' source-file | head -n1
102321:�
我在 Mac 上,所以使用PCRE 的 grep
. 使用 GNU grep 您可以使用该-P
选项。或者,在 Mac 上,可以安装coreutils(通过Homebrew或其他)以获得 GNU grep。
我没有深入研究 的源代码file
,并且手册页没有详细讨论文本编码检测,但我猜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 编码的混合。
让我们尝试在前面使用变音符号 (Ö) 转换我们的损坏(混合编码)测试文件,看看会发生什么。
$ 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
UTF-8 的第一个变音符号被解释为 ISO 8859-1,因为这是我们告诉的iconv
……不是我们想要的,但这就是我们告诉 iconf 要做的。第二个变音符号正确地从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 -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 或更高版本时才有效。
强制file
显示 UTF-8 而不是 US-ASCII
file
即使文件仅包含纯 7 位 ascii ,其他一些答案似乎也侧重于尝试显示 UTF-8。如果你想通了,你可能永远不想这样做。
- 如果一个文件只包含 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,只需将一个非 7 位 ascii 字符插入到该字符的 UTF-8 编码文件中,您就完成了。但我无法想象你需要这样做的用例。最简单的 UTF-8 字符是字节顺序标记 ( BOM ),它是一种特殊的非打印字符,提示文件是非 ascii。这可能是最好的选择,因为它不应该在视觉上影响文件内容,因为它通常会被忽略。
Microsoft 编译器和解释器以及 Microsoft Windows 上的许多软件(如记事本)将 BOM 视为必需的幻数,而不是使用启发式方法。这些工具在将文本保存为 UTF-8 时会添加 BOM,除非 BOM 存在或文件仅包含 ASCII ,否则无法解释 UTF-8。
这是关键:
或文件仅包含 ASCII
因此,除非 BOM 字符存在,否则 Windows 上的某些工具无法读取 UTF-8 文件。但是,这不会影响纯 7 位纯 ascii 文件。即,这不是通过添加 BOM 字符来强制普通 7 位 ascii 文件为 UTF-8 的原因。
这里有更多关于在不需要时使用 BOM 的潜在缺陷的讨论(某些 Microsoft 应用程序使用的实际 UTF-8 文件需要它)。 https://stackoverflow.com/a/13398447/3616686
不过,如果您仍然想这样做,我很想听听您的用例。这里是如何。在 UTF-8 中,BOM 由十六进制序列表示0xEF,0xBB,0xBF
,因此我们可以轻松地将这个字符添加到我们普通的 7 位 ascii 文件的前面。通过在文件中添加非 7 位 ascii 字符,文件不再只是 7 位 ascii。请注意,我们根本没有修改或转换原始的 7 位 ASCII 内容。我们在文件开头添加了一个非 7 位 ASCII 字符,因此文件不再完全由 7 位 ASCII 字符组成。
$ 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