在线二区人妖系列_国产亚洲欧美日韩在线一区_国产一级婬片视频免费看_精品少妇一区二区三区在线

鍍金池/ 教程/ Python/ 字符串和文本
類與對象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時間
測試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語言擴展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

字符串和文本

幾乎所有有用的程序都會涉及到某些文本處理,不管是解析數(shù)據(jù)還是產(chǎn)生輸出。 這一章將重點關(guān)注文本的操作處理,比如提取字符串,搜索,替換以及解析等。 大部分的問題都能簡單的調(diào)用字符串的內(nèi)建方法完成。 但是,一些更為復(fù)雜的操作可能需要正則表達(dá)式或者強大的解析器,所有這些主題我們都會詳細(xì)講解。 并且在操作 Unicode 時候碰到的一些棘手的問題在這里也會被提及到。

使用多個界定符分割字符串

問題

你需要將一個字符串分割為多個字段,但是分隔符(還有周圍的空格)并不是固定的。

解決方案

string 對象的 split() 方法只適應(yīng)于非常簡單的字符串分割情形, 它并不允許有多個分隔符或者是分隔符周圍不確定的空格。 當(dāng)你需要更加靈活的切割字符串的時候,最好使用 re.split() 方法:

>>> line = 'asdf fjdk; afed, fjek,asdf, foo'
>>> import re
>>> re.split(r'[;,\s]\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']

討論

函數(shù)re.split() 是非常實用的,因為它允許你為分隔符指定多個正則模式。 比如,在上面的例子中,分隔符可以是逗號,分號或者是空格,并且后面緊跟著任意個的空格。 只要這個模式被找到,那么匹配的分隔符兩邊的實體都會被當(dāng)成是結(jié)果中的元素返回。 返回結(jié)果為一個字段列表,這個跟 str.split() 返回值類型是一樣的。

當(dāng)你使用 re.split() 函數(shù)時候,需要特別注意的是正則表達(dá)式中是否包含一個括號捕獲分組。 如果使用了捕獲分組,那么被匹配的文本也將出現(xiàn)在結(jié)果列表中。比如,觀察一下這段代碼運行后的結(jié)果:

>>> fields = re.split(r'(;|,|\s)\s*', line)
>>> fields
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
>>>

獲取分割字符在某些情況下也是有用的。 比如,你可能想保留分割字符串,用來在后面重新構(gòu)造一個新的輸出字符串:

>>> values = fields[::2]
>>> delimiters = fields[1::2] + ['']
>>> values
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>> delimiters
[' ', ';', ',', ',', ',', '']
>>> # Reform the line using the same delimiters
>>> ''.join(v+d for v,d in zip(values, delimiters))
'asdf fjdk;afed,fjek,asdf,foo'
>>>

如果你不想保留分割字符串到結(jié)果列表中去,但仍然需要使用到括號來分組正則表達(dá)式的話, 確保你的分組是非捕獲分組,形如 (?:...) 。比如:

