SDB:纯文本与区域设置

跳转到:导航搜索



本文解释了在处理所谓的“纯文本文件”时,必须考虑“区域设置”(语言和文化规则的设置)的原因。

本文中显示的命令并非作为某种权威指令。它们只是作为示例,旨在说明“纯文本”和“区域设置”是相互关联的,并且当两者不匹配时会存在“纯文本与区域设置”的冲突。

本文并非旨在解释编码的世界。因此,它既不提供背景信息,也不提及与底层概念相关的问题(例如“UTF-8 与 Unicode”,请参阅 SDB_Talk:Plain_Text_versus_Locale)。这些主题可以在单独的文章中描述。这篇文章已经足够长了。

本文使用了特殊字符。为了在您的浏览器中显示它们,使用了以下 HTML 实体,并且匹配的特殊字符在此处用括号括起来显示

  • ä 用于显示带变音符的拉丁小写字母 a:( ä )
  • Ã 用于显示带波浪线的拉丁大写字母 A:( Ã )
  • ¤ 用于显示货币符号:( ¤ )
  • € 用于显示欧元符号:( € )
  • δ 用于显示希腊小写字母 delta:( δ )
  • Ξ 用于显示希腊大写字母 XI:( Ξ )
  • ΄ 用于显示希腊音调:( ΄ )
  • Â 用于显示带帽子的拉丁大写字母 A:( Â )
  • â 用于显示带帽子的拉丁小写字母 a:( â )
  • ¬ 用于显示非符号:( ¬ )
  • ` 用于显示重音符号:( ` )
  • ´ 用于显示锐音符号:( ´ )

如果您的浏览器未显示所有这些特殊字符,即如果您在上方看到一对空括号,则可能需要调整浏览器设置,以便正确显示本文。

情况

您拥有所谓的“纯文本文件”,您“一直以来”使用各种“传统的”Unix/Linux 工具来处理它们,但从一段时间开始,您会注意到一些奇怪的结果或意外的副作用,这些结果或副作用过去没有发生过,或者没有在传统工具的手册页中提及。

不存在“纯文本”

存在 ASCII 文本和各种其他编码的文本(例如 ISO-8859-1 和 UTF-8),但对于特定的文本,“纯文本”一词本身没有意义。

推理

所谓的“纯文本文件”不存储其内容所使用的编码信息。

“编码”是指当文本的字符存储时(例如,在文件中),字符被编码为字节值,请参阅下面的“字节与字符”部分。

“纯文本文件”的内容是纯字节序列,没有任何附加信息说明该字节序列意味着哪种编码。

因此,无法自动检测“纯文本文件”中的字节意味着哪种编码。有一些工具可以猜测可能意味着哪种编码,但其结果只是猜测。

对于特定的“纯文本文件”,在没有关于该特定“纯文本文件”中字节序列所意味着的编码的附加信息的情况下,“纯文本”一词本身没有意义。

如果可以,可以使用“纯文本”一词来模糊地描述“以任何编码表示的文本”作为文本,例如,为了将其与任意二进制数据或存储在更高级格式(如 HTML 或 PDF 或任何类型的办公文档)中的文本区分开来。

相反,对于特定文本的文件,应该用实际的编码(如“ASCII 文本”、“ISO-8859-1 文本”或“UTF-8 文本”)替换“纯”这个词。严格来说,“文本”这个词是多余的,因为“ASCII”、“ISO-8859-1”或“UTF-8”序列的字节是特定编码中的文本。

后果

当程序处理“纯文本文件”时,运行程序的用户必须设置与“纯文本文件”的编码匹配的区域设置环境,然后才能运行程序。

“区域设置”是一组语言和文化规则,例如字符集、词法约定等,这些规则通过各种环境变量指定,特别是 LC_ALL 和 LANG(请参阅“man 7 locale”)。“locale”命令显示当前区域设置环境变量的值。 “locale --all-locales”命令输出可以设置为区域设置环境变量值的可用区域设置。

要设置“传统的”Unix/Linux 区域设置环境,请使用

export LC_ALL=POSIX ; export LANG=POSIX

如果您想像“一直以来”一样使用各种“传统的”Unix/Linux 工具来处理您的“纯文本文件”,则必须使用 POSIX 区域设置,否则您会得到奇怪的结果和意外的副作用。

要设置 UTF-8 区域设置环境,请使用以下其中之一

export LC_ALL=en_US.utf8 ; export LANG=en_US.utf8

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

