Python3的str文本和bytes字节序列 Unicode介绍

本文详细介绍了Python3中str与bytes的交互,包括Unicode编码与解码过程中的错误处理,如UnicodeEncodeError和UnicodeDecodeError。讨论了struct和memoryview在处理字节序列中的作用,以及如何检测字节序列的编码。此外,文章还涵盖了Unicode规范化、排序、数据库和双模式函数等内容,帮助理解Python3中的字符串和字节操作。
摘要由CSDN通过智能技术生成

python3的str对象中获取的元素是Unicode字符,这相当于从python2中的unicode对象中获取元素,而不是从python2的str对象中获取原始字节序列

python3的str类型基本相当于python2的unicode类型。

把码位转换成字节序列的过程是编码。 encode (把字符串转换成用于存储或者传输的字节序列)

把字节序列转换为码位的过程是解码。 decode (把字节序列变成人类可读的文本字符串)

s = 'café'

b = s.encode('utf-8')  # str转换为bytes对象
print(b)
s = b.decode('utf-8')  # bytes对象转换为str对象
print(s)
打印
b'caf\xc3\xa9'
café

字节介绍

bytes和bytearray对象的各个元素是介于0~255(包含)之间的整数。

bytes为不可变的字节序列。bytearray可变的字节序列,相当于bytes的可变版本,可以称为字节数组。

