将US-ASCII强制转换为UTF-8编码(iconv)

74

我想将一堆文件从US-ASCII转换成UTF-8编码。

为此,我使用iconv:

iconv -f US-ASCII -t UTF-8 file.php > file-utf8.php

我的原始文件是US-ASCII编码的,这使得转换无法进行。显然这是由于ASCII是UTF-8的子集...

将US ASCII转换为UTF-8或ISO-8859-15

引用一下:

在非ASCII字符被引入之前,文本文件不需要以其他方式出现

没错。如果我在文件中引入一个非ASCII字符并保存它,比如使用Eclipse,那么文件的编码(字符集)将切换到UTF-8。

对于我的情况,我想强制iconv无论如何都将文件转换为UTF-8。无论其中是否存在非ASCII字符。

注意: 原因是我的PHP代码(非ASCII文件...)正在处理一些非ASCII字符串,这导致字符串无法被正确解释(法语):

Il était une fois... l'homme série animée mythique d'Albert

Barillé (Procidis), 1ère

...

  • US ASCII - 就是 - UTF-8的一个子集 (见下面的Ned's答案)
  • 这意味着US ASCII文件实际上是用UTF-8编码的
  • 我的问题来自别处

你能记得你的问题来自哪里吗?我遇到了类似的问题。 - DrogoNevets
1
@DrogoNevets 不太记得了,但我认为这与在PHP中使用UTF8以及与数据库之间的转换有关...utf8_encodeutf8_decode等等...或者更深入一些: http://www.toptal.com/php/a-utf-8-primer-for-php-and-mysql https://dev59.com/13VC5IYBdhLWcg3wfhGL - eightyfive
要执行相反的操作(从UTF8转换为ASCII),请参阅如何去除重音并将字母转换为“普通”ASCII字符? - Skippy le Grand Gourou
12个回答

95

ASCII是UTF-8的子集,因此所有ASCII文件已经采用了UTF-8编码。 ASCII文件中的字节和“将其编码为UTF-8”的字节完全相同。它们之间没有区别,因此不需要做任何事情。

看起来你的问题是文件实际上并不是ASCII格式。你需要确定它们正在使用什么编码,并进行适当的转换。


61

简短回答

  • 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'sgrep,因为我在 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

实际上,file 命令只会查看文件的前几个 KB 来生成其判断。 - tripleee
感谢您的反馈,我更新了我的答案,希望能更有帮助。 ;) - mattpr
我添加了缺失的链接,尽管我不确定我最后一个猜对了没有。 - tripleee
(虽然我很想修复无用的“cat”,但我还是让你自己来吧。) - tripleee
2
非常好的解释。这应该是最佳答案。我有你在这里描述的确切情况。 - Clint L
显示剩余3条评论

26

有人说你不能这样做,我知道当你提出问题并得到这样的答案时可能会感到沮丧。

如果你真的想要在 UTF-8 中显示而不是 US ASCII,则需要分两步进行。

第一步:

iconv -f us-ascii -t utf-16 yourfile > youfileinutf16.*

第二点:

iconv -f utf-16le -t utf-8 yourfileinutf16 > yourfileinutf8.*

接着,如果您执行file -i命令,您会发现新的字符集为UTF-8。


谢谢,这正是我所需要的。 - Aqsa javed

14

我认为Ned已经抓住了问题的核心——你的文件实际上不是ASCII格式。请尝试

iconv -f ISO-8859-1 -t UTF-8 file.php > file-utf8.php

我猜测您实际上使用的是ISO 8859-1。它在大多数欧洲语言中很受欢迎。


1
不行。它没起作用。我试过了,但是无论如何,如果我运行$ file --mime file.php,我得到的结果是file.php: text/x-php charset=us-ascii... 所以我推测我的文件实际上是ASCII编码的? - eightyfive
file 不会检查整个文件;尝试将字符串移到文件顶部,可能放在注释块中。 - sarnold
另一个检查是否有ASCII文件的选项是运行像这个Ruby程序一样的脚本:File.open("file.php").each_char {|c| puts c if c.ord > 127}。(我选择了Ruby,因为我知道如何快速编写此代码;任何其他类似的语言都同样容易。) - sarnold
根据Smultron,我的文件是Unicode(UTF-8)编码的... 因此Ned确实是正确的。US-ASCII是UTF-8的子集。那么我的问题应该来自其他地方(问题在于我没有处理php文件中的非ASCII字符串,而是通过互联网接收它们:我正在抓取一个网页...)。感谢您的时间! - eightyfive

2

US ASCII和UTF-8之间没有区别,因此不需要重新转换。

但是,如果在重新编码时遇到特殊字符的问题,请尝试在源字符集参数后面添加//TRANSLIT。

示例:

iconv -f ISO-8859-1//TRANSLIT -t UTF-8 filename.sql > utf8-filename.sql

这可以帮助我处理奇怪的引号类型,这些类型总是破坏字符集重新编码过程。


2
vim -es '+set fileencoding=utf-8' '+wq!' file
-es会以exscript模式运行vim,因此不会渲染任何内容。然后它会执行设置文件编码的命令(vim会处理细节),最后用'+wq!'关闭文件。

对于这个问题,之前使用iconv的答案并不能很好地解决问题,在加上-c参数时,文件中仍然存在非UTF-8字符。我来晚了,请见谅。


2

以下是一个脚本,它将查找与传递给它的模式匹配的所有文件,并将它们从当前的文件编码转换为UTF-8。如果编码是US ASCII,则仍将显示为US ASCII,因为它是UTF-8的子集。

#!/usr/bin/env bash
find . -name "${1}" |
    while read line;
    do
        echo "***************************"
        echo "Converting ${line}"

        encoding=$(file -b --mime-encoding ${line})
        echo "Found Encoding: ${encoding}"

        iconv -f "${encoding}" -t "utf-8" ${line} -o ${line}.tmp
        mv ${line}.tmp ${line}
    done

1

您可以使用file -i 文件名来检查您的原始文件格式。

一旦您获得了这个信息,您可以执行以下操作:

iconv -f old_format -t utf-8 input_file -o output_file

1
我不小心以UTF-7编码了一个文件,遇到了类似的问题。当我输入file -i name.file时,我会得到charset=us-ascii
由于我了解到UTF-7是US ASCII的子集,UTF-8也是如此,因此iconv -f us-ascii -t utf-9//translit name.file无法工作。
为了解决这个问题,我输入了以下命令: iconv -f UTF-7 -t UTF-8//TRANSLIT name.file -o output.file 除了其他人在这里提出的建议之外,我不确定如何确定编码。

0

仅供参考,file默认情况下不会检查整个内容(如mattpr的长答案中已经提到)以便检测文件的编码。为了强制扫描整个内容以进行字符集检测,可以使用以下代码...

file_to_check="myfile"
bytes_to_scan=$(wc -c < $file_to_check)
file -b --mime-encoding --parameter encoding=$bytes_to_scan $file_to_check

同时参考相关手册https://man7.org/linux/man-pages/man1/file.1.html


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