>>> re.split(r'(?:,|;|\s)\s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>>

字符串開頭或結(jié)尾匹配

問題

你需要通過指定的文本模式去檢查字符串的開頭或者結(jié)尾,比如文件名后綴,URL Scheme 等等。

解決方案

檢查字符串開頭或結(jié)尾的一個簡單方法是使用 str.startswith() 或者是 str.endswith() 方法。比如:

>>> filename = 'spam.txt'
>>> filename.endswith('.txt')
True
>>> filename.startswith('file:')
False
>>> url = 'http://www.python.org'
>>> url.startswith('http:')
True
>>>

如果你想檢查多種匹配可能,只需要將所有的匹配項放入到一個元組中去, 然后傳給 startswith() 或者 endswith() 方法:

>>> import os
>>> filenames = os.listdir('.')
>>> filenames
[ 'Makefile', 'foo.c', 'bar.py', 'spam.c', 'spam.h' ]
>>> [name for name in filenames if name.endswith(('.c', '.h')) ]
['foo.c', 'spam.c', 'spam.h'
>>> any(name.endswith('.py') for name in filenames)
True
>>>

下面是另一個例子:

from urllib.request import urlopen

def read_data(name):
    if name.startswith(('http:', 'https:', 'ftp:')):
        return urlopen(name).read()
    else:
        with open(name) as f:
            return f.read()

奇怪的是,這個方法中必須要輸入一個元組作為參數(shù)。 如果你恰巧有一個 list 或者 set 類型的選擇項, 要確保傳遞參數(shù)前先調(diào)用 tuple() 將其轉(zhuǎn)換為元組類型。比如:

>>> choices = ['http:', 'ftp:']
>>> url = 'http://www.python.org'
>>> url.startswith(choices)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: startswith first arg must be str or a tuple of str, not list
>>> url.startswith(tuple(choices))
True
>>>

討論

startswith()endswith() 方法提供了一個非常方便的方式去做字符串開頭和結(jié)尾的檢查。 類似的操作也可以使用切片來實現(xiàn),但是代碼看起來沒有那么優(yōu)雅。比如:

>>> filename = 'spam.txt'
>>> filename[-4:] == '.txt'
True
>>> url = 'http://www.python.org'
>>> url[:5] == 'http:' or url[:6] == 'https:' or url[:4] == 'ftp:'
True
>>>

你可以能還想使用正則表達(dá)式去實現(xiàn),比如:

>>> import re
>>> url = 'http://www.python.org'
>>> re.match('http:|https:|ftp:', url)
<_sre.SRE_Match object at 0x101253098>
>>>

這種方式也行得通,但是對于簡單的匹配實在是有點小材大用了,本節(jié)中的方法更加簡單并且運行會更快些。

最后提一下,當(dāng)和其他操作比如普通數(shù)據(jù)聚合相結(jié)合的時候 startswith()endswith() 方法是很不錯的。 比如,下面這個語句檢查某個文件夾中是否存在指定的文件類型:

if any(name.endswith(('.c', '.h')) for name in listdir(dirname)):
...

用 Shell 通配符匹配字符串

問題

你想使用 Unix Shell 中常用的通配符(比如 *.py , Dat[0-9]*.csv 等)去匹配文本字符串

解決方案

fnmatch 模塊提供了兩個函數(shù)—— fnmatch()fnmatchcase(),可以用來實現(xiàn)這樣的匹配。用法如下:

>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('foo.txt', '*.txt')
True
>>> fnmatch('foo.txt', '?oo.txt')
True
>>> fnmatch('Dat45.csv', 'Dat[0-9]*')
True
>>> names = ['Dat1.csv', 'Dat2.csv', 'config.ini', 'foo.py']
>>> [name for name in names if fnmatch(name, 'Dat*.csv')]
['Dat1.csv', 'Dat2.csv']
>>>

fnmatch() 函數(shù)使用底層操作系統(tǒng)的大小寫敏感規(guī)則(不同的系統(tǒng)是不一樣的)來匹配模式。比如:

>>> # On OS X (Mac)
>>> fnmatch('foo.txt', '*.TXT')
False
>>> # On Windows
>>> fnmatch('foo.txt', '*.TXT')
True
>>>

如果你對這個區(qū)別很在意,可以使用 fnmatchcase() 來代替。它完全使用你的模式大小寫匹配。比如:

>>> fnmatchcase('foo.txt', '*.TXT')
False
>>>

這兩個函數(shù)通常會被忽略的一個特性是在處理非文件名的字符串時候它們也是很有用的。 比如,假設(shè)你有一個街道地址的列表數(shù)據(jù):

addresses = [
    '5412 N CLARK ST',
    '1060 W ADDISON ST',
    '1039 W GRANVILLE AVE',
    '2122 N CLARK ST',
    '4802 N BROADWAY',
]

你可以像這樣寫列表推導(dǎo):

>>> from fnmatch import fnmatchcase
>>> [addr for addr in addresses if fnmatchcase(addr, '* ST')]
['5412 N CLARK ST', '1060 W ADDISON ST', '2122 N CLARK ST']
>>> [addr for addr in addresses if fnmatchcase(addr, '54[0-9][0-9] *CLARK*')]
['5412 N CLARK ST']
>>>

討論

fnmatch() 函數(shù)匹配能力介于簡單的字符串方法和強大的正則表達(dá)式之間。 如果在數(shù)據(jù)處理操作中只需要簡單的通配符就能完成的時候,這通常是一個比較合理的方案。

如果你的代碼需要做文件名的匹配,最好使用 glob 模塊。參考5.13小節(jié)。

字符串匹配和搜索

問題

你想匹配或者搜索特定模式的文本

解決方案

如果你想匹配的是字面字符串,那么你通常只需要調(diào)用基本字符串方法就行, 比如 str.find(), str.endswith() , str.startswith() 或者類似的方法:

>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> # Exact match
>>> text == 'yeah'
False
>>> # Match at start or end
>>> text.startswith('yeah')
True
>>> text.endswith('no')
False
>>> # Search for the location of the first occurrence
>>> text.find('no')
10
>>>

對于復(fù)雜的匹配需要使用正則表達(dá)式和 re 模塊。 為了解釋正則表達(dá)式的基本原理,假設(shè)你想匹配數(shù)字格式的日期字符串比如 11/27/2012 ,你可以這樣做:

>>> text1 = '11/27/2012'
>>> text2 = 'Nov 27, 2012'
>>>
>>> import re
>>> # Simple matching: \d+ means match one or more digits
>>> if re.match(r'\d+/\d+/\d+', text1):
... print('yes')
... else:
... print('no')
...
yes
>>> if re.match(r'\d+/\d+/\d+', text2):
... print('yes')
... else:
... print('no')
...
no
>>>

如果你想使用同一個模式去做多次匹配,你應(yīng)該先將模式字符串預(yù)編譯為模式對象。比如:

>>> datepat = re.compile(r'\d+/\d+/\d+')
>>> if datepat.match(text1):
... print('yes')
... else:
... print('no')
...
yes
>>> if datepat.match(text2):
... print('yes')
... else:
... print('no')
...
no
>>>

match() 總是從字符串開始去匹配,如果你想查找字符串任意部分的模式出現(xiàn)位置, 使用 findall() 方法去代替。比如:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
['11/27/2012', '3/13/2013']
>>>

在定義正則式的時候,通常會利用括號去捕獲分組。比如:

>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>>

捕獲分組可以使得后面的處理更加簡單,因為可以分別將每個組的內(nèi)容提取出來。比如:

>>> m = datepat.match('11/27/2012')
>>> m
<_sre.SRE_Match object at 0x1005d2750>
>>> # Extract the contents of each group
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'
>>> m.group(2)
'27'
>>> m.group(3)
'2012'
>>> m.groups()
('11', '27', '2012')
>>> month, day, year = m.groups()
>>>
>>> # Find all matches (notice splitting into tuples)
>>> text
'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
[('11', '27', '2012'), ('3', '13', '2013')]
>>> for month, day, year in datepat.findall(text):
... print('{}-{}-{}'.format(year, month, day))
...
2012-11-27
2013-3-13
>>>

findall()方法會搜索文本并以列表形式返回所有的匹配。 如果你想以迭代方式返回匹配,可以使用 finditer() 方法來代替,比如:

>>> for m in datepat.finditer(text):
... print(m.groups())
...
('11', '27', '2012')
('3', '13', '2013')
>>>

討論

關(guān)于正則表達(dá)式理論的教程已經(jīng)超出了本書的范圍。 不過,這一節(jié)闡述了使用 re 模塊進(jìn)行匹配和搜索文本的最基本方法。 核心步驟就是先使用 re.compile() 編譯正則表達(dá)式字符串, 然后使用 match() , findall() 或者 finditer() 等方法。

當(dāng)寫正則式字符串的時候,相對普遍的做法是使用原始字符串比如 r'(\d+)/(\d+)/(\d+)' 。 這種字符串將不去解析反斜杠,這在正則表達(dá)式中是很有用的。 如果不這樣做的話,你必須使用兩個反斜杠,類似 '(\\d+)/(\\d+)/(\\d+)' 。

需要注意的是 match() 方法僅僅檢查字符串的開始部分。它的匹配結(jié)果有可能并不是你期望的那樣。比如:

>>> m = datepat.match('11/27/2012abcdef')
>>> m
<_sre.SRE_Match object at 0x1005d27e8>
>>> m.group()
'11/27/2012'
>>>

如果你想精確匹配,確保你的正則表達(dá)式以$結(jié)尾,就像這么這樣:

>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)$')
>>> datepat.match('11/27/2012abcdef')
>>> datepat.match('11/27/2012')
<_sre.SRE_Match object at 0x1005d2750>
>>>

最后,如果你僅僅是做一次簡單的文本匹配/搜索操作的話,可以略過編譯部分,直接使用 re 模塊級別的函數(shù)。比如:

>>> re.findall(r'(\d+)/(\d+)/(\d+)', text)
[('11', '27', '2012'), ('3', '13', '2013')]
>>>

但是需要注意的是,如果你打算做大量的匹配和搜索操作的話,最好先編譯正則表達(dá)式,然后再重復(fù)使用它。 模塊級別的函數(shù)會將最近編譯過的模式緩存起來,因此并不會消耗太多的性能, 但是如果使用預(yù)編譯模式的話,你將會減少查找和一些額外的處理損耗。

字符串搜索和替換

問題

你想在字符串中搜索和匹配指定的文本模式

解決方案

對于簡單的字面模式,直接使用 str.repalce() 方法即可,比如:

>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> text.replace('yeah', 'yep')
'yep, but no, but yep, but no, but yep'
>>>

對于復(fù)雜的模式,請使用 re 模塊中的 sub() 函數(shù)。 為了說明這個,假設(shè)你想將形式為 11/27/201 的日期字符串改成 2012-11-27 。示例如下:

>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> import re
>>> re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>>

sub() 函數(shù)中的第一個參數(shù)是被匹配的模式,第二個參數(shù)是替換模式。反斜杠數(shù)字比如 \3 指向前面模式的捕獲組號。

如果你打算用相同的模式做多次替換,考慮先編譯它來提升性能。比如:

>>> import re
>>> datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
>>> datepat.sub(r'\3-\1-\2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>>

對于更加復(fù)雜的替換,可以傳遞一個替換回調(diào)函數(shù)來代替,比如:

>>> from calendar import month_abbr
>>> def change_date(m):
... mon_name = month_abbr[int(m.group(1))]
... return '{} {} {}'.format(m.group(2), mon_name, m.group(3))
...
>>> datepat.sub(change_date, text)
'Today is 27 Nov 2012. PyCon starts 13 Mar 2013.'
>>>

一個替換回調(diào)函數(shù)的參數(shù)是一個 match 對象,也就是 match() 或者 find() 返回的對象。 使用 group() 方法來提取特定的匹配部分。回調(diào)函數(shù)最后返回替換字符串。

如果除了替換后的結(jié)果外,你還想知道有多少替換發(fā)生了,可以使用 re.subn() 來代替。比如:

>>> newtext, n = datepat.subn(r'\3-\1-\2', text)
>>> newtext
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>> n
2
>>>

討論

關(guān)于正則表達(dá)式搜索和替換,上面演示的 sub() 方法基本已經(jīng)涵蓋了所有。 其實最難的部分就是編寫正則表達(dá)式模式,這個最好是留給作者自己去練習(xí)了。

字符串忽略大小寫的搜索替換

問題

你需要以忽略大小寫的方式搜索與替換文本字符串

解決方案

為了在文本操作時忽略大小寫,你需要在使用 re 模塊的時候給這些操作提供 re.IGNORECASE 標(biāo)志參數(shù)。比如:

>>> text = 'UPPER PYTHON, lower python, Mixed Python'
>>> re.findall('python', text, flags=re.IGNORECASE)
['PYTHON', 'python', 'Python']
>>> re.sub('python', 'snake', text, flags=re.IGNORECASE)
'UPPER snake, lower snake, Mixed snake'
>>>

最后的那個例子揭示了一個小缺陷,替換字符串并不會自動跟被匹配字符串的大小寫保持一致。 為了修復(fù)這個,你可能需要一個輔助函數(shù),就像下面的這樣:

def matchcase(word):
    def replace(m):
        text = m.group()
        if text.isupper():
            return word.upper()
        elif text.islower():
            return word.lower()
        elif text[0].isupper():
            return word.capitalize()
        else:
            return word
    return replace

下面是使用上述函數(shù)的方法:

>>> re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE)
'UPPER SNAKE, lower snake, Mixed Snake'
>>>

譯者注: matchcase('snake') 返回了一個回調(diào)函數(shù)(參數(shù)必須是match 對象),前面一節(jié)一節(jié)提到過, sub() 函數(shù)除了接受替換字符串外,還能接受一個回調(diào)函數(shù)。

討論

對于一般的忽略大小寫的匹配操作,簡單的傳遞一個 re.IGNORECASE 標(biāo)志參數(shù)就已經(jīng)足夠了。 但是需要注意的是,這個對于某些需要大小寫轉(zhuǎn)換的 Unicode 匹配可能還不夠, 參考2.10小節(jié)了解更多細(xì)節(jié)。

最短匹配模式

問題

你正在試著用正則表達(dá)式匹配某個文本模式,但是它找到的是模式的最長可能匹配。 而你想修改它變成查找最短的可能匹配。

解決方案

這個問題一般出現(xiàn)在需要匹配一對分隔符之間的文本的時候(比如引號包含的字符串)。 為了說明清楚,考慮如下的例子:

>>> str_pat = re.compile(r'\"(.*)\"')
>>> text1 = 'Computer says "no."'
>>> str_pat.findall(text1)
['no.']
>>> text2 = 'Computer says "no." Phone says "yes."'
>>> str_pat.findall(text2)
['no." Phone says "yes.']
>>>

在這個例子中,模式 r'\"(.*)\"' 的意圖是匹配被雙引號包含的文本。 但是在正則表達(dá)式中*操作符是貪婪的,因此匹配操作會查找最長的可能匹配。 于是在第二個例子中搜索 text2 的時候返回結(jié)果并不是我們想要的。

為了修正這個問題,可以在模式中的*操作符后面加上?修飾符,就像這樣:

>>> str_pat = re.compile(r'\"(.*?)\"')
>>> str_pat.findall(text2)
['no.', 'yes.']
>>>

這樣就使得匹配變成非貪婪模式,從而得到最短的匹配,也就是我們想要的結(jié)果。

討論

這一節(jié)展示了在寫包含點(.)字符的正則表達(dá)式的時候遇到的一些常見問題。 在一個模式字符串中,點(.)匹配除了換行外的任何字符。 然而,如果你將點(.)號放在開始與結(jié)束符(比如引號)之間的時候,那么匹配操作會查找符合模式的最長可能匹配。 這樣通常會導(dǎo)致很多中間的被開始與結(jié)束符包含的文本被忽略掉,并最終被包含在匹配結(jié)果字符串中返回。 通過在 * 或者 + 這樣的操作符后面添加一個 ? 可以強制匹配算法改成尋找最短的可能匹配。

多行匹配模式

問題

你正在試著使用正則表達(dá)式去匹配一大塊的文本,而你需要跨越多行去匹配。

解決方案

這個問題很典型的出現(xiàn)在當(dāng)你用點(.)去匹配任意字符的時候,忘記了點(.)不能匹配換行符的事實。 比如,假設(shè)你想試著去匹配 C 語言分割的注釋:

>>> comment = re.compile(r'/\*(.*?)\*/')
>>> text1 '
>>> text2 = '''/* this is a
... multiline comment */
... '''
>>>
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[]
>>>

為了修正這個問題,你可以修改模式字符串,增加對換行的支持。比如:

>>> comment = re.compile(r'/\*((?:.|\n)*?)\*/')
>>> comment.findall(text2)
[' this is a\n multiline comment ']
>>>

在這個模式中, (?:.|\n)指定了一個非捕獲組 (也就是它定義了一個僅僅用來做匹配,而不能通過單獨捕獲或者編號的組)。

討論

re.compile() 函數(shù)接受一個標(biāo)志參數(shù)叫 re.DOTALL ,在這里非常有用。 它可以讓正則表達(dá)式中的點(.)匹配包括換行符在內(nèi)的任意字符。比如:

>>> comment = re.compile(r'/\*(.*?)\*/', re.DOTALL)
>>> comment.findall(text2)
[' this is a\n multiline comment ']

對于簡單的情況使用re.DOTALL 標(biāo)記參數(shù)工作的很好, 但是如果模式非常復(fù)雜或者是為了構(gòu)造字符串令牌而將多個模式合并起來(2.18節(jié)有詳細(xì)描述), 這時候使用這個標(biāo)記參數(shù)就可能出現(xiàn)一些問題。 如果讓你選擇的話,最好還是定義自己的正則表達(dá)式模式,這樣它可以在不需要額外的標(biāo)記參數(shù)下也能工作的很好。

將 Unicode 文本標(biāo)準(zhǔn)化

問題

你正在處理 Unicode 字符串,需要確保所有字符串在底層有相同的表示。

解決方案

在 Unicode 中,某些字符能夠用多個合法的編碼表示。為了說明,考慮下面的這個例子:

>>> s1 = 'Spicy Jalape\u00f1o'
>>> s2 = 'Spicy Jalapen\u0303o'
>>> s1
'Spicy Jalape?o'
>>> s2
'Spicy Jalape?o'
>>> s1 == s2
False
>>> len(s1)
14
>>> len(s2)
15
>>>

這里的文本”Spicy Jalape?o”使用了兩種形式來表示。 第一種使用整體字符”?”(U+00F1),第二種使用拉丁字母”n”后面跟一個”~”的組合字符(U+0303)。

在需要比較字符串的程序中使用字符的多種表示會產(chǎn)生問題。 為了修正這個問題,你可以使用 unicodedata 模塊先將文本標(biāo)準(zhǔn)化:

>>> import unicodedata
>>> t1 = unicodedata.normalize('NFC', s1)
>>> t2 = unicodedata.normalize('NFC', s2)
>>> t1 == t2
True
>>> print(ascii(t1))
'Spicy Jalape\xf1o'
>>> t3 = unicodedata.normalize('NFD', s1)
>>> t4 = unicodedata.normalize('NFD', s2)
>>> t3 == t4
True
>>> print(ascii(t3))
'Spicy Jalapen\u0303o'
>>>

normalize()第一個參數(shù)指定字符串標(biāo)準(zhǔn)化的方式。 NFC 表示字符應(yīng)該是整體組成(比如可能的話就使用單一編碼),而 NFD 表示字符應(yīng)該分解為多個組合字符表示。

Python 同樣支持?jǐn)U展的標(biāo)準(zhǔn)化形式 NFKC 和 NFKD,它們在處理某些字符的時候增加了額外的兼容特性。比如:

