天天看點

Python中的字元串和文本操作

Python中的字元串和文本操作

1.針對任意多的分隔符拆分字元串

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

import re
line = 'asdf fjdk; afed, fjek,asfd,   foo'
---函數 re.split() 是非常實用的,因為它允許你為分隔符指定多個正則模式。---
res_1 = re.split(r'[;,\s]\s*', line)
print(res_1)

--當你使用 re.split() 函數時候,需要特别注意的是正規表達式中的捕獲組是否包含在了括号中。
--如果使用了捕獲組,那麼被比對的文本也将出現在結果清單中。
res_2 = re.split(r'(;|,|\s)\s*', line)
print(res_2)

values = res_2[::2]
print(values)
--如果你不想保留分割字元串到結果清單中去,但仍然需要使用到括号來分組正則 表達式的話,確定你的分組是非捕獲分組,形如 (?:...)
#非捕獲組 (?:....)
res_3 = re.split(r'(?:;|,|\s)\s*', line)
print(res_3)
           

輸出:

['asdf', 'fjdk', 'afed', 'fjek', 'asfd', 'foo']
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asfd', ',', 'foo']
['asdf', 'fjdk', 'afed', 'fjek', 'asfd', 'foo']
['asdf', 'fjdk', 'afed', 'fjek', 'asfd', 'foo']
           

2.在字元串的開頭或結尾處做文本比對

檢查字元串開頭或結尾的一個簡單方法是使用 str.startswith() 或者是 str.endswith() 方法。

--如果你想檢查多種比對可能,隻需要将所有的比對項放入到一個元組中去,然後傳給 startswith() 或者 endswith() 方法:
choices = ['http', 'ftp']
url = 'http://www.python.org'
# print(url.startswith(choices))#TypeError: startswith first arg must be str or a tuple of str, not list
print(url.startswith(tuple(choices)))
           

3.利用shell通配符做字元串比對(待續。。。。)

4.文本模式的比對和查找

import re
text1 = '11/27/2012'
text2 = 'Nov 27, 2012'
if re.match(r'\d+/\d+/\d+', text1):
    print('yes')
else:
    print('no')


if re.match(r'\d+/\d+/\d+', text2):
    print('yes')
else:
    print('no')
           

輸出:

yes
no
           

如果你想使用同一個模式去做多次比對,你應該先将模式字元串預編譯為模式對象。

datepat = re.compile(r'\d+/\d+/\d+')#将正規表達式模式預編譯成一個模式對象

if datepat.match(text1):
    print('yes')
else:
    print('no')

if datepat.match(text2):
    print('yes')
else:
    print('no')
           

輸出:

yes
no
           

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

text = 'Today is 11/27/2012. PyCon starts 3/13/2013'
res = datepat.findall(text)
print(res)
           

輸出:

['11/27/2012', '3/13/2013']
           

在定義正則式的時候,通常會将部分模式用括号包起來的方式引入捕獲組。捕獲組可以使得後面的處理更加簡單,因為可以分别将每個組的内容提取出來。

findall() 方法會搜尋文本并以清單形式傳回所有的比對。如果你想以疊代方式傳回比對,可以使用 finditer() 方法來代替,比如:

m = datepat.match('11/27/2012')
print(m)
print(datepat.findall(text))
for m in datepat.finditer(text):
    print(m.group(0))
           

輸出:

<re.Match object; span=(0, 10), match='11/27/2012'>
['11/27/2012', '3/13/2013']
11/27/2012
3/13/2013
           

以上就是使用 re 子產品進行比對和搜尋文本的最基本方法,核心步驟就是先使用 re.compile() 編譯正規表達式字元串,然後使用 match() , findall() 或者 finditer() 等方法。

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

如果你想精确比對,確定你的正規表達式以 $ 結尾。

如果你打算做大量的比對和搜尋操作的話,最好先編譯正規表達式,然後再重複使用它。子產品級别的函數會将最近編譯過的模式緩存起來,是以并不會消耗太多的性能,但是如果使用預編譯模式的話,你将會減少查找和一些額外的處理損耗。

5.查找和替換文本

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

text = 'yy, yy, yy, hs, yy, hs'
print(text.replace('yy','hs'))
           

輸出:

hs, hs, hs, hs, hs, hs
           

對于複雜的模式,請使用 re 子產品中的 sub() 函數。為了說明這個,假設你想将形 式為 11/27/2012 的日期字元串改成 2012-11-27 。

text = 'Today is 11/27/2012. PyCon starts 3/13/2013'
--sub() 函數中的第一個參數是被比對的模式,第二個參數是替換模式。反斜杠數字 比如 \3 指向前面模式的捕獲組号。
print(re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text))
           

輸出:

Today is 2012-11-27. PyCon starts 2013-3-13
           

除了得到替換後的文本外,如果還想知道一共完成了多少次替換,可以使用 re.subn() 來代替。

datepat = re.compile(r'(\d+)/(\d+)/(\d+)')
newtext, n = datepat.subn(r'\3-\1-\2', text)
print(newtext, n, sep='\n')
           

輸出:

Today is 2012-11-27. PyCon starts 2013-3-13
2
           

6.以不區分大小寫的方式對文本做查找和替換

為了在文本操作時忽略大小寫,你需要在使用 re 子產品的時候給這些操作提供 re.IGNORECASE 标志參數。

import re
text = 'UPPER PYTHON, lower python, Mixed Python'
res = re.findall('python', text, flags=re.IGNORECASE)
print(res)

res = re.sub('python', 'hs', text, flags=re.IGNORECASE)
print(res)
           

輸出:

['PYTHON', 'python', 'Python']
UPPER hs, lower hs, Mixed hs
           

以上例子揭示了一個小缺陷,替換字元串并不會自動跟被比對字元串的大 小寫保持一緻。為了修複這個,你可能需要一個輔助函數,就像下面的這樣:

def matchcase(word):
    def replace_word(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_word

res = re.sub('python', matchcase('hs'), text, flags=re.IGNORECASE)
print(res)
           

輸出:

UPPER HS, lower hs, Mixed Hs
           

注意: matchcase(‘snake’) 傳回了一個回調函數 (參數必須是 match 對象),前 面一節提到過,sub() 函數除了接受替換字元串外,還能接受一個回調函數。

7.定義實作最短比對的正規表達式

你正在試着用正規表達式比對某個文本模式,但是它找到的是模式的最長可能匹 配。而你想修改它變成查找最短的可能比對。

這個問題一般出現在需要比對一對分隔符之間的文本的時候 (比如引号包含的字元串)。

import re
--模式 r'\"(.*)\"' 的意圖是比對被雙引号包含的文本。但是在正規表達式中 * 操作符是貪婪的,是以比對操作會查找最長的可能比對。
--于是在第二個例子中搜尋 text2 的時候傳回結果并不是我們想要的。
str_pat = re.compile(r'\"(.*)\"')
text1 = 'Computer says "no."'
res1 = str_pat.findall(text1)
print(res1)


text2 = 'Computer says "no." Phone says "yes."'
res2 = str_pat.findall(text2)
print(res2)

--為了修正這個問題,可以在模式中的 * 操作符後面加上? 修飾符,就像這樣:
str_pat1 = re.compile(r'\"(.*?)\"')
res3 = str_pat1.findall(text2)
print(res3)
--這樣就使得比對變成非貪婪模式,進而得到最短的比對,也就是我們想要的結果。
           

輸出:

['no.']
['no." Phone says "yes.']
['no.', 'yes.']
           

在一 個模式字元串中,點 (.) 比對除了換行外的任何字元。然而,如果你将點 (.) 号放在開始 與結束符 (比如引号) 之間的時候,那麼比對操作會查找符合模式的最長可能比對。這 樣通常會導緻很多中間的被開始與結束符包含的文本被忽略掉,并最終被包含在比對 結果字元串中傳回。通過在 * 或者 + 這樣的操作符後面添加一個 ? 可以強制比對算法改成尋找最短的可能比對。

8.編寫多行模式的正規表達式

import re

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

print(comment.findall(text1))
print(comment.findall(text2))

--在這個模式中,(?:.|\n) 指定了一個非捕獲組(即,這個組隻做比對但不捕獲結果,也不會配置設定組号)
comment_1 = re.compile(r'/\*((?:.|\n)*?)\*/')
print(comment_1.findall(text2))

--re.DOTALL使得正規表達式中的句點可以比對所有的字元,包括換行符。
comment_2 = re.compile(r'/\*(.*?)\*/', re.DOTALL)
print(comment_2.findall(text2))
           

輸出:

[' this is a comment ']
[]
[' this is a\n                multiline comment ']
[' this is a\n                multiline comment ']
           

9.将Unicode文本統一表示為規範性形式

你正在處理 Unicode 字元串,需要確定所有字元串在底層有相同的表示。在 Unicode 中,某些字元能夠用多個合法的編碼表示。為了說明,考慮下面的這個

import unicodedata
s1 = 'Spicy Jalape\u00f1o'
s2 = 'Spicy Jalapen\u0303o'

print(s1)
print(s2)
print(s1 == s2)
print(len(s1))
print(len(s2))
           

輸出:

Spicy Jalapeño
Spicy Jalapeño
False
14
15
           

這裡的文本”Spicy Jalapeño”使用了兩種形式來表示。第一種使用整體字元”ñ”

(U+00F1),第二種使用拉丁字母”n”後面跟一個”~”的組合字元 (U+0303)。 在需要比較字元串的程式中使用字元的多種表示會産生問題。為了修正這個問題,

你可以使用 unicodedata 子產品先将文本标準化:

t1 = unicodedata.normalize('NFC', s1)
t2 = unicodedata.normalize('NFC', s2)
print(t1 == t2)
print(ascii(t1))
--normalize() 第一個參數指定字元串标準化的方式。NFC 表示字元應該是整體組成 (比如可能的話就使用單一編碼),
--而 NFD 表示字元應該分解為多個組合字元表示。
t3 = unicodedata.normalize('NFD', s1)
t4 = unicodedata.normalize('NFD', s2)
print(t3 == t4)
print(ascii(t3))
           

輸出:

True
'Spicy Jalape\xf1o'
True
'Spicy Jalapen\u0303o'
           

标準化對于任何需要以一緻的方式處理 Unicode 文本的程式都是非常重要的。當 處理來自使用者輸入的字元串而你很難去控制編碼的時候尤其如此。

在清理和過濾文本的時候字元的标準化也是很重要的。比如,假設你想清除掉一些文本上面的變音符的時候:

t1 = unicodedata.normalize('NFD', s1)
print(t1)
--combining() 函數可以測試一個字元是否為組合型字元。
x = ''.join(c for c in t1 if not unicodedata.combining(c))
print(x)
           

輸出:

Spicy Jalapeño
Spicy Jalapeno
           

10.用正規表達式處理Unicode字元(待續…)

11.從字元串中去掉不需要的字元

strip() 方法能用于删除開始或結尾的字元。lstrip() 和 rstrip() 分别從左和從右執行删除操作。預設情況下,這些方法會去除空白字元,但是你也可以指定其他字元。

s1 = ' hello word \n'
print(s1.strip())
print(s1.lstrip())
print(s1.rstrip())

s2 = '-------------hello============='
print(s2.lstrip('-'))
print(s2.rstrip('='))
print(s2.strip('-='))
           

輸出:

hello word
hello word 

 hello word
hello=============
-------------hello
hello
           

strip() 方法在讀取和清理資料以備後續處理的時候是經常會被用到的。比 如,你可以用它們來去掉空格,引号和完成其他任務。但是需要注意的是去除操作不會對字元串的中間的文本産生任何影響。

如果你想進行中間的空格,那麼你需要求助其他技術。比如使用 replace() 方法或者是用正規表達式替換。

通常情況下你想将字元串 strip 操作和其他疊代操作相結合,比如從檔案中讀取多行資料。如果是這樣的話,那麼生成器表達式就可以大顯身手了。比如:

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

在這裡,表達式 lines = (line.strip() for line in f) 執行資料轉換操作。這 種方式非常高效,因為它不需要預先讀取所有資料放到一個臨時的清單中去。它僅僅隻是建立一個生成器,并且每次傳回行之前會先執行 strip 操作。

12.文本過濾和清理(待續…)

13.對齊文本字元串

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

text = 'Hello World'
print(text.ljust(20))
print(text.rjust(20))
print(text.center(20))
--所有這些方法都能接受一個可選的填充字元。比如:
print(text.ljust(20, '*'))
print(text.rjust(20, '*'))
print(text.center(20, '-'))
           

輸出:

Hello World         
         Hello World
    Hello World  
Hello World*********
*********Hello World
----Hello World-----
           

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

r = format(text, '>20')
le = format(text, '<20')
c = format(text, '^20')
print(r)
print(le)
print(c)


r1 = format(text, '=>20s')
print(r1)
c2 = format(text, '=^20s')
print(c2)

--format() 函數的一個好處是它不僅适用于字元串。它可以用來格式化任何值,使 得它非常的通用。比如,你可以用它來格式化數字:
n = 1.34569
print(format(n, '>10'))
print(format(n, '>10.3f'))
           

輸出:

Hello World
Hello World         
    Hello World     
=========Hello World
====Hello World=====
   1.34569
     1.346
           

14.字元串連接配接及合并

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

如果你僅僅隻是合并少數幾個字元串,使用加号 (+) 通常已經足夠了。

最重要的需要引起注意的是,當我們使用加号 (+) 操作符去連接配接大量的字元串的 時候是非常低效率的,因為加号連接配接會引起記憶體複制以及垃圾回收操作。

如果你準備編寫建構大量小字元串的輸出代碼,你最好考慮下使用生 成器函數,利用 yield 語句産生輸出片段。比如:

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

text = ''.join(sample())
print(text)
           

輸出:

IsChicagoNotChicago
           
import sys

for part in sample():
    sys.stdout.write(part)
sys.stdout.write('\n')
           

輸出:

IsChicagoNotChicago
           

再或者你還可以寫出一些結合 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)


for part in combine(sample(), 32768):
    sys.stdout.write(part)
sys.stdout.write('\n')
           

輸出:

IsChicagoNotChicago
           

這裡的關鍵點在于原始的生成器函數并不需要知道使用細節,它隻負責生成字元串片段就行了。

15.給字元串中的變量名做插值處理

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

s = '{name} has {n} messages.'
x = s.format(name='hs', n=24)
print(x)
           

輸出:

hs has 24 messages.
           

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

name = 'yy'
n = 21
y = s.format_map(vars())
print(y)
           

輸出:

yy has 21 messages.
           

vars() 還有一個有意思的特性就是它也适用于對象執行個體。比如:

class Info:
    def __init__(self, name, n):
        self.name = name
        self.n = n


a = Info('HS',30)
z = s.format_map(vars(a))
print(z)
           

輸出:

HS has 30 messages.
           

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

w = s.format(name='YY')
print(w)#KeyError: 'n'
           

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

class safesub(dict):
    def __missing__(self, key):
        return '{' + key + '}'

del n
m = s.format_map(safesub(vars()))
print(m)
           

輸出:

yy has {n} messages.
           

現在你可以像下面這樣寫了:

name = 'yangyang'
n = 21
print(sub('Hello {name}'))
print(sub('You have {n} message.'))
print(sub('You favorite color is {color}'))
           

輸出:

Hello yangyang
You have 21 message.
You favorite color is {color}
           

字元串模闆的使用:

import string

name = 'hongsong'
n = 24
s = string.Template('$name has $n message.')
print(s.substitute(vars()))
           

輸出:

hongsong has 24 message.
           

然而,format() 和 format_map() 相比較上面這些方案而已更加先進,是以應該被優先選擇。使用 format() 方法還有一個好處就是你可以獲得對字元串格式化的所有 支援 (對齊,填充,數字格式化等待),而這些特性是使用像模闆字元串之類的方案不可能獲得的。

16.以固定的列數重新格式化文本

使用 textwrap 子產品來格式化字元串的輸出。

import textwrap

s = "Look into my eyes, look into my eyes, the eyes, the eyes, \
the eyes, not around the eyes, don't look around the eyes, \
look into my eyes, you're under."

print(textwrap.fill(s, 70))
print(textwrap.fill(s, 40))
print(textwrap.fill(s, 40, initial_indent=' '))
print(textwrap.fill(s, 40, subsequent_indent=' '))
           

輸出:

Look into my eyes, look into my eyes, the eyes, the eyes, the eyes,
not around the eyes, don't look around the eyes, look into my eyes,
you're under.
Look into my eyes, look into my eyes,
the eyes, the eyes, the eyes, not around
the eyes, don't look around the eyes,
look into my eyes, you're under.
 Look into my eyes, look into my eyes,
the eyes, the eyes, the eyes, not around
the eyes, don't look around the eyes,
look into my eyes, you're under.
Look into my eyes, look into my eyes,
 the eyes, the eyes, the eyes, not
 around the eyes, don't look around the
 eyes, look into my eyes, you're under.
           

fill() 方法還有一些額外的選項可以用來控制如何處理制表符、句号等。

17.在文本中處理HTML和XML實體(待續…)

18.文本分詞

要對字元串做分詞處理,你不僅需要比對模式,還得指定模式的類型。

import re
from collections import namedtuple
--,?P<TOKENNAME> 用于給一個模式命名
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'
NUM = r'(?P<NUM>\d+)'
PLUS = r'(?P<PLUS>\+)'
TIMES = r'(?P<TIMES>\*)'
EQ = r'(?P<EQ>=)'
WS = r'(?P<WS>\s+)'

master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))

Token = namedtuple('Token', ['type', 'value'])

--scanner() 方法會建立一個 scanner 對象,在這個對象上不斷的調用 match() 方法會一步步的掃描目标文本,一次比對一個模式。
def generate_tokens(pat, text):
    scanner = pat.scanner(text)
    for m in iter(scanner.match, None):
        yield Token(m.lastgroup, m.group())


for tok in generate_tokens(master_pat, 'foo = 42'):
    print(tok)
           

輸出:

Token(type='NAME', value='foo')
Token(type='WS', value=' ')
Token(type='EQ', value='=')
Token(type='WS', value=' ')
Token(type='NUM', value='42')
           

這些标記在正規表達式(即re.compile(’|’.join([NAME, NUM, PLUS, TIMES, EQ, WS])))中的順序同樣也很重要。當進行比對時,re子產品會按照指定的順序來對模式做比對。如果碰巧某個模式是另一個較長模式的子串時,就必須確定較長的那個模式要先做比對。

LT = r'(?P<LT><)' LE = r'(?P<LE><=)' EQ = r'(?P<EQ>=)'
master_pat = re.compile('|'.join([LE, LT, EQ])) # Correct 
master_pat = re.compile('|'.join([LT, LE, EQ])) # Incorrect
           

第二個模式是錯的,因為它會将文本 <= 比對為令牌 LT 緊跟着 EQ,而不是單獨的令牌 LE,這個并不是我們想要的結果。

最後也最重要的是,對于有可能形成子串的模式要多加小心。

19.編寫一個簡單的遞歸下降解析器(待續…)

20.在位元組串上執行文本操作

位元組串(Byte String)同樣也支援大部分和文本字元串一樣的内置操作。

data = b'Hello word'
print(data[0:5])
print(data.startswith(b'Hello'))
print(data.split())
print(data.replace(b'word', b'hsyy'))


data_array = bytearray(b'Hello Word')
print(data_array[0:5])
print(data_array.startswith(b'Hello'))
print(data_array.split())
# print(data_array.replace(b'word', b' word hs_yy'))#沒有替換成功

# -------------------------------------#

data = b'FOO:BAR, SPAM'
import re
# print(re.split('[:,]', data))#TypeError: cannot use a string pattern on a bytes-like object
print(re.split(b'[:,]', data))


# ----------------------------------------------#
--大多數情況下,在文本字元串上的操作均可用于位元組字元串。然而,這裡也有一些需要注意的不同點。
--首先,位元組串的索引操作傳回整數而不是單獨字元。
a = 'Hello World'
print(a[0])

b = b'Hello world'
print(b[0])
print(b)
--位元組字元串不會提供一個美觀的字元串表示,也不能很好的列印出來,除非它們先被解碼為一個文本字元串。比如:
b_string = b.decode('ascii')
print(b_string)

# ----------------------------------------------------#
--如果你想格式化位元組字元串,你得先使用标準的文本字元串,然後将其編碼為位元組串。比如:
b_string_format = '{:10s} {:10d} {:10.2f}'.format('Hongsong', 100, 490.1).encode('ascii')
print(b_string_format)
           

輸出:

b'Hello'
True
[b'Hello', b'word']
b'Hello hsyy'
bytearray(b'Hello')
True
[bytearray(b'Hello'), bytearray(b'Word')]
[b'FOO', b'BAR', b' SPAM']
H
72
b'Hello world'
Hello world
b'Hongsong          100     490.10'
           

最後需要注意的是,使用位元組字元串可能會改變一些操作的語義,特别是那些跟檔案系統有關的操作。

最後提一點,一些程式員為了提升程式執行的速度會傾向于使用位元組字元串而不是文本字元串。盡管操作位元組字元串确實會比文本更加高效 (因為處理文本固有的 Unicode 相關開銷)。這樣做通常會導緻非常雜亂的代碼。你會經常發現位元組字元串并不能和 Python 的其他部分工作的很好,并且你還得手動處理所有的編碼/解碼操作。坦白講,如果你在處理文本的話,就直接在程式中使用普通的文本字元串而不是位元組字元 串。不做死就不會死!