坑爹的 GBK:大家都应该去用 UTF-8

124 天前
 mikewang

最近在把我用 C 写的一批 Linux 工具移植到 Windows 上,在字符编码上遇到了大坑。


举个简单的例子:数文件层级。

在 Linux 上,我们数斜杠数量就好。

在 Windows 上,再加上反斜杠,应该就好了。——我是这样想的。

#include <stdio.h>

int main(int argc, char *argv[]) {
    int level;
    const char *p;

    if (argc < 2) {
        return 1;
    }

    for (level = 0, p = argv[1]; *p; p++) {
        if (*p == '/' || *p == '\\') {
            level++;
        }
    }

    printf("%d\n", level);

    return 0;
}

用 MinGW 的 GCC 编译一下,然后跑几个用例:

gcc -o getlevel.exe getlevel.c
C:\>getlevel C:\浙江省\宁波市\北仑区\小港街道.txt
4

C:\>getlevel C:\浙江省\宁波市\北仑区\大碶街道.txt
5

天塌了,这么简单的代码竟然出了 bug 。


原来 的 编码是 {0xb4, 0x5c},其中 0x5c 和反斜杠的 ASCII 编码一模一样。

GBK 的第一字节兼容 ASCII ,但第二字节的范围是 0x40 ~ 0xfe,与 ASCII 的 0x00 ~ 0x7f 重叠。BUG 就这么诞生了。

UTF-8 没有这个问题的原因是:只要字节范围在 0x00 ~ 0x7f,那么就一定是 ASCII ,因为后续字节都避开了这个范围。虽然中文编码比 GB 系列长了,但是这个设计确实省了很多事。包括 strstr() strcmp() 之类的都不会出现奇奇怪怪的 bug 。


或许我应该使用 wmain() 然后获取 wchar_t,但是 wmain() 是 Windows 特有的东西,这样做就没法和 Linux 公用同一套代码了。目前加上了 mbtowc() 作为修复。原本简洁的代码变得十分复杂:(

说到这又不得不吐槽下 Windows 的各种奇怪 API 了,不知道它是如何存活到现在的...

12255 次点击
所在节点    C
99 条回复
sagaxu
124 天前
早年还有比 GBK 更坑爹的 GB2312 ,某个大领导的“镕”字不在 GB2312 范围中
minami
124 天前
这代码给人看乐了,学艺不精也能算 bug ,就跟 tcp 粘包侠一样可乐
adoal
124 天前
Windows 老老实实用 wide 版本的 API ,不要用 C style stringsc 处理和 OS API 相关的字符串,不要听什么 utf8everywhere 的鼓吹。
geelaw
124 天前
@hwdq0012 #9 任何设计之初就没有打算适配 Windows ME 或者更低版本的 Windows 的 Windows 软件不用 UTF-16 调用 Windows API 都是自始错误的设计,因为 Windows NT 系列的最初版本 (3.1) 就是使用 UCS-2 (后来改为 UTF-16 )作为原生字符串表示的。

@w568w #12 有必要提示其他读者:Unicode 码点的官方名字是 code point ,使用 rune 这种字母类型名字称呼 code point 似乎是 Go 引起的一种不必要的时尚潮流,而且这种时髦感也被 .NET 团队吸收了。
kirory
124 天前
std::filesystem::path {argv[1]}
AoEiuV020JP
124 天前
c/c++折腾跨平台就是很麻烦, 最近有一些代码需要跨平台编译出动态库, 纠结许久还是放弃 c/c++改用 go ,代价就是动态库大了一些,但代码真的很省心,很现代,
w568w
124 天前
@geelaw #24 是的,这里 [1] 也有人讨论这个问题。我用 rune 是因为我最常写的 Dart 里也吸收了这个名词。

[1] https://learn.microsoft.com/en-us/answers/questions/2085971/why-is-system-text-rune-named-like-this
1BF6oSYCD9ngBHo1
124 天前
没有人提到 C23 的 char8_t 吗,最近学 C23 看到个大量采用这个的库 https://github.com/micl2e2/mcpc ,震惊! C 里面也可以全程 UTF8 !
yk000123
124 天前
偏个题。不能用斜杠、反斜杠数量来判断文件目录层级。首先 Linux 里有`.`,`..`,其次同一个文件的相对路径和绝对路径的斜杠数量也可能不同。还有,Windows 里我不清楚,但只是 Linux 里,`/path/to/file`和`//path////to///file`指向的是同一个文件。
aloxaf
124 天前
@vinle 这不是 utf8 吧,只是单独提出了一个类型用来表示 unicode code unit ,语义上更明确了,但没有任何编码信息
mikewang
124 天前
@geelaw #5
@yk000123 #29

抱歉,其实是因为完整的代码逻辑很长,这里是我随手举的例子,没有完全说明清楚。传入的路径是标准化后的绝对路径(如 realpath() 处理后的字符串),所以不考虑 ./ ../ // 等情况了。移植到 Windows 上是做了 #ifdef _WIN32 处理的, Linux 上不做反斜杠判断。

@geelaw #6
Linux 上确实可以不是 UTF-8 ,正如中文 Windows 上也不一定是 GBK (可以手动改成实验状态的 UTF-8 ),但可以认为已经成为了事实上的标准。绝大多数用户使用默认配置就是这种情况了。

@w568w #12
在 UTF-8 上应该是可靠的(只要不是去数字符数的话)。这里的困境是:我也知道有问题,但是似乎没有办法简单解决。正如需求就是简单的数斜杠,那么真的需要引入一个 Unicode 库吗,其实我自己也是怀疑的(?)
另外 mbtowc(),wc 是 widechar 吧,不是 NULL 空终止。

@minami #22
其实是说我的代码有 BUG 啦,这个代码确实学艺不精,其实我也想知道 *应该* 怎么写,或许你也可以举个例子 hhh 这是很多人都会犯的错误。但在 UTF-8 ,它是允许你这么遍历的。一个是方便我这种懒人,二是让那些欧美地区人写的这类代码也能正常跑在中文上。
比如说 strstr() 找子串,GBK 是用不得的。utf-8 在不引入第三方库下就能这么找,是不是挺省事?;)
lisxour
124 天前
@yk000123 其实楼主的代码加上相对路径的识别就好了,说白了缺少三种特殊处理,“.”、“..”、空白,经常和路径打交道的,这三种特殊情况,第一行 if 就开始处理了
aloxaf
124 天前
发现目录层级这玩意儿还是有些门道的

