unicode和utf8 —— 从一个遍历文件名的脚本,谈谈对Python2和Python3中字符编码差异的理解

对编码问题一直一知半解,之前也是得过且过,正好有个同事要我帮忙写个脚本,涉及这方面的问题,借这个契机研究了一下.

先贴几篇比较好的:

1.阮老师的上古文章(07年…),虽然古老但对理解帮助很大,从最基础讲起,逻辑清晰易理解. (ps: 阮老师的博客都有此特点, 在这里推荐一波, 从js到linux, 精通前后端, 是可以当文档看的博客): http://www.ruanyifeng.com/blo…

2.最好看了上一篇再看这篇(解释了py2中为什么不能用 setdefaultencoding): https://blog.ernest.me/post/p…

3.关于UnicodeDecodeError: https://stackoverflow.com/que…

以及Python3的官方文档:https://docs.python.org/relea…

=============================================================================
建议以上几篇理解的差不多后再看正文:

简单说一下:
2.x中的编码概念是不够清晰的,str类型的对象会被赋予默认编码,且既可以对其编码又可以对其解码(单这一点就足够造成很多混乱…),而我们在代码中常直接使用带编码的str进行os库相关的操作,就容易导致很多问题。对于python内部来说,解释器处理操作系统的文件目录相关的东西时,必须使用unicode。新手如果要读取文件名并进行一些处理时,经常遇到乱码,以及windows和linux下效果不同的问题。另外一个主要场景就是stream,流处理,这个就是写文件或者前后端通信之类,这个相对前面问题来说其实还算好处理的。然后还有字符串拼接。

3.x去掉了 unicode类型 和 unicode()函数,(也就没有u'xxx'这种写法了),区分出str类型和bytes类型,而且str不再同时有encodedecode方法,bytes只有decodestr只有encode
3.x中,没有了unicode这个类型,可以理解为str成为了unicode类型,"All text is Unicode"。而带编码的字符串则由bytes类型来处理。但也不能简单地理解为3.x的str和bytes分别对应2.x的unicode和str。

所以2.x处理字符串原则其实也很简单,就是把str当成bytes,内部只用unicode,外部进的和出的都编码成str。

这里可能有个疑问就是,按之前的理解(假设已经读了第1篇)unicode是编码规则,但不是存储方式,uft8才是它的实现,才能用来存储,那么如果python内部是用unicode方式处理文本,在内存中python解释器如何正确读取字符呢?这里要理解清楚所谓实现,其实多的就是一个字节数的信息,unicode和utf8本质上都是一串0和1,只是缺一个字节数量的区分,即,从信息量上来说: unicode + 自身长度 = utf8。这样,在python解释器的处理过程中,python自然有办法用自己的标记来正确读写“自身长度”这个信息,因为这里不需要和外界交互,不需要类似utf8这样的约定规则,自己内部能正确获取信息即可。utf8是为了省硬盘空间,内存中不太需要这样的东西。(这段属于个人想当然的理解,仅供参考)

重点,重点,重点,贴一下py2中处理编码的原则(摘自上面第3篇),也就是我上面那句总结的完整版,如果你理解了为什么有这个原则说明差不多理解了py2的编码:

·所有 text string 都应该是 unicode 类型,而不是 str,如果你在操作 text,而类型却是 str,那就是在制造 bug。
·在需要转换的时候,显式转换。从字节解码成文本,用 var.decode(encoding),从文本编码成字节,用 var.encode(encoding)。
·从外部读取数据时,默认它是字节,然后 decode 成需要的文本;同样的,当需要向外部发送文本时,encode 成字节再发送。

除了上面几篇,百度还有无数其他的讲解,本篇就不再赘述原理之类的,上脚本讲下实际应用,脚本功能是递归遍历目录下所有文件:

#-*- coding:utf-8 -*-
'''
Description :  
递归遍历目录下所有文件(排除目录),并逐行写入到指定文件中。
可以分别用py2或py3来执行,结果相同。
可以不带参数,或者 python xxxx <path> <writepath>

主要干两件事:
第一步,把文件路径解码成unicode,传给os用来遍历 (仅py2)
第二步,把文件名编码后写入文件
这样正好覆盖了上面提到的两个主要场景。
'''

