python中理解字符串和编码为什么这么难

大数据

在学习python2的时候,字符串和编码可以说是最让人困惑的知识点,假如知其然而不知其所以然,则在后续的写代码和学习过程中会让人很痛苦,甚至会放弃,而对比PHP语言来说,即使完全不了解编码等知识,也可以写出代码,这是幸事,但反过来说太透明会让你失去很多能力.

python2字符串和编码难理解的原因在于,一方面很多书籍很少说这方面的知识,另外一方面是python设计导致的,编码问题和文件编码,系统环境,IO操作等都有关系,混杂在一块很让人头疼.

网络上也有很多中文资料去说明,但是在学习的时候只能借鉴,原因在于写的人理解的也是比较片面,很容易误导人,所以在学习过程中一定要去实践,要仔细琢磨.

自己综合学习了下,以自己的方式写了篇博客,能力有限,希望不要误导人.

编码

对于编码个人觉得理解概念即可,具体的转换规则,存储规则可以不用太仔细了解,这类似于进制,知道概念即可,不强制掌握进制转换的方法.

讲编码的文章很多,掌握以下概念即可.

世界上任何一个字符都可以用一个Unicode编码来表示,一旦字符的Unicode编码确定下来后,就不会再改变了,但是unicode存在二个局限性,第一一个Unicode字符在网络上传输或者最终存储起来的时候,并不见得每个字符都需要两个字节,所以可能会造成空间浪费,第二一个Unicode字符保存到计算机里面时就是一串01数字,那么计算机怎么知道一个2字节的Unicode字符是表示一个2字节的字符呢,还是表示两个1字节的字符呢.

Unicode只是规定如何编码,并没有规定如何传输、保存这个编码.

例如“汉”字的Unicode编码是6C49,可以用4个ascii数字来传输、保存这个编码,也可以用utf-8编码的3个连续的字节E6 B1 89来表示它,关键在于通信双方都要认可.

因此Unicode编码有不同的实现方式,比如:UTF-8、UTF-16等等

python下的编码

python2对于编码理解困难,很大一部分原因在于系统有很多编码,这里说明下

#windows环境和linux环境下的区别
sys.getdefaultencoding()
sys.getfilesystemencoding()
locale.getdefaultlocale()
locale.getpreferredencoding()
sys.stdout.encoding()
  • sys.getdefaultencoding() 不管在何种环境下返回都是ascii,所以默认情况下转码解码默认都是ascii
  • 对于str类型, locale.getdefaultlocale() 决定了具体的编码格式.具体见下面说明
  • sys.stdout.encoding 表示输出使用的编码,同样的文件编码,同样的代码,不同的系统环境输出是有差异的

最佳实践