export LC_ALL=de_DE.utf8 ; export LANG=de_DE.utf8

运行程序的用户必须知道其“纯文本文件”的正确编码。

一个特殊情况是,当程序收到根据用户通过区域设置环境指定的编码而言是非法的字节时,程序会做什么。

例如,在“POSIX”区域设置环境中出现非 ASCII 字节,或者在“...utf8”区域设置环境中出现 UTF-8 中不可能的字节序列。

在这种情况下,结果是不确定的,因为程序在这种情况下做什么是实现细节。程序可能会中止或跳过非法字节,或者做其他任何事情。

字节与字符

根据区域设置,相同的字节值可能意味着不同的字符,相同的字符可能被编码为不同的字节值。

在 ISO-8859-1 和 ISO-8859-15 中,十六进制字节值 0xE4 意味着字符 ä(带变音符的拉丁小写字母 a,例如德语 a-umlaut)。在 UTF-8 中,该字符使用两个十六进制字节值 0xC3 和 0xA4 编码。但在 ISO-8859-1 中,这两个字节意味着字符 Ã(带波浪线的拉丁大写字母 A)和 ¤(货币符号),在 ISO-8859-15 中,这两个字节意味着字符 Ã(带波浪线的拉丁大写字母 A)和 €(欧元符号)。

在 ISO-8859-7 中,十六进制字节值 0xE4 意味着字符 δ(希腊小写字母 delta)。在 UTF-8 中,该字符使用两个十六进制字节值 0xCE 和 0xB4 编码。但在 ISO-8859-7 中,这两个字节意味着字符 Ξ(希腊大写字母 XI)和音调 ΄(希腊音调)。

在 ISO-8859-1 中,十六进制字节值 0xA4 意味着字符 ¤(货币符号)。在 UTF-8 中,该字符使用两个十六进制字节值 0xC2 和 0xA4 编码。但在 ISO-8859-1 中,这两个字节意味着字符 Â(带帽子的拉丁大写字母 A)和 ¤(货币符号)。

在 ISO-8859-15 中,十六进制字节值 0xA4 意味着字符 €(欧元符号)。在 UTF-8 中,该字符使用三个十六进制字节值 0xE2、0x82 和 0xAC 编码。但在 ISO-8859-15 中,这三个字节意味着字符 â(带帽子的拉丁小写字母 a)、0x82 是一个不可打印的“BPH”(此处允许断行)ISO-8859 控制字符,最后是 ¬(非符号)。

只有对于 ASCII 文本(7 位十六进制字节值 0x00 到 0x7F,请参阅“man ascii”),相同的字节值与字符之间存在相同的 1:1 映射,对于“常用”编码(特别是 ISO-8859 编码和 UTF-8),因此只有对于 ASCII 文本,区域设置环境才没有区别。

ASCII 字符集由 33 个不可打印的控制字符(十六进制字节值从 0x00 到 0x1F 和 0x7F)和以下 95 个可打印字符组成,从空格字符开始(十六进制字节值从 0x20 到 0x7E)

  ! " # $ % & ' ( ) * + , - . /