>>> s = '\ufb01' # A single character
>>> s
'?'
>>> unicodedata.normalize('NFD', s)
'?'
# Notice how the combined letters are broken apart here
>>> unicodedata.normalize('NFKD', s)
'fi'
>>> unicodedata.normalize('NFKC', s)
'fi'
>>>

討論

標(biāo)準(zhǔn)化對于任何需要以一致的方式處理 Unicode 文本的程序都是非常重要的。 當(dāng)處理來自用戶輸入的字符串而你很難去控制編碼的時候尤其如此。

在清理和過濾文本的時候字符的標(biāo)準(zhǔn)化也是很重要的。 比如,假設(shè)你想清除掉一些文本上面的變音符的時候(可能是為了搜索和匹配):

>>> t1 = unicodedata.normalize('NFD', s1)
>>> ''.join(c for c in t1 if not unicodedata.combining(c))
'Spicy Jalapeno'
>>>

最后一個例子展示了 unicodedata 模塊的另一個重要方面,也就是測試字符類的工具函數(shù)。 combining() 函數(shù)可以測試一個字符是否為和音字符。 在這個模塊中還有其他函數(shù)用于查找字符類別,測試是否為數(shù)字字符等等。

Unicode 顯然是一個很大的主題。如果想更深入的了解關(guān)于標(biāo)準(zhǔn)化方面的信息, 請看考 Unicode 官網(wǎng)中關(guān)于這部分的說明 Ned Batchelder 在他的網(wǎng)站上對 Python 的 Unicode 處理問題也有一個很好的介紹。

