请教, Python3 的 str 底层是用什么编码储存的?

2016-01-02 17:11:22 +08:00
 fy
感觉 utf32 有时候代价太大,

utf8 的话,分片不便,可能代价更大……

没读过源码,求知道的告知一下
5322 次点击
所在节点    Python
19 条回复
qnnnnez
2016-01-02 18:19:00 +08:00
utf8
lcj2class
2016-01-02 18:55:13 +08:00
https://docs.python.org/3/howto/unicode.html#the-string-type

从这里看应该是 utf8 ,但是不确定,做了下面的实验,

import pickle
a = "中国人"
with open("C:/Python33/a.txt", "bw") as f:
pickle.dump(a, f)

然后找个能够查看二进制的编辑器,可以看到“中国人”被保存成了 utf-8 的

e4b8ad ,中
e59bbd ,国
e4baba ,人
fy
2016-01-02 19:08:59 +08:00
@lcj2class WOW ,这个办法好。
timonwong
2016-01-02 19:25:47 +08:00
https://docs.python.org/3/howto/unicode.html#the-string-type
这里描述的是 source encoding (python 源文件默认 utf-8 编码)

这里是从 python3.3 开始的内部结构(PyUnicodeObject )
https://www.python.org/dev/peps/pep-0393/
比较奇葩就是了
ruoyu0088
2016-01-02 20:59:29 +08:00
pickle 无法 dump 对象内存中的真正的值,可以使用 ctypes 直接访问对象内存:

import ctypes
a = "中国人"
import binascii

binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))

输出为:

b'0200000000000000a073c93ba17f000003000000000000008a3d25ed4d04cf49a86ac83ba17f000000000000000000000000000000000000000000000000000000000000000000002d4efd56ba4e0000'

最后那部分是保存字符的,每个字符 2 个字节。

而 a = "abcd"的输出为:

b'0200000000000000a073c93ba17f00000400000000000000c687538778475d60e57fc83ba17f000000000000000000006162636400'

每个字符一个字节。
lcj2class
2016-01-02 21:19:55 +08:00
@ruoyu0088

那这样说的话,就是 UTF-16 存储的了。你能找到源码对应位置吗?我找了半天没找到
ruoyu0088
2016-01-02 21:27:44 +08:00
@lcj2class

不是 UTF-16 储存,是根据字符串的内容自动选择。

代码在 unicodeobject.c
congeec
2016-01-02 21:53:40 +08:00
@lcj2class 你这样测的是文件内容的编码,因为极有可能编译器是以 utf-8 编码打开文件的。把文件编码改成 GBK 再试一次看有没有异常
lcj2class
2016-01-02 21:59:00 +08:00
@ruoyu0088
https://github.com/python/cpython/blob/8a3d7944f8290f095e3c195dd4bafaed9e8e777a/Objects/unicodeobject.c

这段代码好长,先贴出来,后面慢慢看。

@congeec
不是的,我是以二进制的方式写的,和文件的编码没什么关系。
congeec
2016-01-02 22:02:49 +08:00
@lcj2class 你用 hex 编辑器看看文件内容,用 gbk 和 utf-8 分别写的时候 hex 值是不一样的。我说的不是 a.txt 而是你的源码文件
lcj2class
2016-01-02 22:11:08 +08:00
@congeec
确实是这样的, 学习了。🙏
fy
2016-01-03 00:16:10 +08:00
我去,原来如此复杂。
感谢楼上几位给出的资料和方法。

python 源码中这个文件有 15665 行,实在是让人望而生畏。
@ruoyu0088 提到编码是自动选择的,我结合读文档的理解和一些测试,
再次进行了一遍求证,最后得到的结论是:

python3 的字符串是根据输入确定编码,在 Latin1 , UTF16 、 UTF32 之间进行切换。


验证的过程是这样的:

1. 首先是 ucs2(utf16) 的情况 (据我所知 ucs2 与 u16 等价, ucs4 与 u32 等价,不知是否正确)

In [60]: a = '中文'

In [61]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
Out[61]: b'010000003094e51d0200000056f1a772a8000000f447c7000000000000000000020000002d4e87650000'

In [62]: binascii.hexlify('中文'.encode('utf-16'))
Out[62]: b'fffe2d4e8765'