0 1 2 3 4 5 6 7 8 9 : ; < = > ? @
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ `
a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~

单字节与多字节字符

ASCII 和 ISO-8859 编码使用单个字节来编码一个字符。

单个字节可以存储十进制值 0 到 255,因此单个字节编码只能支持最多 256 个不同的字符。

因此,使用 ASCII 和 ISO-8859 编码,不可能在同一个文本中拥有特定的字符。例如,不能同时拥有 ä(带变音符的拉丁小写字母 a)和 δ(希腊小写字母 delta),因为同一个文本不能同时使用 ISO-8859-1 和 ISO-8859-7 编码。

但是使用 UTF-8,可以在同一个文本中组合任意字符,但需要付出代价:UTF-8 是一种多字节编码。

仅对于 ASCII 字符(请参阅“man ascii”),UTF-8 使用与 ASCII 相同的单字节编码,因此 UTF-8 与 ASCII 兼容。

但是对于所有非 ASCII 字符,UTF-8 使用两个或多个字节,因此 UTF-8 与所有 ISO-8859 编码不兼容。

UTF-8 与 ISO-8859 不兼容导致意外结果

德语中表示二进制的词是“binär”,最后一个但倒数第二个字符是 ä(德语 a-umlaut)。

在 ISO-8859-1 和 ISO-8859-15 中,德语 a-umlaut ä 编码为十六进制字节值 0xE4,即八进制字节值 \0344。

在 UTF-8 中,德语 a-umlaut ä 编码为十六进制字节值 0xC3 0xA4,即八进制字节值 \0303 \0244。

根据区域设置环境,"wc" 工具可以计算不同数量的字符。

非 ASCII 字符 a-umlaut ä 作为八进制字节值的反斜杠转义序列 "\0nnn" 输入,因此可以使用任何键盘输入它,无论是否有 ä 键。此外,"\0nnn" 可以防止意外的键盘输入结果,请参阅“键盘输入取决于区域设置环境”部分。

user@host$ export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

user@host$ echo -en "bin\0344r" | wc --chars

 5

user@host$ echo -en "bin\0303\0244r" | wc --chars

 6

user@host$ export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

user@host$ echo -en "bin\0344r" | wc --chars

 4

user@host$ echo -en "bin\0303\0244r" | wc --chars

 5

“wc”工具只能在运行“wc”工具的区域设置环境与输入(此处为单词“binär”)的编码匹配时计算正确的字符数。

处理 UTF-8 比 ISO-8859 和 ASCII 慢

以下命令创建两个文件,每个文件包含 100000 行 ISO-8859-15 编码和 UTF-8 编码的德语二进制词“binär”。

然后,在适当的区域设置环境中运行“wc”工具,以计算每个文件中的正确字符数(也计算了标记每行结尾的换行符控制字符)。

user@host$ for i in $( seq 100000 ) ; do echo -e "bin\0344r" >>/tmp/text.iso885915 ; done

user@host$ for i in $( seq 100000 ) ; do echo -e "bin\0303\0244r" >>/tmp/text.utf8 ; done

user@host$ ls -l /tmp/text.*

 ... 600000 ... /tmp/text.iso885915
 ... 700000 ... /tmp/text.utf8

user@host$ export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

user@host$ time cat /tmp/text.iso885915 | wc --chars

 600000

 real    0m0.005s
 ...

user@host$ export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

user@host$ time cat /tmp/text.utf8 | wc --chars 

 600000

 real    0m0.050s
 ...

“cat 的有用用法”确保“wc”从管道获取其输入,以便“wc”只有纯输入来计算字符数,而不能“作弊”通过使用其他信息源(例如,在单字节编码的情况下,文件大小)。

在此示例中,计算 UTF-8 文件中的正确字符数比计算 ISO-8859-15 文件中的正确字符数慢 10 倍。

实际数字在很大程度上取决于特定的系统、特定程序的特定版本以及特定程序使用的系统库的版本。

一个次要原因是 UTF-8 文件比 ISO-8859 文件大,因为 UTF-8 是一种多字节编码。

在上面的示例中,UTF-8 文件比 ISO-8859 文件大约 17%,因此这并不是处理 UTF-8 文件慢 10 倍的主要原因。

主要原因是 UTF-8 多字节编码比 ISO-8859 和 ASCII 单字节编码更复杂。

因此,处理 UTF-8 比处理单字节编码更复杂,因此处理 UTF-8 比处理 ISO-8859 和 ASCII 需要更多的计算时间。

屏幕上显示的内容还取决于字体

屏幕上显示哪些字符取决于区域设置环境,还取决于显示字符的工具使用的字体。

特定字体包含特定字符的特定字形。字形是字符的图形表示。例如,同一个字符 'A' 可以显示为各种字形,例如

  • A(普通)
  • A(粗体)
  • A(斜体)
  • A(粗体和斜体)

“xlsfonts”命令列出当前正在运行的 X 服务器可用的所有字体(通常有数千个字体)。例如

xlsfonts -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso8859-15"

列出当前可用的所有具有“fixed”样式的 ISO-8859-15 字体。

在以下命令中,需要使用十六进制字节值为 0x27 的 ASCII 字符 ' (撇号) 作为单引号。不要将其与外观相似但不同的字符混淆,例如 ASCII 字符 ` (重音符) 或 ISO-8859-1 字符 ´ (急性重音) 或各种其他重音字符,例如 ISO-8859-7 字符 ΄ (希腊音调符号)。即使字符(更准确地说,是字符的字形)在屏幕上看起来完全相同(这取决于使用的字体),但如果它们的字节值不同,则这些字符也是不同的。

这些命令

export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso8859-15" -e "echo -e 'bin\0344r' ; sleep 9"