在正則式中使用 Unicode

問題

你正在使用正則表達(dá)式處理文本,但是關(guān)注的是 Unicode 字符處理。

解決方案

默認(rèn)情況下 re 模塊已經(jīng)對一些 Unicode 字符類有了基本的支持。 比如, \\d已經(jīng)匹配任意的 unicode 數(shù)字字符了:

>>> import re
>>> num = re.compile('\d+')
>>> # ASCII digits
>>> num.match('123')
<_sre.SRE_Match object at 0x1007d9ed0>
>>> # Arabic digits
>>> num.match('\u0661\u0662\u0663')
<_sre.SRE_Match object at 0x101234030>
>>>

如果你想在模式中包含指定的 Unicode 字符,你可以使用 Unicode 字符對應(yīng)的轉(zhuǎn)義序列(比如 \uFFF 或者 \UFFFFFFF)。 比如,下面是一個匹配幾個不同阿拉伯編碼頁面中所有字符的正則表達(dá)式:

>>> arabic = re.compile('[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff]+')
>>>

當(dāng)執(zhí)行匹配和搜索操作的時候,最好是先標(biāo)準(zhǔn)化并且清理所有文本為標(biāo)準(zhǔn)化格式(參考2.9小節(jié))。 但是同樣也應(yīng)該注意一些特殊情況,比如在忽略大小寫匹配和大小寫轉(zhuǎn)換時的行為。

>>> pat = re.compile('stra\u00dfe', re.IGNORECASE)
>>> s = 'stra?e'
>>> pat.match(s) # Matches
<_sre.SRE_Match object at 0x10069d370>
>>> pat.match(s.upper()) # Doesn't match
>>> s.upper() # Case folds
'STRASSE'
>>>

討論

混合使用 Unicode 和正則表達(dá)式通常會讓你抓狂。 如果你真的打算這樣做的話,最好考慮下安裝第三方正則式庫, 它們會為 Unicode 的大小寫轉(zhuǎn)換和其他大量有趣特性提供全面的支持,包括模糊匹配。

刪除字符串中不需要的字符

問題

你想去掉文本字符串開頭,結(jié)尾或者中間不想要的字符,比如空白。

解決方案

strip() 方法能用于刪除開始或結(jié)尾的字符。lstrip()rstrip() 分別從左和從右執(zhí)行刪除操作。 默認(rèn)情況下,這些方法會去除空白字符,但是你也可以指定其他字符。比如:

>>> # Whitespace stripping
>>> s = ' hello world \n'
>>> s.strip()
'hello world'
>>> s.lstrip()
'hello world \n'
>>> s.rstrip()
' hello world'
>>>
>>> # Character stripping
>>> t = '-----hello====='
>>> t.lstrip('-')
'hello====='
>>> t.strip('-=')
'hello'
>>>

討論

這些 strip() 方法在讀取和清理數(shù)據(jù)以備后續(xù)處理的時候是經(jīng)常會被用到的。 比如,你可以用它們來去掉空格,引號和完成其他任務(wù)。

但是需要注意的是去除操作不會對字符串的中間的文本產(chǎn)生任何影響。比如:

>>> s = ' hello     world \n'
>>> s = s.strip()
>>> s
'hello     world'
>>>

如果你想處理中間的空格,那么你需要求助其他技術(shù)。比如使用 replace() 方法或者是用正則表達(dá)式替換。示例如下:

>>> s.replace(' ', '')
'helloworld'
>>> import re
>>> re.sub('\s+', ' ', s)
'hello world'
>>>

通常情況下你想將字符串 strip 操作和其他迭代操作相結(jié)合,比如從文件中讀取多行數(shù)據(jù)。 如果是這樣的話,那么生成器表達(dá)式就可以大顯身手了。比如:

with open(filename) as f:
    lines = (line.strip() for line in f)
    for line in lines:
        print(line)

在這里,表達(dá)式lines = (line.strip() for line in f)執(zhí)行數(shù)據(jù)轉(zhuǎn)換操作。 這種方式非常高效,因為它不需要預(yù)先讀取所有數(shù)據(jù)放到一個臨時的列表中去。 它僅僅只是創(chuàng)建一個生成器,并且每次返回行之前會先執(zhí)行 strip 操作。

對于更高階的 strip,你可能需要使用 translate() 方法。請參閱下一節(jié)了解更多關(guān)于字符串清理的內(nèi)容。

審查清理文本字符串

問題

一些無聊的幼稚黑客在你的網(wǎng)站頁面表單中輸入文本”pyt???”,然后你想將這些字符清理掉。

解決方案

文本清理問題會涉及到包括文本解析與數(shù)據(jù)處理等一系列問題。 在非常簡單的情形下,你可能會選擇使用字符串函數(shù)(比如 str.upper()str.lower() )將文本轉(zhuǎn)為標(biāo)準(zhǔn)格式。 使用 str.replace() 或者 re.sub() 的簡單替換操作能刪除或者改變指定的字符序列。 你同樣還可以使用2.9小節(jié)的 unicodedata.normalize() 函數(shù)將 unicode 文本標(biāo)準(zhǔn)化。

然后,有時候你可能還想在清理操作上更進(jìn)一步。比如,你可能想消除整個區(qū)間上的字符或者去除變音符。 為了這樣做,你可以使用經(jīng)常會被忽視的 str.translate() 方法。 為了演示,假設(shè)你現(xiàn)在有下面這個凌亂的字符串:

>>> s = 'pyt???\fis\tawesome\r\n'
>>> s
'pyt???\x0cis\tawesome\r\n'
>>>

第一步是清理空白字符。為了這樣做,先創(chuàng)建一個小的轉(zhuǎn)換表格然后使用 translate() 方法:

>>> remap = {
...     ord('\t') : ' ',
...     ord('\f') : ' ',
...     ord('\r') : None # Deleted
... }
>>> a = s.translate(remap)
>>> a
'pyt??? is awesome\n'
>>>

正如你看的那樣,空白字符 \t\f 已經(jīng)被重新映射到一個空格?;剀囎址?r 直接被刪除。

你可以以這個表格為基礎(chǔ)進(jìn)一步構(gòu)建更大的表格。比如,讓我們刪除所有的和音符:

>>> import unicodedata
>>> import sys
>>> cmb_chrs = dict.fromkeys(c for c in range(sys.maxunicode)
...                         if unicodedata.combining(chr(c)))
...
>>> b = unicodedata.normalize('NFD', a)
>>> b
'pyt??? is awesome\n'
>>> b.translate(cmb_chrs)
'python is awesome\n'
>>>

上面例子中,通過使用 dict.fromkeys() 方法構(gòu)造一個字典,每個 Unicode 和音符作為鍵,對于的值全部為 None 。

然后使用 unicodedata.normalize() 將原始輸入標(biāo)準(zhǔn)化為分解形式字符。 然后再調(diào)用 translate 函數(shù)刪除所有重音符。 同樣的技術(shù)也可以被用來刪除其他類型的字符(比如控制字符等)。

作為另一個例子,這里構(gòu)造一個將所有 Unicode 數(shù)字字符映射到對應(yīng)的 ASCII 字符上的表格:

>>> digitmap = { c: ord('0') + unicodedata.digit(chr(c))
...         for c in range(sys.maxunicode)
...         if unicodedata.category(chr(c)) == 'Nd' }
...
>>> len(digitmap)
460
>>> # Arabic digits
>>> x = '\u0661\u0662\u0663'
>>> x.translate(digitmap)
'123'
>>>

另一種清理文本的技術(shù)涉及到 I/O 解碼與編碼函數(shù)。這里的思路是先對文本做一些初步的清理, 然后再結(jié)合 encode() 或者 decode()操作來清除或修改它。比如:

>>> a
'pyt??? is awesome\n'
>>> b = unicodedata.normalize('NFD', a)
>>> b.encode('ascii', 'ignore').decode('ascii')
'python is awesome\n'
>>>

這里的標(biāo)準(zhǔn)化操作將原來的文本分解為單獨的和音符。接下來的 ASCII 編碼/解碼只是簡單的一下子丟棄掉那些字符。 當(dāng)然,這種方法僅僅只在最后的目標(biāo)就是獲取到文本對應(yīng) ACSII 表示的時候生效。

討論

文本字符清理一個最主要的問題應(yīng)該是運行的性能。一般來講,代碼越簡單運行越快。 對于簡單的替換操作, str.replace() 方法通常是最快的,甚至在你需要多次調(diào)用的時候。 比如,為了清理空白字符,你可以這樣做:

def clean_spaces(s):
    s = s.replace('\r', '')
    s = s.replace('\t', ' ')
    s = s.replace('\f', ' ')
    return s

如果你去測試的話,你就會發(fā)現(xiàn)這種方式會比使用 translate() 或者正則表達(dá)式要快很多。

另一方面,如果你需要執(zhí)行任何復(fù)雜字符對字符的重新映射或者刪除操作的話, tanslate() 方法會非常的快。

從大的方面來講,對于你的應(yīng)用程序來說性能是你不得不去自己研究的東西。 不幸的是,我們不可能給你建議一個特定的技術(shù),使它能夠適應(yīng)所有的情況。 因此實際情況中需要你自己去嘗試不同的方法并評估它。

盡管這一節(jié)集中討論的是文本,但是類似的技術(shù)也可以適用于字節(jié),包括簡單的替換,轉(zhuǎn)換和正則表達(dá)式。

字符串對齊

問題