''' 
Python2: str -> (decode) -> unicode -> (encode) -> str
Python3: bytes -> (decode) -> str(unicode) -> (encode) -> bytes 
'''

import sys
import os

try:
    PATH = sys.argv[1]
except IndexError:
    # 在这里写一个你能找到的名字最乱,里面文件名最杂的文件夹
    PATH = r'./'  # raw string, 表示不进行转义, 如果复制一个带反斜杠后面带数字或字母的路径, 不加上这个r就会出错
    
try:
    WRITE_PATH = sys.argv[2]
except IndexError:
    WRITE_PATH = 'abc'   # 指定要写入的文件名


PY2 = sys.version.startswith('2')


if PY2:
    # 不理解编码的人经常用这个当做万能药,这个确实也有用,但严重不推荐使用,见第3篇
    # import sys
    # reload(sys)  
    # sys.setdefaultencoding('utf8')
    # PATH = PATH.decode()  # 这样就默认以utf8解码,由于上面的代码导致传进来的PATH会被默认编码为utf8


    # 记住原则,在python内处理文本字符串,永远保证是unicode类型,所以这里要进行解码。关于'ignore'参数见第4篇
    # 这里PATH不带中文时,无论哪种都会默认为ascii编码,带其他非ascii文字时,根据来源如果是:
    # 1. sys.argv传入,那么PATH的编码跟操作系统有关。如果传一个中文,windows下和linux下编码分别是ISO-8859-1和utf8,可以自己用chardet打印看看
    # 2. 文件中写死,本来理解是跟这个文件本身编码有关,但文件编码同样是utf8的情况下,windows下打印了Windows-1252(ISO-8859-1的超集),linux下仍然是utf8。所以还是跟操作系统有关
    # 这里默认在linux系统下执行,所以直接用utf8解了,如果要兼容,可以用chardet获取编码类型后指定进行解码
    PATH = PATH.decode('utf8', 'ignore')

# if PY3,无论传入还是写死PATH都将会是```str```类型,当然也就不需要也不能进行解码啦


def getf(path):
    l = []
    res = os.listdir(path)
    for each in res:
        subpath = os.path.join(path, each)
        if os.path.isdir(subpath):
            l.extend(getf(subpath))
        else:
            l.append(each)

    return l

res = getf(PATH)



if PY2:
    # Python2, 由于py2中概念的模糊, 可以直接用'w'打开去写,而不需要'wb'
    # 不过不编码成utf8的话也是会抛UnicodeDecodeError的,写文件需要编码这个原则py2还是有的。可以检查一下 "%s\n" % each 的类型毫无疑问是unicode
    with open(WRITE_PATH, 'w') as f:
        for each in res:
            f.write(("%s\n" % each).encode('utf8'))

else:
    # Python3, 可以用w打开然后不编码直接写string(即unicode),也是可以成功写的,不过那样结果是非ascii都乱码。
    # 而编了码就转为了bytes类型,所以Python3想正确实现就必须用二进制方式打开 (wb)
    # 如果打开方式和写入类型不对应,会抛TypeError,很明确
    with open(WRITE_PATH, 'wb') as f:
        for each in res:
            f.write(("%s\n" % each).encode('utf8'))

总结下代码,首先可以看到py3是没毛病的,对编码的操作概念清晰,没有任何困扰。

py2这块确实有硬伤,虽然很多时候也无所谓,但在要严谨或者专门处理编码的代码中,一定要记住开头贴的原则。

关于setdefaultencoding,除非实在不重要的场景,又需要临时简单解决,可以凑合一下,普通场景不建议用.
原因第3篇解释得很清楚。另附官方文档的说明如下:
set the current default string encoding used by the Unicode implementation. If name does not match any available encoding, LookupError is raised. This function is only intended to be used by the site module implementation and, where needed, by sitecustomize. Once used by the site module, it is removed from the sys module’s namespace.

    原文作者:nifhlheimr
    原文地址: https://segmentfault.com/a/1190000013199341
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