启动一个在 ISO-8859-15 环境中使用 ISO-8859-15 字体的 xterm 窗口,该窗口将按预期显示 ISO-8859-15 编码的单词 "binär"

binär

相反,这些命令

export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso8859-15" -e "echo -e 'bin\0303\0244r' ; sleep 9"

也会启动一个在 ISO-8859-15 环境中使用 ISO-8859-15 字体的 xterm 窗口,该窗口将显示 UTF-8 编码的单词 "binär" 作为

binÀr

如果使用的字体与区域设置环境不匹配,例如在

export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso8859-1" -e "echo -e 'bin\0303\0244r' ; sleep 9"

这些命令启动一个在 ISO-8859-15 环境中,但使用 ISO-8859-1 字体的 xterm 窗口,它将显示 UTF-8 编码的单词 "binär" 作为

binär

要正确显示 UTF-8 编码的单词,xterm 必须在与单词编码匹配的区域设置环境中运行,并且 xterm 必须使用与 xterm 运行的区域设置环境匹配的字体。例如,UTF-8 在 ISO-10646-1 中定义,因此 ISO-10646-1 字体与 UTF-8 区域设置环境匹配

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso10646-1" -e "echo -e 'bin\0303\0244r' ; sleep 9"

显示

binär

相反,这些命令

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso10646-1" -e "echo -e 'bin\0344r' ; sleep 9"

可能会显示

bin?

当应在 UTF-8 区域设置环境中使用 UTF-8/ISO-10646-1 字体显示 ISO-8859-1 或 ISO-8859-15 编码的单词 "binär" 时。

如果存在 UTF-8 中不可能出现的字节序列,则程序的行为未定义。在这种情况下,程序可能会显示类似 ? 字符的非法 UTF-8 字节序列 "\0344r"。可以使用以下方法测试此功能:

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso10646-1" -e "echo -e 'bin\0344rXX' ; sleep 9"

这可能会显示

bin?XX

如果您的区域设置环境与文本的编码不匹配,或者您的字体与您的区域设置环境不匹配,那么您是否获得完全相同的结果并不重要,因为在这些情况下,特定版本的特定程序如何执行是实现细节。

只有当您的区域设置环境与文本的编码匹配,并且您的字体与您的区域设置环境匹配时,您才能获得与您的期望匹配的结果。

键盘输入取决于区域设置环境

假设您想输入德语单词 "binär"(二进制)。

当键盘上有一个标有 a-umlaut ä 的键时,这取决于当前的区域设置环境,按下 ä 键会产生哪些字节值,即按下 ä 键时应用哪个 a-umlaut ä 字符的编码。

要在 ISO-8859-15 编码中输入带有 a-umlaut ä 字符的单词 "binär",请启动一个在 ISO-8859-15 环境中使用 ISO-8859-15 字体的 xterm 窗口

export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso8859-15" &

在此 xterm 窗口中键入(特别是不要从其他地方复制和粘贴单词 "binär",否则可能会得到意外的结果)

export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

echo -n "binär" >/tmp/somefile

od -t x1 /tmp/somefile

xterm 窗口中的 "export LC_ALL=...; export LANG=..." 是必要的,以确保从 xterm 内部启动的程序的区域设置环境(此处为 "echo" 和 "od")与 xterm 本身运行的区域设置环境匹配。

“od” 工具以十六进制数字的形式转储文件中的字节值,从而产生此输出

62 69 6e e4 72

这些十六进制数字代表 ISO-8859-15 编码中单词 "binär" 的字符。

要在 UTF-8 编码中输入带有 a-umlaut ä 字符的单词 "binär",请启动一个在 UTF-8 环境中使用 UTF-8/ISO-10646-1 字体的 xterm 窗口

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso10646-1" &

在此 xterm 窗口中输入(特别是手动键入 "binär")

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

echo -n "binär" >/tmp/somefile

od -t x1 /tmp/somefile

现在 "od" 产生此输出

62 69 6e c3 a4 72

这些十六进制数字代表 UTF-8 编码中单词 "binär" 的字符。

文件名中的非 ASCII 字符

如果您真的想惹麻烦:在文件名中使用非 ASCII 字符。

由于键盘输入取决于区域设置环境,并且屏幕上显示的内容还取决于字体,因此使用非 ASCII 字符作为文件名是一种容易让人发疯的方法。

使用以下命令启动一个 xterm 窗口

export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso8859-15" &

在此 xterm 窗口中输入(特别是手动键入 "binär")