`..` 其实不能被删掉,也就是说 a/c 和 a/b/../c 并不等价,因为 b 可能是一个符号链接,此时它的父目录就不是 a 。

Rust 和 Python 的实现都是正确的,只会删掉多余的 `/` 和 `.`,并且在文档中强调了这一点
Go 和 NodeJS 都会把 `..` 也删掉,但 NodeJS 提到了它的行为并不严格遵守 POSIX 规范
geelaw
124 天前
@mikewang #31 一个中国生活的、使用 Windows 二次元爱好者,很可能分区是 NTFS 格式,同一个文件的文件名里既有中文,又有日语。此时无论用户的代码页是 936 (简体中文) 还是 932 (日语) 都无法通过非 Unicode API 访问此文件。
minami
124 天前
@mikewang 字符编码方式永远都是 trade off 的艺术,你不能光看一项优点,就忽略了它其他方面的缺点,GBK 作为定长编码,相比变长编码还是有独到的优势的。而且默认字符编码这个问题,尊重平台特性,尊重历史兼容性,才是正确的,就像 Apple 拼尽全力,也无法彻底去掉大小写不敏感一样
mikewang
124 天前
@geelaw #34 其实 936 是包含了平片假名的,只要没有生僻字勉强还行(
所以我也很好奇其他 posix 程序是怎么移植过来的,毕竟大多数 API 都是 char *,到最后一步再转成 LPCWSTR 么,好像也有问题。

好在 Windows 10 1903 往后可以通过 manifest 指定 code page 为 UTF-8 (65001)了,以后 ANSI API 应该还有发展空间:
https://learn.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page
bbao
124 天前
那个啥,我冒失了,原谅我,楼主是个 14 岁的初中生~~~~~~~~~~~~~ 是我不够 nice 。
mk3s
124 天前
@bbao 这就有点尬黑了,tomcat 只是集成了,啊不,人家是进化了(
mikewang
124 天前
@bbao 别啊,我现在工作了,虽然时间不长。或许是看到了我的历史帖子,那是我注册 v 站的十周年纪念,不是说今年(
geelaw
124 天前
@mikewang #36 我印象里见过 -U8 结尾的 Win32 API ,用这个比设置代码页为 65001 之后用 -A 要好,当然,-U8 和 -A 在面对目前的文件系统时,都不如 -W 好。

-A 属于为了兼容性维持的 API ,内部操作都是转换为 UTF-16 之后调用 -W 的;我的理解是允许 manifest 设置 65001 是为了让 POSIX 程序最初的移植容易一点,而非作为主要存在的形式。

因为文件系统的路径并不需要是合法的 UTF-16 ,所以直接用 -W 和文件系统交互依然是惟一正确的选择。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1133223

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX