一、字符串的表示和存储
字符串是字符的序列,每个字符都有有一个数字作为标识,同时会有一个将标识转换为存储字节的编码方案;
s = 'hello world python'
for c in s:
print(c, end=' ')
h e l l o w o r l d p y t h o n
ACSII为协议内的每个字符分别对应一个数字,然后以这个数字的二进制形式存储到计算机;
s = 'hello world python'
for c in s:
num = ord(c)
print(num, format(num, 'b'))
104 1101000
101 1100101
108 1101100
108 1101100
111 1101111
32 100000
119 1110111
111 1101111
114 1110010
108 1101100
100 1100100
32 100000
112 1110000
121 1111001
116 1110100
104 1101000
111 1101111
110 1101110
ACSII协议覆盖的字符十分有限,使用一个字节就可以保存,这也是其比较简单的根源;
s = b'é'
File "<ipython-input-19-b82fcf157fe5>", line 1
s = b'é'
^
SyntaxError: bytes can only contain ASCII literal characters.
unicode标准为每个字符制定一个数字作为code point;
s = 'è ç í'
for c in s:
print(ord(c))
232
32
231
32
237
unicode支持大量的字符,需要使用多个字节来存储,这就涉及到字节的大小端、空间占用及与ACSII的兼容性问题;
UTF-32编码方案直接使用4个字节来承载code poin的二进制形式,涉及大小端问题,比较浪费空间,使用较少;
s = 'èçí'
for b in s.encode('utf_32be'):
print(hex(b), end=' ')
print()
for b in s.encode('utf_32le'):
print(hex(b), end=' ')
print()
for b in s.encode('utf_32'):
print(hex(b), end=' ')
0x0 0x0 0x0 0xe8 0x0 0x0 0x0 0xe7 0x0 0x0 0x0 0xed
0xe8 0x0 0x0 0x0 0xe7 0x0 0x0 0x0 0xed 0x0 0x0 0x0
0xff 0xfe 0x0 0x0 0xe8 0x0 0x0 0x0 0xe7 0x0 0x0 0x0 0xed 0x0 0x0 0x0
UTF-16编码方案根据前两个字节的范围来确定使用两个字节还是四个字节,虽然比UTF-32节省空间,但是使用也比较少;
s = 'èçí'
for b in s.encode('utf_16be'):
print(hex(b), end=' ')
print()
for b in s.encode('utf_16le'):
print(hex(b), end=' ')
print()
for b in s.encode('utf_16'):
print(hex(b), end=' ')
0x0 0xe8 0x0 0xe7 0x0 0xed
0xe8 0x0 0xe7 0x0 0xed 0x0
0xff 0xfe 0xe8 0x0 0xe7 0x0 0xed 0x0
UTF-8也使用变长字节,每个字符使用的字节个数与其Unicode编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数为1~4不等;
s = 'èçí'
for b in s.encode('utf_8'):
print(hex(b), end=' ')
0xc3 0xa8 0xc3 0xa7 0xc3 0xad
utf-16和utf-32编码方案默认生成的字节序列会添加BOM(byte-order mark)即\xff\xfe,指明编码的时候使用Interl CPU小字节序。
二、字节数组
bytes和bytearray的元素都是介于0-255之间的整数,但是通过字符编码方案也可以存储任何的字符串;字节数组切片还是对应的字节数组;
字节数组可以直接显示ASCII字符;
s = 'helloèçí'
b_arr = bytes(s, 'utf_8')
print(type(b_arr))
print(type(b_arr))
for b in b_arr:
print(b, end=' ')
print()
print('element of bytes is int number', b_arr[0])
print('splice of bytes is bytes',end = ' ' )
b_arr_splice = b_arr[:1]
print(b_arr_splice)
num_b_arr = bytes([299])
<class 'bytes'>
b'hello\xc3\xa8\xc3\xa7\xc3\xad'
104 101 108 108 111 195 168 195 167 195 173
element of bytes is int number 104
splice of bytes is bytes b'h'
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-61-b8f064f91cf5> in <module>()
13 print(b_arr_splice)
14
---> 15 num_b_arr = bytes([299])
ValueError: bytes must be in range(0, 256)
struct模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列。struct模块能处理bytes、bytearray和memoryview对象。
import struct
record_format = 'hd4s'
pack_bytes = struct.pack(record_format, 7 , 3.14,b'gbye')
print(type(pack_bytes))
print(pack_bytes)
with open('struct.b', 'wb') as fp:
fp.write(pack_bytes)
record_size = struct.calcsize(record_format)
with open('struct.b', 'rb') as fp:
record_bs = fp.read(record_size)
print(struct.unpack(record_format, record_bs))
三、不要依赖默认编码
读写文本文件的时候最好要显示的指定编码方案,防止编码方案不匹配出现乱码或者错误;
open('cafe.txt', 'w', encoding='utf-8').write('café')
fp = open('cafe.txt')
print(fp)
print(fp.read())
由于Linux的默认编码是UTF-8,所以运行结果正常
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='UTF-8'>
café
但是在windows 10上执行就不这么幸运了,我们可以看到IO的默认编码方案是cp936
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
caf茅
在Linux和windows上分别执行以下探测默认编码方案的代码
import sys, locale
expressions = '''
locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.isatty()
sys.stdout.encoding
sys.stdin.isatty()
sys.stdin.encoding
sys.stderr.isatty()
sys.stderr.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
'''
with open('encoding', 'w') as my_file:
for expression in expressions.split():
value = eval(expression)
print(expression.rjust(30), '->', repr(value))
在Ubuntu上执行,可以看到输出的都是UTF-8;
locale.getpreferredencoding() -> 'UTF-8'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'UTF-8'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'utf-8'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
在windows 10上执行,locale.getpreferredencoding()和my_file的编码都是cp936;
locale.getpreferredencoding() -> 'cp936'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'cp936'
sys.stdout.isatty() -> True
sys.stdout.encoding -> 'utf-8'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
如果没有指定编码方案,操作文本文件的时候默认使用locale.getpreferredencoding(),在windows10上将python的执行结果重定向到文件,可以看到sys.stdout.encoding变成了cp936;
locale.getpreferredencoding() -> 'cp936'
type(my_file) -> <class '_io.TextIOWrapper'>
my_file.encoding -> 'cp936'
sys.stdout.isatty() -> False
sys.stdout.encoding -> 'cp936'
sys.stdin.isatty() -> True
sys.stdin.encoding -> 'utf-8'
sys.stderr.isatty() -> True
sys.stderr.encoding -> 'utf-8'
sys.getdefaultencoding() -> 'utf-8'
sys.getfilesystemencoding() -> 'utf-8'
python使用sys.getdefaultencoding()进行二进制数据与字符串之间的转换;
sys.getfilesystemencoding( )用于编解码文件名(不是文件内容)。把字符串参数作为文件名传给open( )函数时就会使用它;
四、规范化字符串之后进行比较
因为Unicode有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。
# 同样的一个字符会有不同的构成方式
s1 = 'café'
s2 = 'cafe\u0301'
print((s1, s2))
print((len(s1), len(s2)))
print(s1 == s2)
('café', 'café')
(4, 5)
False
U+0301是COMBINING ACUTE ACCENT,加在“e”后面得到“é”。在Unicode标准中,'é’和’e\u0301’这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python看到的是不同的码位序列,因此判定二者不相等。
Python中unicodedata.normalize函数提供的Unicode规范化。这个函数的第一个参数是这4个字符串中的一个:‘NFC’、‘NFD’、‘NFKC’和’NFKD’。NFC(Normalization Form C)使用最少的码位构成等价的字符串,而NFD把组合字符分解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期:
# normalize字符串再进行比较
from unicodedata import normalize
s1 = 'café'
s2 = 'cafe\u0301'
print((s1, s2))
print((len(s1), len(s2)))
print(s1 == s2)
s1_nfc_nor = normalize('NFC', s1)
s2_nfc_nor = normalize('NFC', s2)
print((s1_nfc_nor, s2_nfc_nor))
print((len(s1_nfc_nor), len(s2_nfc_nor)))
print(s1_nfc_nor == s2_nfc_nor)
s1_nfd_nor = normalize('NFD', s1)
s2_nfd_nor = normalize('NFD', s2)
print((s1_nfd_nor, s2_nfd_nor))
print((len(s1_nfd_nor), len(s2_nfd_nor)))
print(s1_nfd_nor == s2_nfd_nor)
# ('café', 'café')
# (4, 5)
# False
# ('café', 'café')
# (4, 4)
# True
# ('café', 'café')
# (5, 5)
# True
在另外两个规范化形式(NFKC和NFKD)的首字母缩略词中,字母K表示“compatibility”(兼容性)。这两种是较严格的规范化形式,对“兼容字符”有影响。虽然Unicode的目标是为各个字符提供“规范的”码位,但是为了兼容现有的标准,有些字符会出现多次。例如,虽然希腊字母表中有“μ”这个字母(码位是U+03BC,GREEK SMALL LETTER MU),但是Unicode还是加入了微符号’µ’(U+00B5),以便与latin1相互转换。因此,微符号是一个“兼容字符”。
# NFKC的规范化
from unicodedata import normalize, name
half = '½'
print(len(half))
print(hex(ord(half)))
half_nor = normalize('NFKC', half)
print(half_nor)
print(type(half_nor))
print(len(half_nor))
for c in half_nor:
print(hex(ord(c)), end=' ')
print()
four_squared = '4²'
four_squared_no = normalize('NFKC', four_squared)
print(four_squared_no)
micro = 'µ'
micro_nor = normalize('NFKC', micro)
print(micro_nor)
print(ord(micro), ord(micro_nor))
print(name(micro), name(micro_nor))
# 1
# 0xbd
# 1⁄2
# <class 'str'>
# 3
# 0x31 0x2044 0x32
# 42
# μ
# 181 956
# MICRO SIGN GREEK SMALL LETTER MU
使用’1/2’替代’½’可以接受,微符号也确实是小写的希腊字母’µ’,但是把’4²’转换成’42’就改变原意了。某些应用程序可以把’4²’保存为’42’,但是normalize函数对格式一无所知。因此,NFKC或NFKD可能会损失或曲解信息。
大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由str.casefold( )方法(Python 3.3新增)支持。对于只包含latin1字符的字符串s,s.casefold( )得到的结果与s.lower( )一样,唯有两个例外:微符号’µ’会变成小写的希腊字母“μ”(在多数字体中二者看起来一样);德语Eszett(“sharp s”,ß)会变成“ss”。
# 大小写折叠
micro = 'µ'
print(name(micro))
micro_cf = micro.casefold()
print(name(micro_cf))
print((micro, micro_cf))
eszett = 'ß'
print(name(eszett))
eszett_cf = eszett.casefold()
print((eszett, eszett_cf))
# MICRO SIGN
# GREEK SMALL LETTER MU
# ('µ', 'μ')
# LATIN SMALL LETTER SHARP S
# ('ß', 'ss')
Google搜索涉及很多技术,其中一个显然是忽略变音符号(如重音符、下加符等),至少在某些情况下会这么做。去掉变音符号不是正确的规范化方式,因为这往往会改变词的意思,而且可能误判搜索结果。但是对现实生活却有所帮助:人们有时很懒,或者不知道怎么正确使用变音符号,而且拼写规则会随时间变化,因此实际语言中的重音经常变来变去。
# 极端规范化,去掉变音符号
import unicodedata
import string
def shave_marks(txt):
txt_nor = normalize('NFD', txt)
txt_shaved = ''.join(c for c in txt_nor if not unicodedata.combining(c))
return normalize('NFC', txt_shaved)
order = 'è ç í'
print(shave_marks(order))
greek = 'έ é'
print(shave_marks(greek))
def shave_marks_latin(txt):
txt_nor = normalize('NFD', txt)
latin_base = False
keep = []
for c in txt_nor:
if unicodedata.combining(c) and latin_base:
continue;
keep.append(c)
if not unicodedata.combining(c):
latin_base = c in string.ascii_letters
shaved = ''.join(keep)
return normalize('NFC', shaved)
print(shave_marks_latin(order))
print(shave_marks_latin(greek))
# e c i
# ε e
# e c i
# έ e