文件本身的编码和文件头编码( # coding=utf-8 )保持一致

Python2中str和unicode对象

首先声明下,自己运行的代码在windows和linux环境各有一份示例,且通过python交互式解析器来说明.

python解析器不用用户定义编码头,所以内部处理依赖于 locale 环境.

在windows机器运行

>>> import locale
>>> locale.getdefaultlocale()
('zh_CN', 'cp936')

在linux机器运行

>>> import locale
>>> locale.getdefaultlocale()
('en_US', 'UTF-8')

在python中和字符串相关的数据类型,分别是str、unicode两种,他们都是basestring的子类.

在python代码中定义str,unicode类型,解析器是如何解析的呢

  • 读出文件内容
  • 将内容根据 文件编码 解码成为unicode
  • 解析unicode字符串,假如定义是 u 开头,创建一个unicode对象
  • 解析str字符串,将会从unicode按照文件编码再编码成为str对象

通过代码看看字符串在内部是如何存储的

str类型

#windows
>>> a="哈哈"
>>> type(a)
<type 'str'>
>>> a
'\xb9\xfe\xb9\xfe'
>>> len(a)
4
>>> a[1]
'\xfe'

#linux
>>> a = '哈哈'
>>> type(a)
<type 'str'>
>>> a
'\xe5\x93\x88\xe5\x93\x88'
>>> len(a)
6
>>> a[1]
'\x93'

str存储的是已经编码后的字节序列,输出时看到每个字节用16进制表示,以\x开头,

linux环境下每个汉字会占用3个字节的长度,windows环境下每个汉字会占用2个字节的长度

unicode类型

#linux和windows环境一样
>>> a=u'哈哈'
>>> type(a)
<type 'unicode'>
>>> a
u'\u54c8\u54c8'
>>> len(a)
2
>>> a[1]
u'\u54c8'

unicode是 "字符" 串,存储的是编码前的字符,输出是看到字符以\u开头,每个汉字占用一个长度

通过上述可以看出:

  • 定义unicode和系统环境没有联系,存储的是以u开头的unicode字符集
  • str类似于字符数组,str类型定义内部存储则和系统环境有关系,假如系统环境是utf-8则存储utf-8规则的字符数组,假如系统环境是cp936则存储cp936规则的字符数组.
  • str类型不要使用len这样的函数,因为截取出来可能就是所谓的乱码了.

python2中str和unicode如何转换

既然同时存在str和unicode类型,则就涉及到二者的转换了.

先说基本概念

  • str = unicode.encode(字符编码),从unicode转换成指定编码的str对象
  • unicode = str.decode(字符编码),特指从指定编码的str对象转换为unicode对象

注意:

  • str转换为unicode的时候,必须知道原有字符串编码是什么类型的,假如指定错误则会报错
  • str从一种编码转换为另外一种编码的时候,必须先转换为unicode,再转换成指定编码的str类型

一般情况下,不应该同时定义str和unicode类型,尽量使用unicode类型,假如都统一使用unicode类型,那为什么还要出现str类型呢,在python2中,一般在I/O操作的时候才会有编码转换,这在后面描述.

print字符串发生了什么

任何对象都默认包含内建方法 str ,在print的时候,该方法生效

假如print unicode对象,则根据默认编码解码为str对象.

假如print str对象,由于输出就是str对象,默认不用做任何解码.

看下面的例子,注意这里是通过python file.py的方式运行,在windows下运行是乱码,而在linux下运行显示正确,原因在于 sys.stdout.encoding

#!/usr/bin/env python
#coding=utf-8
a = '哈哈'
print a

sys.stdout.encoding 表示print输出使用的编码.

在linux环境下,由于输出的编码本来就是utf-8,所以能正确显示

在windows环境下,str字符串存储的是utf-8序列,显示要求的却是gbk,则出现乱码,所以需要转码,修改如下:

#!/usr/bin/env python
#coding=utf-8
a = '哈哈'
print a.decode('utf-8').encode('gbk')  #在内部存储gbk类型的str字符串

而定义uinicode对象的时候,不会涉及任何的转码问题,print的时候,unicode对象能够根据文件编码自动转换

以下代码在任何环境下都能正常运行

#!/usr/bin/env python
#coding=utf-8
a = u'哈哈'
print a

IO操作发生了什么

正因为有IO操作,str类型的对象可能才有存在的意义.或者说假如没有可恶的str对象,则世界就太平了.

内置的open函数打开文件时,read方法读取的是一个str,用你知道的编码把它解码成unicode

open函数打开文件之后的写操作,则需要将需要写入的字符串按照其编码encode为一个str.

是不是很熟悉,print语句输出和文件打开一样都是str类型,尽可能处理的时候抛弃str,确保处理的对象是unicode,只在需要的时候才转码.

通过下面的代码就能明白繁琐的转码和解码操作

#!/usr/bin/env python
#coding=utf-8

import os 
def filewrite(): 
    file1 = os.getcwd() + "\1.txt"
    file2 = os.getcwd() + "\2.txt"
    str= u'我们是中国人'

    f = open(file1, "a")
    f.write(str.encode('gbk'))
    f.close()

    f2 = open(file2, "a")
    f2.write(str.encode('utf-8'))
    f2.close()

def fileread():

    file1 = os.getcwd() + "\1.txt"
    file2 = os.getcwd() + "\2.txt"

    f= open(file1,"r")
    data = f.read()
    print(data)

    f= open(file2,"r")
    data = f.read()
    print(data.decode('utf-8'))

filewrite()
fileread()

IO操作使用codecs

codecs模块也提供了一个open函数,可以直接指定好编码打开一个文本文件,那读取到的文件内容则直接是一个unicode字符串.对应的指定编码后的写入文件,则可以直接将unicode写到文件中

#!/usr/bin/env python
#coding=utf-8
import codecs
import os 

def codecswrite() :
    file3 = os.getcwd() + "\3.txt"
    str = u'哈哈'
    f = codecs.open(file3,"w",'utf-8') 
    f.write(str)
    f.close()

def codecsread():
    file3 = os.getcwd() + "\3.txt"
    f = codecs.open(file3,"r",'utf-8') 
    data = f.read()
    print (data)

codecswrite()
codecsread()

字符串拼接发生了什么

unicode和str类型通过+拼接时,输出结果是unicode类型,相当于先将str类型的字符串通过decode()方法解码成unicode再拼接

#windows环境,python交互式运行
>>>a="中国"
>>>b=u"你好"
>>>a+b

会出现 UnicodeDecodeError: 'ascii' codec can't decode byte 0xd6 in position 0: ordinal not in range(128) 错误,原因在于python自动将str类型的变量按照默认的编码格式sys.getdefaultencoding()来解码,默认编码即ascii,而这个字符不在ascii的范围内,就出现了错误,所以需要修改如下

#windows环境,python交互式运行
>>>a="中国"
>>>b=u"你好"
>>> a.decode('gbk')+b
u'\u4e2d\u56fd\u4f60\u597d'

让我们轻松下

这里看下python中的字符串的处理方式,其实在学习的时候和PHP比较下更让人有印象

单引号,双引号,转义

python中,字符串可以用单引号和双引号括起来,没有区别.

假如字符串用单引号括起来,则字符串中则不能有单引号,除非通过转义去处理,同理双引号也一样

str和repr

>>> "hello"
'hello'
>>> print "hello"
hello

通过python打印的字符串会被双引号括起来,这是因为python打印值的时候会保持该值在python代码中的状态.

而通过python语句则结果不一样.

这里就涉及到值被转换为字符串的两种机制:

str函数 :把值转换为合理形式的字符串,以便人类能够理解.

repr函数 :把值转换为python能够认识的值.

>>> print repr("hello")
'hello'
>>> print repr(1000L)  
1000L
>>> print str("hello") 
hello
>>> print str(1000L)  
1000

input和raw_input

input假设输入的是合法的python表达式,不然会报错

>>> name=input("please:")
please:hello
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'hello' is not defined

应该变更为:

>>> name=input("please:")
please:"hello"
>>> print name
hello

而raw_input函数则将所有的输入当作原始数据

>>> name=raw_input("please:")
please:hello
>>> print name
hello

长字符串

假如要写多行的字符可以使用三个引号

str='''hello
world str
'''
print(str)

引号之间的内容原本是什么样输出也是什么样的,可以直接使用单双引号,不用转义

普通字符也可以跨行,只要一行之中最后一个字符是反斜线,那么换行就转义了

str='hello\
world'
print(str)

原始字符串

原始字符串对于反斜线不会特殊对待

str=r'hello\nworld'
print(str)

\n 字符在原始字符串里面就不是换行了而是原始的\n字符