你想通過某種對齊方式來格式化字符串

解決方案

對于基本的字符串對齊操作,可以使用字符串的 ljust() , rjust()center() 方法。比如:

>>> text = 'Hello World'
>>> text.ljust(20)
'Hello World         '
>>> text.rjust(20)
'         Hello World'
>>> text.center(20)
'    Hello World     '
>>>

所有這些方法都能接受一個可選的填充字符。比如:

>>> text.rjust(20,'=')
'=========Hello World'
>>> text.center(20,'*')
'****Hello World*****'
>>>

函數(shù) format() 同樣可以用來很容易的對齊字符串。 你要做的就是使用 <,>或者 ^ 字符后面緊跟一個指定的寬度。比如:

>>> format(text, '>20')
'         Hello World'
>>> format(text, '<20')
'Hello World         '
>>> format(text, '^20')
'    Hello World     '
>>>

如果你想指定一個非空格的填充字符,將它寫到對齊字符的前面即可:

>>> format(text, '=>20s')
'=========Hello World'
>>> format(text, '*^20s')
'****Hello World*****'
>>>

當(dāng)格式化多個值的時候,這些格式代碼也可以被用在 format()方法中。比如:

>>> '{:>10s} {:>10s}'.format('Hello', 'World')
'     Hello      World'
>>>

format() 函數(shù)的一個好處是它不僅適用于字符串。它可以用來格式化任何值,使得它非常的通用。 比如,你可以用它來格式化數(shù)字:

>>> x = 1.2345
>>> format(x, '>10')
'    1.2345'
>>> format(x, '^10.2f')
'   1.23   '
>>>

討論

在老的代碼中,你經(jīng)常會看到被用來格式化文本的% 操作符。比如:

>>> '%-20s' % text
'Hello World         '
>>> '%20s' % text
'         Hello World'
>>>

但是,在新版本代碼中,你應(yīng)該優(yōu)先選擇 format() 函數(shù)或者方法。 format() 要比 % 操作符的功能更為強大。 并且 format()也比使用 ljust(), rjust()center()方法更通用, 因為它可以用來格式化任意對象,而不僅僅是字符串。

如果想要完全了解 format()函數(shù)的有用特性, 請參考在線 Python 文檔

合并拼接字符串

問題

你想將幾個小的字符串合并為一個大的字符串

解決方案

如果你想要合并的字符串是在一個序列或者 iterable 中,那么最快的方式就是使用 join() 方法。比如:

>>> parts = ['Is', 'Chicago', 'Not', 'Chicago?']
>>> ' '.join(parts)
'Is Chicago Not Chicago?'
>>> ','.join(parts)
'Is,Chicago,Not,Chicago?'
>>> ''.join(parts)
'IsChicagoNotChicago?'
>>>

初看起來,這種語法看上去會比較怪,但是 join()被指定為字符串的一個方法。 這樣做的部分原因是你想去連接的對象可能來自各種不同的數(shù)據(jù)序列(比如列表,元組,字典,文件,集合或生成器等), 如果在所有這些對象上都定義一個 join() 方法明顯是冗余的。 因此你只需要指定你想要的分割字符串并調(diào)用他的 join() 方法去將文本片段組合起來。

如果你僅僅只是合并少數(shù)幾個字符串,使用加號(+)通常已經(jīng)足夠了:

>>> a = 'Is Chicago'
>>> b = 'Not Chicago?'
>>> a + ' ' + b
'Is Chicago Not Chicago?'
>>>

加號(+)操作符在作為一些復(fù)雜字符串格式化的替代方案的時候通常也工作的很好,比如:

>>> print('{} {}'.format(a,b))
Is Chicago Not Chicago?
>>> print(a + ' ' + b)
Is Chicago Not Chicago?
>>>

如果你想在源碼中將兩個字面字符串合并起來,你只需要簡單的將它們放到一起,不需要用加號(+)。比如:

>>> a = 'Hello' 'World'
>>> a
'HelloWorld'
>>>

討論

字符串合并可能看上去并不需要用一整節(jié)來討論。 但是不應(yīng)該小看這個問題,程序員通常在字符串格式化的時候因為選擇不當(dāng)而給應(yīng)用程序帶來嚴(yán)重性能損失。

最重要的需要引起注意的是,當(dāng)我們使用加號(+)操作符去連接大量的字符串的時候是非常低效率的, 因為加號連接會引起內(nèi)存復(fù)制以及垃圾回收操作。 特別的,你永遠(yuǎn)都不應(yīng)像下面這樣寫字符串連接代碼:

s = ''
for p in parts:
    s += p

這種寫法會比使用 join() 方法運行的要慢一些,因為每一次執(zhí)行+=操作的時候會創(chuàng)建一個新的字符串對象。 你最好是先收集所有的字符串片段然后再將它們連接起來。

一個相對比較聰明的技巧是利用生成器表達(dá)式(參考1.19小節(jié))轉(zhuǎn)換數(shù)據(jù)為字符串的同時合并字符串,比如:

>>> data = ['ACME', 50, 91.1]
>>> ','.join(str(d) for d in data)
'ACME,50,91.1'
>>>

同樣還得注意不必要的字符串連接操作。有時候程序員在沒有必要做連接操作的時候仍然多此一舉。比如在打印的時候:

print(a + ':' + b + ':' + c) # Ugly
print(':'.join([a, b, c])) # Still ugly
print(a, b, c, sep=':') # Better

當(dāng)混合使用 I/O 操作和字符串連接操作的時候,有時候需要仔細(xì)研究你的程序。 比如,考慮下面的兩端代碼片段:

# Version 1 (string concatenation)
f.write(chunk1 + chunk2)

# Version 2 (separate I/O operations)
f.write(chunk1)
f.write(chunk2)

