V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
fy
V2EX  ›  Python

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

  •  
  •   fy ·
    fy0 · 2016-01-02 17:11:22 +08:00 · 5312 次点击
    这是一个创建于 3036 天前的主题,其中的信息可能已经有所发展或是发生改变。
    感觉 utf32 有时候代价太大,

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

    没读过源码,求知道的告知一下
    第 1 条附言  ·  2016-01-03 00:19:44 +08:00
    结论汇总在 12 楼,感谢大家,又学到了新姿势。
    19 条回复    2016-01-12 19:14:56 +08:00
    qnnnnez
        1
    qnnnnez  
       2016-01-02 18:19:00 +08:00
    utf8
    lcj2class
        2
    lcj2class  
       2016-01-02 18:55:13 +08:00   ❤️ 1
    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
        3
    fy  
    OP
       2016-01-02 19:08:59 +08:00
    @lcj2class WOW ,这个办法好。
    timonwong
        4
    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
        5
    ruoyu0088  
       2016-01-02 20:59:29 +08:00   ❤️ 1
    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
        6
    lcj2class  
       2016-01-02 21:19:55 +08:00
    @ruoyu0088

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

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

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

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

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

    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
        13
    ruoyu0088  
       2016-01-03 09:20:03 +08:00
    末尾加\0 是为了和 C 语言兼容,这样可以直接把字符串的地址传递给 C 语言的函数处理。
    pynix
        14
    pynix  
       2016-01-03 12:20:31 +08:00
    utf8
    pynix
        15
    pynix  
       2016-01-03 12:22:05 +08:00
    虚拟机层面 3.0 开始使用 Unicode ,后来为了性能优化又改成 utf8 了,好像是 3.3 来着。
    lcj2class
        16
    lcj2class  
       2016-01-03 13:29:01 +08:00
    之前就发现这里的坑了,之前总结过一次,现在更新了 Python 相关部分,可以参考

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

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

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

    python 在写扩展的时候,字符串的姿势有好几种,包括这种直接的\0 为末尾,中间不能有\0 的字符串,以及给出长度的字符串等等。
    qnnnnez
        19
    qnnnnez  
       2016-01-12 19:14:56 +08:00 via iPhone
    看了下代码,发现确实是 UCS1, UCS2, UCS4 三种。之前听许多人说是 utf8 ,就想当然地以为是 utf8 了。现在想想,如果用变长编码,那还会有许多问题。为自己之前的答案道歉。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   4206 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 72ms · UTC 01:01 · PVG 09:01 · LAX 18:01 · JFK 21:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.