export LC_ALL=en_GB.iso885915 ; export LANG=en_GB.iso885915

touch /tmp/binär

ls /tmp/binär

正如预期的那样,“ls” 产生以下输出

/tmp/binär

使用不同的区域设置启动第二个 xterm 窗口

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

xterm -fn "-*-fixed-*-*-*-*-*-*-*-*-*-*-iso10646-1" &

在第二个 xterm 窗口中输入(特别是手动键入 "binär")

export LC_ALL=en_GB.utf8 ; export LANG=en_GB.utf8

ls /tmp/binär

现在 "ls" 产生以下输出

ls: cannot access /tmp/binär: No such file or directory

原因是磁盘上的文件名 /tmp/binär 以 ISO-8859-15 编码存储为这些字节值(十六进制数字)

2f 74 6d 70 2f 62 69 6e e4 72

但在第二个 xterm 窗口中,文件名 /tmp/binär 对于 "ls" 命令以 UTF-8 编码输入为这些字节值(十六进制数字)

2f 74 6d 70 2f 62 69 6e c3 a4 72

并且两者不匹配。磁盘上没有名为 "2f 74 6d 70 2f 62 69 6e c3 a4 72" 的文件。磁盘上的文件名为 "2f 74 6d 70 2f 62 69 6e e4 72"。

由于磁盘上没有名为 "2f 74 6d 70 2f 62 69 6e c3 a4 72" 的文件,因此可以在第二个 xterm 窗口中创建它(手动键入 "binär" 以获得 UTF-8 编码的文件名)

touch /tmp/binär

因此,现在有两个具有相同文件名字符的独立文件

/  t  m  p  /  b  i  n  ä  r

它们以不同的文件名字节值存储在磁盘上。

对于操作系统,文件名只是一个字节序列,不包含任何关于此字节序列代表哪些字符的额外信息,如上所述:没有“纯文本”之说。

一些关于怪异的补充说明

在文件名中使用空格来愚弄他人

touch '/tmp/lostinspaces '

touch '/tmp/lostinspaces  '

ls /tmp
...
lostinspaces 
lostinspaces  
...

探索使用各种非 ASCII(最好是 UTF-8/Unicode)空格字符的奇妙机会!;)

在用户名和密码中使用非 ASCII 字符来锁定自己

除了您的键盘输入取决于您的区域设置环境之外,最终您的输入是否被识别为有效的用户名和密码还取决于处理您的输入的后续工具和服务的区域设置环境。

为登录成功或失败而激动吧!;s

另请参阅 https://openprinting.github.io/cups/faq.html,其中摘录如下

Unicode 密码在 Web 浏览器、超文本传输协议 (HTTP) 以及 UNIX 中都支持不足。许多浏览器只是在 8 位 (!) 处截断密码字符,并且无法知道操作系统提供的可插拔身份验证模块 (PAM) 使用哪种字符集。因此,今天无法可靠地支持 Unicode 密码。

同样也适用于 Unicode 用户名,但各种文档解释了哪些字符可以或应该用于用户名。例如,POSIX 要求用户名中的字符仅来自所谓的“可移植文件名字符集”,但“man useradd” 更加严格

用户名必须以小写字母或下划线开头,后跟小写字母、数字、下划线或连字符。它们可以以美元符号结尾。用正则表达式表示:[a-z_][a-z0-9_-]*[$]?

因此,用户名中的非 ASCII 字符无效。

在技术值(如 URL)中使用非 ASCII 字符来帮助攻击者

虽然 ASCII 中的同形异义词相对容易看到,但 Unicode 中的同形异义词超出了肉眼识别的范围。通过在技术值(如 URL)中使用 UTF-8/Unicode,您的用户必须使用这些值,然后攻击者可以利用同形异义词(参见 https://en.wikipedia.org/wiki/IDN_homograph_attack)。

很高兴这些攻击不是您的问题,而是您用户的问题!:s

总结

特别是由于 ASCII 和 ISO-8859 编码中一个字符的字节数(一个字符一个字节)与 UTF-8(一个或多个字节一个字符)不同,并且由于现在默认设置了 UTF-8 区域设置环境,因此,如果您在运行程序之前没有设置与“纯文本文件”编码匹配的区域设置环境,则可能会获得任何奇怪的结果。

但是,仅设置匹配的区域设置环境可能不足以获得预期的正确结果。例如,在屏幕上输出的情况下,字体也必须与区域设置环境匹配。

参见