如果兩個字符串很小,那么第一個版本性能會更好些,因為 I/O 系統(tǒng)調(diào)用天生就慢。 另外一方面,如果兩個字符串很大,那么第二個版本可能會更加高效, 因為它避免了創(chuàng)建一個很大的臨時結(jié)果并且要復(fù)制大量的內(nèi)存塊數(shù)據(jù)。 還是那句話,有時候是需要根據(jù)你的應(yīng)用程序特點來決定應(yīng)該使用哪種方案。

最后談一下,如果你準(zhǔn)備編寫構(gòu)建大量小字符串的輸出代碼, 你最好考慮下使用生成器函數(shù),利用 yield 語句產(chǎn)生輸出片段。比如:

def sample():
    yield 'Is'
    yield 'Chicago'
    yield 'Not'
    yield 'Chicago?'

這種方法一個有趣的方面是它并沒有對輸出片段到底要怎樣組織做出假設(shè)。 例如,你可以簡單的使用 join() 方法將這些片段合并起來:

text = ''.join(sample())

或者你也可以將字符串片段重定向到 I/O:

for part in sample():
    f.write(part)

再或者你還可以寫出一些結(jié)合I/O操作的混合方案:

def combine(source, maxsize):
    parts = []
    size = 0
    for part in source:
        parts.append(part)
        size += len(part)
        if size > maxsize:
            yield ''.join(parts)
            parts = []
            size = 0
        yield ''.join(parts)

# 結(jié)合文件操作
with open('filename', 'w') as f:
    for part in combine(sample(), 32768):
        f.write(part)

這里的關(guān)鍵點在于原始的生成器函數(shù)并不需要知道使用細(xì)節(jié),它只負(fù)責(zé)生成字符串片段就行了。

字符串中插入變量

問題

你想創(chuàng)建一個內(nèi)嵌變量的字符串,變量被它的值所表示的字符串替換掉。

解決方案

Python 并沒有對在字符串中簡單替換變量值提供直接的支持。 但是通過使用字符串的 format() 方法來解決這個問題。比如:

>>> s = '{name} has {n} messages.'
>>> s.format(name='Guido', n=37)
'Guido has 37 messages.'
>>>

或者,如果要被替換的變量能在變量域中找到, 那么你可以結(jié)合使用 format_map()vars() 。就像下面這樣:

>>> name = 'Guido'
>>> n = 37
>>> s.format_map(vars())
'Guido has 37 messages.'
>>>

vars()還有一個有意思的特性就是它也適用于對象實例。比如:

>>> class Info:
...     def __init__(self, name, n):
...         self.name = name
...         self.n = n
...
>>> a = Info('Guido',37)
>>> s.format_map(vars(a))
'Guido has 37 messages.'
>>>

formatformat_map() 的一個缺陷就是它們并不能很好的處理變量缺失的情況,比如:

>>> s.format(name='Guido')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'n'
>>>

一種避免這種錯誤的方法是另外定義一個含有 __missing__() 方法的字典對象,就像下面這樣:

class safesub(dict):
"""防止key找不到"""
def __missing__(self, key):
    return '{' + key + '}'

現(xiàn)在你可以利用這個類包裝輸入后傳遞給 format_map()

>>> del n # Make sure n is undefined
>>> s.format_map(safesub(vars()))
'Guido has {n} messages.'
>>>

如果你發(fā)現(xiàn)自己在代碼中頻繁的執(zhí)行這些步驟,你可以將變量替換步驟用一個工具函數(shù)封裝起來。就像下面這樣:

import sys

def sub(text):
    return text.format_map(safesub(sys._getframe(1).f_locals))

現(xiàn)在你可以像下面這樣寫了:

>>> name = 'Guido'
>>> n = 37
>>> print(sub('Hello {name}'))
Hello Guido
>>> print(sub('You have {n} messages.'))
You have 37 messages.
>>> print(sub('Your favorite color is {color}'))
Your favorite color is {color}
>>>

討論

多年以來由于 Python 缺乏對變量替換的內(nèi)置支持而導(dǎo)致了各種不同的解決方案。 作為本節(jié)中展示的一個可能的解決方案,你可以有時候會看到像下面這樣的字符串格式化代碼:

>>> name = 'Guido'
>>> n = 37
>>> '%(name) has %(n) messages.' % vars()
'Guido has 37 messages.'
>>>

你可能還會看到字符串模板的使用:

>>> import string
>>> s = string.Template('$name has $n messages.')
>>> s.substitute(vars())
'Guido has 37 messages.'
>>>

然而, format()format_map() 相比較上面這些方案而已更加先進(jìn),因此應(yīng)該被優(yōu)先選擇。 使用 format() 方法還有一個好處就是你可以獲得對字符串格式化的所有支持(對齊,填充,數(shù)字格式化等待), 而這些特性是使用像模板字符串之類的方案不可能獲得的。

本機還部分介紹了一些高級特性。映射或者字典類中鮮為人知的 __missing__() 方法可以讓你定義如何處理缺失的值。 在 SafeSub 類中,這個方法被定義為對缺失的值返回一個占位符。 你可以發(fā)現(xiàn)缺失的值會出現(xiàn)在結(jié)果字符串中(在調(diào)試的時候可能很有用),而不是產(chǎn)生一個 KeyError 異常。

sub()函數(shù)使用sys._getframe(1)返回調(diào)用者的棧幀??梢詮闹性L問屬性 f_locals來獲得局部變量。 毫無疑問絕大部分情況下在代碼中去直接操作棧幀應(yīng)該是不推薦的。 但是,對于像字符串替換工具函數(shù)而言它是非常有用的。 另外,值得注意的是

上一篇:文件與 IO下一篇:元編程