我们可以看到,“中文”两个字在编码为 utf16 之后首先是 fffe 这个头部,随后的 2d4e 8765 分别对应两个字,这与从内存中弄到的字符串形态是相同的。


2. 那么我们在文本中加入一个 UCS2 表示不了的字符串呢?会怎么样?

In [64]: a = '\U000a1ffa 中文'

In [65]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
Out[65]: b'010000003094e51d03000000220bc0a8b000000000000000000000000000000000000000fa1f0a002d4e00008765000000000000'

我们可以看到, 2d4e 变成了 2d4e0000 , 8765 变成了 87650000 ,最后是 8 个 0 (一个 UCS4 字符)结尾。

而 fa1f0a00 是 000a1ffa 在内存中的形式(从右向左,每一个字节——即俩 HEX ——逐个倒装)

其实我觉得奇怪的地方在于, python 其实记录了文本的长度,为啥坚持 C 风格的字符串(末尾加\0 )?



看看这个字符串 encode 后的样子吧!

In [66]: binascii.hexlify('\U000a1ffa 中文'.encode('utf-32'))
Out[66]: b'fffe0000fa1f0a002d4e000087650000'

In [67]: binascii.hexlify('\U000a1ffa 中文'.encode('utf-8'))
Out[67]: b'f2a1bfbae4b8ade69687'

头部变成了 fffe0000 其他都一致。


3. 最后再看看单字节的字符串

In [68]: a = "\x9a\x9b"

In [69]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
Out[69]: b'010000003094e51d02000000b9bdd189a4000000000000000000000000000000000000009a9b00'

=====

In [70]: a = "\x9a\x9b 中"

In [71]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
Out[71]: b'010000003094e51d03000000a4036e4ba8656164f44dc7000000000000000000030000009a009b002d4e0000'

=====

In [72]: a = "\x9a\x9b 中\U000a1ffa"

In [73]: binascii.hexlify(ctypes.string_at(id(a), a.__sizeof__()))
Out[73]: b'010000003094e51d04000000776e9375b000005a000000000000000000000000000000009a0000009b0000002d4e0000fa1f0a0000000000'

果然不出所料。



说起来\U0000000 是一个比较少用的语法,专门用来转义 UCS4 的。
要不是前段时间搞了 tinyre 这个项目,我肯定是弄不出这样的字符的……
顺便宣传一下在下最近这个项目,一个正则引擎: https://github.com/fy0/tinyre
ruoyu0088
2016-01-03 09:20:03 +08:00
末尾加\0 是为了和 C 语言兼容,这样可以直接把字符串的地址传递给 C 语言的函数处理。
pynix
2016-01-03 12:20:31 +08:00
utf8
pynix
2016-01-03 12:22:05 +08:00
虚拟机层面 3.0 开始使用 Unicode ,后来为了性能优化又改成 utf8 了,好像是 3.3 来着。
lcj2class
2016-01-03 13:29:01 +08:00
之前就发现这里的坑了,之前总结过一次,现在更新了 Python 相关部分,可以参考

http://liujiacai.net/blog/2015/11/20/strings/#Python
fy
2016-01-03 15:53:33 +08:00
@ruoyu0088 也是吧,不过也只能管一部分。因为 python 字符串内部可以有\0 。总体来说付出一个字的位置还是值得的。

但想想还是很蛋疼啊,比如经常有人掺杂一两个诡异的字符在纯英文文本里面,整个文本就会被拉长 2-4 倍不等。

不过我想最常用的字符集也就是 UCS2 了吧,所以还能够接受的样子。
fy
2016-01-03 15:55:38 +08:00
对了,其实将\0 当作字符串末尾我觉得不是一个好的设计。

python 在写扩展的时候,字符串的姿势有好几种,包括这种直接的\0 为末尾,中间不能有\0 的字符串,以及给出长度的字符串等等。
qnnnnez
2016-01-12 19:14:56 +08:00
看了下代码,发现确实是 UCS1, UCS2, UCS4 三种。之前听许多人说是 utf8 ,就想当然地以为是 utf8 了。现在想想,如果用变长编码,那还会有许多问题。为自己之前的答案道歉。

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

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

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

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

© 2021 V2EX