cafe = bytes('café', encoding=''utf-8")

cafe[0] 结果为99

然而,字节序列(二进制)的切片却始终是字节序列

cafe[:1] 结果为'c'

cafe_array = bytearray(cafe)

cafe_array[-1:] 结果为bytearray(b'\xa9') # bytearray的切片还是bytearry对象

二进制序列其实是整数序列,但是他们的字面展示各个字节的值,会有三种方式:

  • 可打印范围内的字节,使用ASCII字符本身。 比如以上的 caf
  • 制表符、换行符、回车符等 \t \n \r
  • 其他字节的值,使用十六进制转移序列。 比如以上的 \xc3\xa9

结构体struct和内存视图memoryview

struct模块提供了一些函数,比如把打包的字节序列转换成不同类型字段组成的元祖,还有一些函数可以反向转换,把元祖转换成打包的字节序列。

struct模块能处理bytes、bytearray、memoryview对象。

memoryview是共享内存,让你访问其他二进制序列、打包的数组、缓冲中的数据切片,而无需复制字节序列。

memoryview对象的切片是一个新的memory对象,而且不会复制字节序列。

示例,使用struct和memoryview查询GIF图像的头部信息

import struct

fmt = "<3s3sHH"  # 结构体: <是小字节序列, 3s3s是两个3字节序列,HH是两个16位二进制整数。
with open('s.gif', 'rb') as f:
    memoryview_ = memoryview(f.read())  # 创建一个memoryview对象

gif_header = memoryview_[:10]  # 使用切片再创建一个memoryview对对象,这里不会复制字节序列!
print(bytes(gif_header))
uppack = struct.unpack(fmt, gif_header)  # 拆包,等到一个元祖,包含了图片信息:类型,版本,宽度,高度
print(uppack)
del gif_header  # 删除引用,释放memoryview实例所占的内存
del memoryview_
打印
b'GIF89a,\x01,\x01'
(b'GIF', b'89a', 300, 300)

编码器介绍

Python自带了超过100种编码器,用于文本和字节的转换。每个编码器都有一个名称,比如"utf_8" 还有别名utf8/utf-8/U8

这些名称都可以传递给open() str.encode() bytes.decode() 等函数的的encoding参数。

一些典型的编码:

latin1 (iso8859_1) 一种重要的编码,是很多编码的基础。

cp1252 是微软指定的latin1的超集,新增了一些符号。

gb2312 简体中文编码的旧标准。

cp936 其实就是GBK,IBM在发明Code Page的时候将GBK放在第936页,所以叫CP936

utf-8 目前最常见的8位编码。可以兼容ASCII所有

utf-16le 是utf-16的16位编码的另一种形式,

UnicodeEncodeError

多数的非UTF编码器只能处理Unicode字符的一小部分。把文本转换为字节序列的时候,如果编码中没有定义某个字符,就会抛出异常UnicodeEncodeError

>>> city = 'Sāo Paulo'

>>> city.encode('utf8')

b'S\xc4\x81o Paulo'

>>> city.encode('cp437')

Traceback (most recent call last):

 File "<stdin>", line 1, in <module>

 File "C:\Users\lijiachang\AppData\Local\Programs\Python\Python37-32\lib\encodings\cp437.py", line 12, in encode

   return codecs.charmap_encode(input,errors,encoding_map)

UnicodeEncodeError: 'charmap' codec can't encode character '\u0101' in position 1: character maps to <undefined>

编码时可以指定errors='ignore'来忽略错误,变成空白字符

或者是errors='replace'把无法编码的字符变为? (推荐这种方式,用户知道编码出了问题)

或者是errors='xmlcharrefreplace'把无法编码的字符变为XML实体

>>> city.encode('cp437',errors='ignore')

b'So Paulo'

>>> city.encode('cp437',errors='replace')

b'S?o Paulo'

>>> city.encode('cp437',errors='xmlcharrefreplace')

b'S&#257;o Paulo'

>>>

UnicodeDecodeError

同样不是每一个字符序列都有有效的utf-8或者utf-16对应。

>>> city = b'S\xc4\x81o Paulo'

>>> city.decode('utf-8')

'Sāo Paulo'

>>> city.decode('cp1252')

Traceback (most recent call last):

 File "<stdin>", line 1, in <module>

 File "C:\Users\lijiachang\AppData\Local\Programs\Python\Python37-32\lib\encodings\cp1252.py", line 15, in decode

   return codecs.charmap_decode(input,errors,decoding_table)

UnicodeDecodeError: 'charmap' codec can't decode byte 0x81 in position 2: character maps to <undefined>

同样也可以使用errors参数来处理错误:

>>> city.decode('cp1252',errors='replace')

'S�o Paulo'

�的含义是Unicode官方指定的未知字符。

SyntaxError 加载模块时预期之外的编码

Python3中默认使用的是UTF-8 编码源码。

Python2中默认使用是的ASCII编码。

比如在python3中加载py模块的时候包含了除utf-8之外的编码,而且没有编码声明,就报错SyntaxError。

为了修正这个问题,可以在顶部加入coding注释

#coding:cp1252

在Python2常见到文件头部的# coding:utf-8 在Python3中就不需要了,因为Python3默认使用了utf-8

找出字节序列的编码

使用编码侦测包 chardet,在代码中动态检测当前页面或者文件中的编码格式信息。

import chardet
import urllib.request

TestData = urllib.request.urlopen('http://www.baidu.com/').read()
print(chardet.detect(TestData))
打印
{'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}

以上的结果分析, 其准确率99%的概率,编码格式为utf-8

*BOM:字节顺序标记

byte-order mark 字节顺序标记,出现在文件的头部,用来标记用哪种编码(小字节序,大字节序)。

编码的默认值

在windows上的默认编码问题,可以看出在多设备下运行的代码,一定不能依赖默认编码

以utf-8编码写入文件,然后以默认编码读取:

with open('cafe.txt', 'w', encoding='utf-8') as f:
    f.write('café')

with open('cafe.txt', 'r') as f:
    print(f)
    print(f.read())
    
打印
<_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp936'>
caf茅

读取出现乱码的原因就是,windows中系统的默认编码可以看到打印encoding='cp936' CP936其实就是GBK

以二进制模式打开文本文件,使用chardet猜编码:

with open('cafe.txt', 'rb') as f:
    b = f.read()
    print(chardet.detect(b))
    
打印
{'encoding': 'utf-8', 'confidence': 0.505, 'language': ''}

查看系统的一些编码

locale.getpreferredencoding() 这个是文本文件默认使用的编码,很重要。如果打开文件没有指定encoding,就会以这个编码。还有重定向文件内容的时候,也会使用这个编码。

sys.stdout.encoding 控制台编码

sys.getfilesystemencoding() 用于编解码文件名(不是文件内容)。比如把字符串参数作为文件名传给open()函数就会使用他,如果传入的文件名是字节序列,就不经改动直接给OS API。

sys.getdefaultencoding() python在二进制和字符串之间的转换,内部使用的这个编码。

import sys, locale

my_file = open('dummy', 'w')
expressions = """locale.getpreferredencoding()
type(my_file)
my_file.encoding
sys.stdout.encoding
sys.stdin.encoding
sys.getdefaultencoding()
sys.getfilesystemencoding()
"""

for ex in expressions.split():
    value = eval(ex)
    print(ex.rjust(30), ' > ', repr(value))
打印
 locale.getpreferredencoding()  >  'cp936'
                 type(my_file)  >  <class '_io.TextIOWrapper'>
              my_file.encoding  >  'cp936'
           sys.stdout.encoding  >  'UTF-8'
            sys.stdin.encoding  >  'UTF-8'
      sys.getdefaultencoding()  >  'utf-8'
   sys.getfilesystemencoding()  >  'utf-8'

以上查看了文件默认编码,在Python中是无法修改的,需要在Linux系统中配置/etc/profile

export LC_ALL="en_US.utf8"
export LANG="en_US.utf8"

规范化Unicode字符串 NFC/NFD

café这个词有两种构成方式,分别有4个和有5个码位,但是结果是一样的

以下示例是在linux的python3中,windows可能有不同的打印

>>> s1 = 'café'

>>> s2 = 'cafe\u0301'

>>> s1,s2

('café', 'café')

>>> len(s1),len(s2)

(4, 5)

>>> s1==s2

False

U+0301是音符,加到e后面得到了é。在Unicode标准中é和e/u0301这样的序列叫做标准等价物,应用程序应该把他们视为相同的字符。但是在Python中看到的是不同的码位序列,判断了二者不相等。

解决方法就是使用unicodedata.normalize函数提供Unicode规范化,这个函数的第一个参数可以是四种模式:'NFC' 'NFD' 'NFKC' 'NFKD'

NFC: Normalization Form C 使用最少的码位构成等价的字符串。(最少)

NFD: 把组合字符分解成基字符和单独的组合字符。(最多)

from unicodedata import normalize

s1 = 'café'
s2 = 'cafe\u0301'
print(len(normalize('NFC', s1)), len(normalize('NFC', s2)))
print(len(normalize('NFD', s1)), len(normalize('NFD', s2)))

print(normalize('NFC', s1) == normalize('NFC', s2))
print(normalize('NFD', s1) == normalize('NFD', s2))
打印
4 4
5 5
True
True

NFKC和NFKD基于以上两种模式,提高了兼容性。K表示compatiblity兼容性。这种模式中,各个兼容字符会被替换成一个或者多个兼容分解的字符,这样会有些格式损失。

比如½符号会被分解成三个字符序列1/2

比如4²会被转换成42 这就难以接受了。。

from unicodedata import normalize

s1 = '½'
s2 = '4²'
print(normalize('NFKC', s1), normalize('NFKC', s2))
打印
1⁄2 42

str.casefold() 这个是把字符串转换为小写,和str.lower()非常类似。但是个别字符和lower函数结果不一样, 比如说'ß'会转换成'ss'

casefold函数可识别更多的对象将其输出为小写,而lower函数只能完成ASCII码中A-Z之间的大写到小写的转换

规范化文本匹配的函数

from unicodedata import normalize


def nfc_equal(str1, str2):
    """以NFC模式处理文本后对比"""
    return normalize('NFC', str1) == normalize('NFC', str2)


def fold_equal(str1, str2):
    """转为小写对比"""
    return normalize('NFC', str1.casefold()) == normalize('NFC', str2.casefold())


print(nfc_equal('café', 'cafe\u0301'))
print(fold_equal('Strß', 'strss'))

打印
True
True

去掉音符的函数

from unicodedata import normalize, combining


def shave_marks(str1):
    """去掉字符串中的音符"""
    norm_text = normalize('NFD', str1)  # 把字符串分解成基字符和组合记号
    shave_text = ''.join(s for s in norm_text if not combining(s))  # 过滤掉组合记号。 combining()返回字符chr的权威组合值,若未定义这样的值,则返回0。
    return normalize('NFC', shave_text)  # 重组基字符


print(nfc_equal('café'))
打印
cafe

Unicode排序

Python比较任何类型序列,都会一个个比较其中的元素。对于字符串来说,比较的是码位。在处理非ASCII码时,结果可能有问题。

Python中处理非ASCII码的标准排序方式是用local.strxfrm函数,这个函数的官方说明是:把字符串转换成合适所在区域进行比较的形式。

使用local.strxfrm函数之前,要先设置setlocale(LC_COLLATE), <your_locale>)

import locale

locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')

sorted_fruits = sorted(fruits, key=locale.strxfrm)

以上说到的排序方法,只能使用与linux中。

可以使用第三方库,pyuca模块,适用于mac os、linux、windows。

import pyuca

coll = pyuca.Collator()

sorted_fruits = sorted(fruits, key=coll.sort_key)

Unicode数据库

Unicode标准提供了一个完整数据库,不仅有码位与字符名称直接的映射,还有字符的元数据,②可以识别出2,½可以识别出0.5

unicodedata.numeric(char) 可以识别出各种数字符号对应的数值。

unicodedata.name(char) 可以展示字符在Unicode中的名称。

补充一下:

数字判断的两个函数:

isdigit() True: Unicode数字,byte数字(单字节),全角数字(双字节),罗马数字 False: 汉字数字 Error: 无

isnumeric() True: Unicode数字,全角数字(双字节),罗马数字,汉字数字 False: 无 Error: byte数字(单字节)

格式化:

%d 十进制整数

%o 八进制整数

%x 十六进制整数 (但是前面不带0x)

import unicodedata

re_digit = re.compile(r'\d')

sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'

for char in sample:
    print('U+%04x' % ord(char),  # ord()  返回对应的 ASCII 数值,或者 Unicode 数值。%x 十六进制整数
          char.center(6),  # 长度为6的居中显示
          're_digit' if re_digit.match(char) else '-',  # 是否匹配正则'\d'
          'is_digit' if char.isdigit() else '-',  # 是否符合isdigit()函数。检测字符串是否只数字组成的
          'is_isnumeric' if char.isnumeric() else '-',  # 是否符合isnumeric()函数。 检测字符串是否是只包含数字字符。比如中文数字也是数字字符。
          format(unicodedata.numeric(char), '5.2f'),  # 显示数值。格式化为长度5,小数点后保留2位
          unicodedata.name(char),  # Unicode标准中字符的名称

          sep='\t'
          )
打印
U+0031	  1   	re_digit	is_digit	is_isnumeric	 1.00	DIGIT ONE
U+00bc	  ¼   	-	-	is_isnumeric	 0.25	VULGAR FRACTION ONE QUARTER
U+00b2	  ²   	-	is_digit	is_isnumeric	 2.00	SUPERSCRIPT TWO
U+0969	  ३   	re_digit	is_digit	is_isnumeric	 3.00	DEVANAGARI DIGIT THREE
U+136b	  ፫   	-	is_digit	is_isnumeric	 3.00	ETHIOPIC DIGIT THREE
U+216b	  Ⅻ   	-	-	is_isnumeric	12.00	ROMAN NUMERAL TWELVE
U+2466	  ⑦   	-	is_digit	is_isnumeric	 7.00	CIRCLED DIGIT SEVEN
U+2480	  ⒀   	-	-	is_isnumeric	13.00	PARENTHESIZED NUMBER THIRTEEN
U+3285	  ㊅   	-	-	is_isnumeric	 6.00	CIRCLED IDEOGRAPH SIX

支持字符串和字节序列的双模式函数

有一些模块中的函数,可以使用字符串或者字节序列作为参数,然后根据类型展现不同的行为。

re正则表达式模块

re.compile(r'\d+') 这种是字符串模式,能够匹配ASCII字符和之外的Unicode数字

re.compile(rb'\d+') 这种是字节序列模式,只能匹配ASCII字符

补充,字符串正则表达式有个re.ASCII标志,可以只匹配ASCII字符。

os模块

Linux内存对Unicode的支持并不完善,使用字节序列作为文件名的文件,在不同操作系统中,可能无法利用最合理的方法解码成字符串。

这种情况,可以把字节序列直接传给python来做处理,通过os模块的函数,直接取得字节序列的值。

os.listdir('.') 这种是通过系统解码后,把文件名传递给python。 如 digits-π.txt

os.listdir(b'.') 参数是字节序列,listdir函数返回的也是字节序列。由我们自行处理成字符串。 如digits-\xcf\x80.txt

针对系统文件名的处理,os模块特殊提供了编码和解码函数:

fsencode(filename): 如果参数filename是str类型,就使用sys.getfilesystemencoding()得到的编码把filename编码成字节序列返回。否则,直接返回不处理的filename

fsdecode(filename):如果参数是bytes类型,就使用sys.getfilesystemencoding()得到的编码把filename解码成字节序列返回。否则,直接返回不处理的filename

错误处理方式surrogateescape

在Python3中编解码器的错误处理,除了ignore、replace等,还多了一个surrogateescape:当遇到解码的字节会替换成Unicode中的U+DC00到U+DCFF之间的码位,这些码位都是保留的,用于应用程序内部使用。

b = b'digits-\xcf\x80'

b_s = b.decode('ascii', errors='surrogateescape')

b_s结果:digits-\udccf\udc80

b_s.encode('ascii', errors='surrogateescape')

结果b'digits-\xcf\x80'

也就是说,当无法解码时,会在Unicode找一个预留不用的码位,来映射这个字节,当编码时再映射回去。不过这样也没啥可读性。

以上代码我在Python3.7中测试无效,会报错:UnicodeEncodeError: 'utf-8' codec can't encode characters in position 7-8: surrogates not allowed

暂时不知道原因。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值