Python的文本和字節序列


一、字符串的表示和存儲

字符串是字符的序列,每個字符都有有一個數字作為標識,同時會有一個將標識轉換為存儲字節的編碼方案;

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

代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM