自 url.el 关于 Multibyte text in HTTP request 的一件怪事 继续讨论:
(string-bytes (concat (symbol-name 'GET) (encode-coding-string "我" 'utf-8)))
;; => 9
GET 3 个 byte,「我」3 个byte,加起来 6 byte,Emacs 却说有 9 个。用的 ASCII 却有正常了:
(string-bytes (concat (symbol-name 'GET) (encode-coding-string "foo" 'utf-8)))
;; => 6
Emacs 好多函数明明 unibyte 就行,不知道为何返回 multibyte:
(multibyte-string-p (symbol-name 'GET))
;; => t
(multibyte-string-p "GET")
;; => nil
(multibyte-string-p (url-host (url-generic-parse-url "http://example.com")))
;; => t
使用 url.el 的时候会遇到这问题,有时 multibyte 没问题,有时又不行:
url-http-create-request: Multibyte text in HTTP request
最保险做法是保证 URL url-request-method url-request-extra-headers url-request-data 全使用 unibyte,但是这有可能比较烦人,上面提到 Emacs 有时会冷不丁地返回 multibyte,更简单的做法是只有在有 url-request-data 的时候,才把全使用 unibyte,因为除了 url-request-data 其它参数都不可能包含 UTF-8 字符。
Youmu
3
似乎都转换成utf-8之后就是正常的
(string-bytes (concat (encode-coding-string (symbol-name 'GET) 'utf-8)
(encode-coding-string "我" 'utf-8)))
;; => 6
1 个赞
嗯,都转换成 unibyte 之后是 OK 的,问题是不知道 、意识不到、想不通为啥 (symbol-name 'GET)
等等这也要转。
encode-coding-string decode-coding-string 我老是分不清了。
cireu
5
multibyte string(下称string)是字符串抽象,里面保存的是 字符 (character). unibyte string(下称bytes)是字符串编码后在计算机里的存储形式,类似其他语言的字节流。
乍一看可能不清楚,用seq-into
转化为vector或者list就能看明白
(seq-into "我能吞下玻璃而不伤身体" 'vector)
;; => [25105 33021 21534 19979 29627 29827 32780 19981 20260 36523 20307]
(seq-into (encode-coding-string "我能吞下玻璃而不伤身体" 'utf-8) 'vector)
;; =>
;; [230
;; 136
;; 145
;; 232
;; 131
;; 189
;; 229
;; 144
;; 158
;; 228
;; 184
;; 139
;; 231
;; 142
;; 187
;; 231
;; 146
;; 131
;; 232
;; 128
;; 140
;; 228
;; 184
;; 141
;; 228
;; 188
;; 164
;; 232
;; 186
;; 171
;; 228
;; 189
;; 147]
string转换成vector后,每个元素代表了该字符的码点(在Unicode字符集里的位置)。而bytes转换为vector,其元素就是用调用encode-coding-string
时所指定的编码形式进行编码后的字节。
Emacs的string使用的编码是utf-8-emacs
,其他编码的文件我们在打开时先转码成字符,保存时再编码成字节流写回硬盘。该编码的编码方式与utf-8一致,编码“emacs”字符集。“emacs”字符集是“unicode”字符集的超集,开辟了#x110000..#x3fffff
来一些额外字符。[^1][^2][^3]其中#x3fff00..#x3fffff
是非ASCII编码范围的裸字节的字符表示形式(128-255
)
为什么要给裸字节赋予字符码位?我们知道,UTF-8编码并非稠密的,对于任意字节流,我们不一定能将其按UTF-8编码解析为合法字符串。这块额外字符定义就是为了给无法解析为Unicode character的漏网之byte一个身份(像暂住证),使string能正常生成。
或许看起来不太合理,我们更喜欢bytes和string能分开成两个类型(或许用#b"\200\201"
这样的reader syntax来表示bytes),对于非法byte转string我们或许希望直接error。但是别忘了Emacs是有40年历史的骨灰级软件,存在出于历史局限性上的设计不完美也无可厚非[^4]。
回到这个问题上,这个问题的本质就是concat
在连接string和bytes时,会把bytes强转成string,而且不经任何编码,因为Emacs reader在读取字符串时,会尽量读取成unibyte string(bytes)[^5],除非遇到真正的multibyte character(如汉字)。对于unibyte string是ASCII字符的情况,这样做是符合UTF-8编码的。而如果unibyte里含有raw byte,就会直接使用#x3fff00..#x3fffff
的raw byte码位。
再用seq-into
大法。
(seq-into (concat (symbol-name 'GET) (encode-coding-string "我" 'utf-8)) 'vector)
;; => [71 69 84 4194278 4194184 4194193]
(seq-into (concat "GET" (encode-coding-string "我" 'utf-8)) 'vector)
;; => [71 69 84 230 136 145]
在这里(symbol-name 'GET)
返回一个multibyte string,因为Emacs内部就是保存为multibyte string,因此大意了,没有转换。而4194278
4194184
4194193
都是一个22bit的数字,加上padding,约为3个bytes,因此string-bytes结果返回为9. 同理第二个因为是ASCII字符,默认读取成unibyte,concat后依然是unibyte,不转换为character,返回6.
-
Text Representations (GNU Emacs Lisp Reference Manual)
-
对于#x110000..#x3fff00
这段码位的用途,猜测是用来处理类似GB18030
对Unicode的奇怪映射规则用
-
emacs/mule-conf.el at master · emacs-mirror/emacs · GitHub
-
事实上,像Java和C++的字符串设计也饱受诟病,Java11以前的char是一个UTF-16编码的双字节,
没法处理3字节的字符,因此被迫引入“代理对”这种概念。C++ STL的std::string
则完完全全是一个字节流,对字符串相关的处理毫无帮助。而新出的语言则学乖的不少(Go除外)
-
emacs/lread.c at master · emacs-mirror/emacs · GitHub
10 个赞
cireu
6
1 个赞
是把230
这个code point的字符转成了4194278
?byte转string是如何产生这个结果的?
然后 1+1+1+3+3+3=12?为什么答案是9?
cireu
8
(string-bytes (string 4194278))
;; => 2
(string-bytes (string 4194184))
;; => 2
(string-bytes (string 4194193))
;; => 2
当时写的时候这里有点想当然了,这里应该是2 bytes
在mule-conf.el L80里定义的,在这里 emacs/src/character.h at master · emacs-mirror/emacs · GitHub 实现。具体执行转换是concat执行的哈
BYTE8_TO_CHAR (int byte)
{
return byte + 0x3FFF00;
}
是什么魔法……不过还真是这么算出来的,230+0x3FFF00=4194278
分配给 8bit 字节的代码空间是 0x3fff80~0x3fffff: