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

字符串和文本

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

使用多個(gè)界定符分割字符串

問題

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

解決方案

string 對(duì)象的 split() 方法只適應(yīng)于非常簡(jiǎn)單的字符串分割情形, 它并不允許有多個(gè)分隔符或者是分隔符周圍不確定的空格。 當(dāng)你需要更加靈活的切割字符串的時(shí)候,最好使用 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() 是非常實(shí)用的,因?yàn)樗试S你為分隔符指定多個(gè)正則模式。 比如,在上面的例子中,分隔符可以是逗號(hào),分號(hào)或者是空格,并且后面緊跟著任意個(gè)的空格。 只要這個(gè)模式被找到,那么匹配的分隔符兩邊的實(shí)體都會(huì)被當(dāng)成是結(jié)果中的元素返回。 返回結(jié)果為一個(gè)字段列表,這個(gè)跟 str.split() 返回值類型是一樣的。

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

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

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

>>> 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é)果列表中去,但仍然需要使用到括號(hào)來分組正則表達(dá)式的話, 確保你的分組是非捕獲分組,形如 (?:...) 。比如:

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

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

問題

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

解決方案

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

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

如果你想檢查多種匹配可能,只需要將所有的匹配項(xiàng)放入到一個(gè)元組中去, 然后傳給 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
>>>

下面是另一個(gè)例子:

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()

奇怪的是,這個(gè)方法中必須要輸入一個(gè)元組作為參數(shù)。 如果你恰巧有一個(gè) list 或者 set 類型的選擇項(xiàng), 要確保傳遞參數(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() 方法提供了一個(gè)非常方便的方式去做字符串開頭和結(jié)尾的檢查。 類似的操作也可以使用切片來實(shí)現(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á)式去實(shí)現(xiàn),比如:

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

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

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

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

用 Shell 通配符匹配字符串

問題

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

解決方案

fnmatch 模塊提供了兩個(gè)函數(shù)—— fnmatch()fnmatchcase(),可以用來實(shí)現(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
>>>

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

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

這兩個(gè)函數(shù)通常會(huì)被忽略的一個(gè)特性是在處理非文件名的字符串時(shí)候它們也是很有用的。 比如,假設(shè)你有一個(gè)街道地址的列表數(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ù)匹配能力介于簡(jiǎn)單的字符串方法和強(qiáng)大的正則表達(dá)式之間。 如果在數(shù)據(jù)處理操作中只需要簡(jiǎn)單的通配符就能完成的時(shí)候,這通常是一個(gè)比較合理的方案。

如果你的代碼需要做文件名的匹配,最好使用 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
>>>

對(duì)于復(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
>>>

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

>>> 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']
>>>

在定義正則式的時(shí)候,通常會(huì)利用括號(hào)去捕獲分組。比如:

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

捕獲分組可以使得后面的處理更加簡(jiǎn)單,因?yàn)榭梢苑謩e將每個(gè)組的內(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()方法會(huì)搜索文本并以列表形式返回所有的匹配。 如果你想以迭代方式返回匹配,可以使用 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)寫正則式字符串的時(shí)候,相對(duì)普遍的做法是使用原始字符串比如 r'(\d+)/(\d+)/(\d+)' 。 這種字符串將不去解析反斜杠,這在正則表達(dá)式中是很有用的。 如果不這樣做的話,你必須使用兩個(gè)反斜杠,類似 '(\\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>
>>>

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

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

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

字符串搜索和替換

問題

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

解決方案

對(duì)于簡(jiǎn)單的字面模式,直接使用 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'
>>>

對(duì)于復(fù)雜的模式,請(qǐng)使用 re 模塊中的 sub() 函數(shù)。 為了說明這個(gè),假設(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ù)中的第一個(gè)參數(shù)是被匹配的模式,第二個(gè)參數(shù)是替換模式。反斜杠數(shù)字比如 \3 指向前面模式的捕獲組號(hào)。

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

>>> 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.'
>>>

對(duì)于更加復(fù)雜的替換,可以傳遞一個(gè)替換回調(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.'
>>>

一個(gè)替換回調(diào)函數(shù)的參數(shù)是一個(gè) match 對(duì)象,也就是 match() 或者 find() 返回的對(duì)象。 使用 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)涵蓋了所有。 其實(shí)最難的部分就是編寫正則表達(dá)式模式,這個(gè)最好是留給作者自己去練習(xí)了。

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

問題

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

解決方案

為了在文本操作時(shí)忽略大小寫,你需要在使用 re 模塊的時(shí)候給這些操作提供 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'
>>>

最后的那個(gè)例子揭示了一個(gè)小缺陷,替換字符串并不會(huì)自動(dòng)跟被匹配字符串的大小寫保持一致。 為了修復(fù)這個(gè),你可能需要一個(gè)輔助函數(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') 返回了一個(gè)回調(diào)函數(shù)(參數(shù)必須是match 對(duì)象),前面一節(jié)一節(jié)提到過, sub() 函數(shù)除了接受替換字符串外,還能接受一個(gè)回調(diào)函數(shù)。

討論

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

最短匹配模式

問題

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

解決方案

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

>>> 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.']
>>>

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

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

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

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

討論

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

多行匹配模式

問題

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

解決方案

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

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

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

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

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

討論

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

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

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

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

問題

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

解決方案

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

>>> 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”后面跟一個(gè)”~”的組合字符(U+0303)。

在需要比較字符串的程序中使用字符的多種表示會(huì)產(chǎn)生問題。 為了修正這個(gè)問題,你可以使用 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()第一個(gè)參數(shù)指定字符串標(biāo)準(zhǔn)化的方式。 NFC 表示字符應(yīng)該是整體組成(比如可能的話就使用單一編碼),而 NFD 表示字符應(yīng)該分解為多個(gè)組合字符表示。

Python 同樣支持?jǐn)U展的標(biāo)準(zhǔn)化形式 NFKC 和 NFKD,它們?cè)谔幚砟承┳址臅r(shí)候增加了額外的兼容特性。比如:

>>> 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)化對(duì)于任何需要以一致的方式處理 Unicode 文本的程序都是非常重要的。 當(dāng)處理來自用戶輸入的字符串而你很難去控制編碼的時(shí)候尤其如此。

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

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

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

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

在正則式中使用 Unicode

問題

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

解決方案

默認(rèn)情況下 re 模塊已經(jīng)對(duì)一些 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 字符對(duì)應(yīng)的轉(zhuǎn)義序列(比如 \uFFF 或者 \UFFFFFFF)。 比如,下面是一個(gè)匹配幾個(gè)不同阿拉伯編碼頁面中所有字符的正則表達(dá)式:

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

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

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

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

問題

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

解決方案

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

>>> # 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ù)處理的時(shí)候是經(jīng)常會(huì)被用到的。 比如,你可以用它們來去掉空格,引號(hào)和完成其他任務(wù)。

但是需要注意的是去除操作不會(huì)對(duì)字符串的中間的文本產(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àn)樗恍枰A(yù)先讀取所有數(shù)據(jù)放到一個(gè)臨時(shí)的列表中去。 它僅僅只是創(chuàng)建一個(gè)生成器,并且每次返回行之前會(huì)先執(zhí)行 strip 操作。

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

審查清理文本字符串

問題

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

解決方案

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

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

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

第一步是清理空白字符。為了這樣做,先創(chuàng)建一個(gè)小的轉(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)被重新映射到一個(gè)空格。回車字符 r 直接被刪除。

你可以以這個(gè)表格為基礎(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)造一個(gè)字典,每個(gè) Unicode 和音符作為鍵,對(duì)于的值全部為 None 。

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

作為另一個(gè)例子,這里構(gòu)造一個(gè)將所有 Unicode 數(shù)字字符映射到對(duì)應(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ù)。這里的思路是先對(duì)文本做一些初步的清理, 然后再結(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)化操作將原來的文本分解為單獨(dú)的和音符。接下來的 ASCII 編碼/解碼只是簡(jiǎn)單的一下子丟棄掉那些字符。 當(dāng)然,這種方法僅僅只在最后的目標(biāo)就是獲取到文本對(duì)應(yīng) ACSII 表示的時(shí)候生效。

討論

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

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

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

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

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

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

字符串對(duì)齊

問題

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

解決方案

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

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

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

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

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

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

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

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

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

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

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

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

討論

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

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

但是,在新版本代碼中,你應(yīng)該優(yōu)先選擇 format() 函數(shù)或者方法。 format() 要比 % 操作符的功能更為強(qiáng)大。 并且 format()也比使用 ljust(), rjust()center()方法更通用, 因?yàn)樗梢杂脕砀袷交我鈱?duì)象,而不僅僅是字符串。

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

合并拼接字符串

問題

你想將幾個(gè)小的字符串合并為一個(gè)大的字符串

解決方案

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

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

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

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

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

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

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

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

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

討論

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

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

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

這種寫法會(huì)比使用 join() 方法運(yùn)行的要慢一些,因?yàn)槊恳淮螆?zhí)行+=操作的時(shí)候會(huì)創(chuàng)建一個(gè)新的字符串對(duì)象。 你最好是先收集所有的字符串片段然后再將它們連接起來。

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

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

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

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

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

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

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

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

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

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

這種方法一個(gè)有趣的方面是它并沒有對(duì)輸出片段到底要怎樣組織做出假設(shè)。 例如,你可以簡(jiǎn)單的使用 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)鍵點(diǎn)在于原始的生成器函數(shù)并不需要知道使用細(xì)節(jié),它只負(fù)責(zé)生成字符串片段就行了。

字符串中插入變量

問題

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

解決方案

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

>>> 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()還有一個(gè)有意思的特性就是它也適用于對(duì)象實(shí)例。比如:

>>> 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() 的一個(gè)缺陷就是它們并不能很好的處理變量缺失的情況,比如:

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

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

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

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

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

如果你發(fā)現(xiàn)自己在代碼中頻繁的執(zhí)行這些步驟,你可以將變量替換步驟用一個(gè)工具函數(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 缺乏對(duì)變量替換的內(nèi)置支持而導(dǎo)致了各種不同的解決方案。 作為本節(jié)中展示的一個(gè)可能的解決方案,你可以有時(shí)候會(huì)看到像下面這樣的字符串格式化代碼:

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

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

>>> 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() 方法還有一個(gè)好處就是你可以獲得對(duì)字符串格式化的所有支持(對(duì)齊,填充,數(shù)字格式化等待), 而這些特性是使用像模板字符串之類的方案不可能獲得的。

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

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

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