本節(jié)內(nèi)容,要講解的和我們的信息檢索有關(guān)系,這一方面也是Python在目前非常流行的一個應(yīng)用方向:爬蟲。本節(jié)內(nèi)容什么是正則表達式正則表達式
正則表達式:也成為規(guī)則表達式,英文名稱Regular Expression,我們在程序中經(jīng)常會縮寫為regex或者regexp,專門用于進行文本檢索、匹配、替換等操作的一種技術(shù)。
注意:正則表達式是一種獨立的技術(shù),并不是某編程語言獨有的
關(guān)于正則表達式的來歷
long long logn years ago,美國新澤西州的兩個人類神經(jīng)系統(tǒng)工作者,不用干正事也能正常領(lǐng)工資的情況下,有段時間閑的發(fā)慌,于是他們開始研究一個課題~怎么使用數(shù)學(xué)方式來描述人類的神經(jīng)網(wǎng)絡(luò)。這一研究,還真是搞事!另一個數(shù)學(xué)家Stephen Kleene根據(jù)他們的研究基礎(chǔ),通過數(shù)學(xué)算法處理,發(fā)布了《神經(jīng)網(wǎng)事件表示法》,利用的就是正則集合的數(shù)學(xué)符號描述這個模型,正則表達式的概念進入了人們的視線。
又來了一個搞事的人~某個家伙上學(xué)學(xué)完正常的課程之后(這事在中國貌似發(fā)生不了),開始搗鼓計算機操作系統(tǒng),并且搞出了現(xiàn)在在軟件行業(yè)非常出名的系統(tǒng):Unix,它就是Unix之父Ken Thompson,這個家伙也看到了那個數(shù)學(xué)家發(fā)布的論文,于是將正則表達式經(jīng)過優(yōu)化處理之后,引入到了Unix操作系統(tǒng)中專門用于文本的高效檢索。
一發(fā)不可收拾,正則表達式,開始陸陸續(xù)續(xù)在各個編程語言中出現(xiàn),并以它優(yōu)雅的表現(xiàn)形式和高效的工作態(tài)度,而著名于各個語言和行業(yè)方向。
正則表達式,是一種特殊的符號,這樣的符號是需要解釋才能使用的,也就是需要正則表達式引擎來進行解釋,目前正則表達式的引擎主要分三種:DFA,NFA、POSIX NFA,有興趣了正則表達式引擎的童鞋,可以自己查看資料
接下來,我們開始了解這樣一個神秘的可以類似人類神經(jīng)網(wǎng)絡(luò)一樣思考問題的技術(shù)的語法結(jié)構(gòu)。
注意:我們通過python程序進行測試,但是正則表達式的語法結(jié)構(gòu)在各種語言環(huán)境中都是通用的。
我們通過一個簡單的案例入手:通常情況下,我們會驗證用戶輸入的手機號碼是否合法,是否156/186/188開頭的手機號碼,如果按照常規(guī)驗證手段,就需要對字符串進行拆分處理,然后逐步匹配
重要提示:python中提供了
re
模塊,包含了正則表達式的所有功能,專門用于進行正則表達式的處理;
我們首先看一下,常規(guī)的手機號碼驗證過程
userphone = input("請輸入手機號碼:")
# 驗證用戶手機號碼是否合法的函數(shù)
def validatePhone(phone):
msg = "提示信息:請輸入手機號碼"
# 判斷輸入的字符的長度是否合法
if len(phone) == 11:
# 判斷是否156/186/188開頭
if phone.startswith("156") or phone.startswith("186") or phone.startswith("188"):
# 判斷每一個字符都是數(shù)字
for num in phone:
# isdigit()函數(shù)用于判斷調(diào)用者是否數(shù)字
if not num.isdigit():
msg = "不能包含非法字符"
return msg
msg = "手機號碼合法"
else:
msg = "開頭數(shù)字不合法"
else:
msg = "長度不合法"
return msg
# 開始測試
print(validatePhone(userphone))
執(zhí)行上面的代碼,分別輸入不同的手機號碼,結(jié)果如下
請輸入手機號碼:188
長度不合法請輸入手機號碼:15568686868
開頭數(shù)字不合法請輸入手機號碼:1566868686a
不能包含非法字符請輸入手機號碼:15688888888
手機號碼合法
我們再次使用正則表達式來改造這段程序
注意:如果下面的程序中出現(xiàn)了一些語法不是很明白,沒關(guān)系,后面會詳細(xì)講解
import re
# 接收用戶輸入
userphone = input("請輸入手機號碼")
# 定義驗證手機號碼的函數(shù)
def validatePhone(phone):
# 定義正則表達式,Python中的正則表達式還是一個字符串,是以r開頭的字符串
regexp = r"^(156|186|188)\d{8}$"
# 開始驗證
if re.match(regexp, phone):
return "手機號碼合法"
else:
return "手機號碼只能156/186/188開頭,并且每一個字符都是數(shù)字,請檢查"
# 開始驗證
print(validatePhone(userphone))
執(zhí)行上面的代碼,我們得到正常驗證的結(jié)果,大家可以自己試一試。
我們從這兩套代碼中,可以看出來,使用了正則表達式之后的程序變得非常簡潔了,那保持好你的沖動和熱情,讓正則表達式來搞事吧
python提供的正則表達式處理模塊re,提供了各種正則表達式的處理函數(shù)
2.3.1 字符串查詢匹配的函數(shù):
函數(shù) | 描述 |
---|---|
re.match(reg, info) | 用于在開始位置匹配目標(biāo)字符串info中符合正則表達式reg的字符,匹配成功會返回一個match對象,匹配不成功返回None |
re.search(reg, info) | 掃描整個字符串info,使用正則表達式reg進行匹配,匹配成功返回匹配的第一個match對象,匹配不成功返回None |
re.findall(reg, info) | 掃描整個字符串info,將符合正則表達式reg的字符全部提取出來存放在列表中返回 |
re.fullmatch(reg, info) | 掃描整個字符串,如果整個字符串都包含在正則表達式表示的范圍中,返回整個字符串,否則返回None |
re.finditer(reg, info) | 掃描整個字符串,將匹配到的字符保存在一個可以遍歷的列表中 |
參考官方re.py源代碼如下:
def match(pattern, string, flags=0):
"""Try to apply the pattern at the start of the string, returning
a match object, or None if no match was found."""
return _compile(pattern, flags).match(string)
def fullmatch(pattern, string, flags=0):
"""Try to apply the pattern to all of the string, returning
a match object, or None if no match was found."""
return _compile(pattern, flags).fullmatch(string)
def search(pattern, string, flags=0):
"""Scan through string looking for a match to the pattern, returning
a match object, or None if no match was found."""
return _compile(pattern, flags).search(string)
def findall(pattern, string, flags=0):
"""Return a list of all non-overlapping matches in the string.
If one or more capturing groups are present in the pattern, return
a list of groups; this will be a list of tuples if the pattern
has more than one group.
Empty matches are included in the result."""
return _compile(pattern, flags).findall(string)
def finditer(pattern, string, flags=0):
"""Return an iterator over all non-overlapping matches in the
string. For each match, the iterator returns a match object.
Empty matches are included in the result."""
return _compile(pattern, flags).finditer(string)
2.3.2 字符串拆分替換的函數(shù):
函數(shù) | 描述 |
---|---|
re.split(reg, string) | 使用指定的正則表達式reg匹配的字符,將字符串string拆分成一個字符串列表,如:re.split(r"\s+", info),表示使用一個或者多個空白字符對字符串info進行拆分,并返回一個拆分后的字符串列表 |
re.sub(reg, repl, string) | 使用指定的字符串repl來替換目標(biāo)字符串string中匹配正則表達式reg的字符 |
參考官方源代碼如下:
def split(pattern, string, maxsplit=0, flags=0):
"""Split the source string by the occurrences of the pattern,
returning a list containing the resulting substrings. If
capturing parentheses are used in pattern, then the text of all
groups in the pattern are also returned as part of the resulting
list. If maxsplit is nonzero, at most maxsplit splits occur,
and the remainder of the string is returned as the final element
of the list."""
return _compile(pattern, flags).split(string, maxsplit)
def sub(pattern, repl, string, count=0, flags=0):
"""Return the string obtained by replacing the leftmost
non-overlapping occurrences of the pattern in string by the
replacement repl. repl can be either a string or a callable;
if a string, backslash escapes in it are processed. If it is
a callable, it's passed the match object and must return
a replacement string to be used."""
return _compile(pattern, flags).sub(repl, string, count)
接下來,我們進入正則表達式干貨部分
在使用正則表達式的過程中,一些包含特殊含義的字符,用于表示字符串中一些特殊的位置,非常重要,我們先簡單了解一下一些常用的元字符
元字符 | 描述 |
---|---|
^ | 表示匹配字符串的開頭位置的字符 |
$ | 表示匹配字符串的結(jié)束位置的字符 |
. | 表示匹配任意一個字符 |
\d | 匹配一個數(shù)字字符 |
\D | 匹配一個非數(shù)字字符 |
\s | 匹配一個空白字符 |
\S | 匹配一個非空白字符 |
\w | 匹配一個數(shù)字/字母/下劃線中任意一個字符 |
\W | 匹配一個非數(shù)字字母下劃線的任意一個字符 |
\b | 匹配一個單詞的邊界 |
\B | 匹配不是單詞的開頭或者結(jié)束位置 |
上干貨:代碼案例
# 導(dǎo)入正則表達式模塊
import re
# 定義測試文本字符串,我們后續(xù)在這段文本中查詢數(shù)據(jù)
msg1 = """Python is an easy to learn, powerful programming language.
It has efficient high-level data structures and a simple but effective approach to object-oriented programming.
Python’s elegant syntax and dynamic typing, together with its interpreted nature,
make it an ideal language for scripting and rapid application development in many areas on most platforms.
"""
msg2 = "hello"
msg3 = "hello%"
# 定義正則表達式,匹配字符串開頭是否為python
regStart = r"efficient"
# 從字符串開始位置匹配,是否包含符合正則表達式的內(nèi)容,返回匹配到的字符串的Match對象
print(re.match(regStart, msg1))
# 掃描整個字符串,是否包含符合正則表達式的內(nèi)容,返回匹配到的第一個字符串的Match對象
print(re.search(regStart, msg1))
# 掃描整個字符串,是否包含符合正則表達式的內(nèi)容,返回匹配到的所有字符串列表
print(re.findall(regStart, msg1))
# 掃描整個字符串,是否包含符合正則表達式的內(nèi)容,返回匹配到的字符串的迭代對象
for r in re.finditer(regStart, msg1):
print("->"+ r.group())
# 掃描整個字符串,是否包含在正則表達式匹配的內(nèi)容中,是則返回整個字符串,否則返回None
print(re.fullmatch(r"\w*", msg2))
print(re.fullmatch(r"\w*", msg3))
上述代碼執(zhí)行結(jié)果如下:
~ None
~
~['efficient']
~->efficient
~
~None
正則表達式中的量詞,是用于限定數(shù)量的特殊字符
量詞 | 描述 |
---|---|
x* | 用于匹配符號*前面的字符出現(xiàn)0次或者多次 |
x+ | 用于匹配符號+前面的字符出現(xiàn)1次或者多次 |
x? | 用于匹配符號?前面的字符出現(xiàn)0次或者1次 |
x{n} | 用于匹配符號{n}前面的字符出現(xiàn)n次 |
x{m,n} | 用于匹配符號{m,n}前面的字符出現(xiàn)至少m次,最多n次 |
x{n, } | 用于匹配符號{n, }前面的字符出現(xiàn)至少n次 |
接上代碼干貨:
# 導(dǎo)入正則表達式模塊
import re
# 定義測試文本字符串,我們后續(xù)在這段文本中查詢數(shù)據(jù)
msg1 = """goodgoodstudy!,dooodooooup"""
# 匹配一段字符串中出現(xiàn)單詞o字符0次或者多次的情況
print(re.findall(r"o*", msg1))
# 匹配一段字符串中出現(xiàn)單詞o字符1次或者多次的情況
print(re.findall(r"o+", msg1))
# 匹配一段字符串中出現(xiàn)單詞o字符0次或者1次的情況
print(re.findall(r"o?", msg1))
# 匹配字符串中連續(xù)出現(xiàn)2次字符o的情況
print(re.findall(r"o{2}", msg1))
# 匹配字符串中連續(xù)出現(xiàn)2次以上字符o的情況
print(re.findall(r"o{2,}", msg1))
# 匹配字符串中連續(xù)出現(xiàn)2次以上3次以內(nèi)字符o的情況
print(re.findall(r"o{2,3}", msg1))
上述代碼大家可以自行嘗試并分析結(jié)果。執(zhí)行結(jié)果如下:
['', 'oo', '', '', 'oo', '', '', '', '', '', '', '', '', '', 'ooo', '', 'oooo', '', '', '']
['oo', 'oo', 'ooo', 'oooo']
['', 'o', 'o', '', '', 'o', 'o', '', '', '', '', '', '', '', '', '', 'o', 'o', 'o', '', 'o', 'o', 'o', 'o', '', '', '']
['oo', 'oo', 'oo', 'oo', 'oo']
['oo', 'oo', 'ooo', 'oooo']
['oo', 'oo', 'ooo', 'ooo']
在正則表達式中,針對字符的匹配,除了快捷的元字符的匹配,還有另一種使用方括號進行的范圍匹配方式,具體如下:
范圍 | 描述 |
---|---|
[0-9] | 用于匹配一個0~9之間的數(shù)字,等價于\d |
[^0-9] | 用于匹配一個非數(shù)字字符,等價于\D |
[3-6] | 用于匹配一個3~6之間的數(shù)字 |
[a-z] | 用于匹配一個a~z之間的字母 |
[A-Z] | 用于匹配一個A~Z之間的字母 |
[a-f] | 用于匹配一個a~f之間的字母 |
[a-zA-Z] | 用于匹配一個a~z或者A-Z之間的字母,匹配任意一個字母 |
[a-zA-Z0-9] | 用于匹配一個字母或者數(shù)字 |
[a-zA-Z0-9_] | 用于匹配一個字母或者數(shù)字或者下劃線,等價于\w |
[^a-zA-Z0-9_] | 用于匹配一個非字母或者數(shù)字或者下劃線,等價于\W |
注意:不要使用[0-120]來表示0~120之間的數(shù)字,這是錯誤的
整理測試代碼如下:
# 引入正則表達式模塊
import re
msg = "Hello, The count of Today is 800"
# 匹配字符串msg中所有的數(shù)字
print(re.findall(r"[0-9]+", msg))
# 匹配字符串msg中所有的小寫字母
print(re.findall(r"[a-z]+", msg))
# 匹配字符串msg中所有的大寫字母
print(re.findall(r"[A-Z]+", msg))
# 匹配字符串msg中所有的字母
print(re.findall(r"[A-Za-z]+", msg))
上述代碼執(zhí)行結(jié)果如下:
['800']
['ello', 'he', 'count', 'of', 'oday', 'is']
['H', 'T', 'T']
['Hello', 'The', 'count', 'of', 'Today', 'is']
正則表達式主要是用于進行字符串檢索匹配操作的利器
在一次完整的匹配過程中,可以將匹配到的結(jié)果進行分組,這樣就更加的細(xì)化了我們對匹配結(jié)果的操作
正則表達式通過圓括號()進行分組,以提取匹配結(jié)果的部分結(jié)果
常用的兩種分組:
分組 | 描述 |
---|---|
(expression) | 使用圓括號直接分組;正則表達式本身匹配的結(jié)果就是一個組,可以通過group()或者group(0)獲取;然后正則表達式中包含的圓括號就是按照順序從1開始編號的小組 |
(?P |
使用圓括號分組,然后給當(dāng)前的圓括號表示的小組命名為name,可以通過group(name)進行數(shù)據(jù)的獲取 |
廢話少說,上干貨:
# 引入正則表達式模塊
import re
# 用戶輸入座機號碼,如"010-6688465"
phone = input("請輸入座機號碼:")
# 1.進行正則匹配,得到Match對象,對象中就包含了分組信息
res1 = re.search(r"^(\d{3,4})-(\d{4,8})$", phone)
# 查看匹配結(jié)果
print(res1)
# 匹配結(jié)果為默認(rèn)的組,可以通過group()或者group(0)獲取
print(res1.group())
# 獲取結(jié)果中第一個括號對應(yīng)的組數(shù)據(jù):處理區(qū)號
print(res1.group(1))
# 獲取結(jié)果中第二個括號對應(yīng)的組數(shù)據(jù):處理號碼
print(res1.group(2))
# 2.進行正則匹配,得到Match對象,對象中就包含了命名分組信息
res2 = re.search(r"^(?P
\d{3,4})-(?P\d{4,8})$"
, phone)
# 查看匹配結(jié)果
print(res2)
# 匹配結(jié)果為默認(rèn)的組,可以通過group()或者group(0)獲取
print(res2.group(0))
# 通過名稱獲取指定的分組信息:處理區(qū)號
print(res2.group("nstart"))
# 通過名稱獲取指定分組的信息:處理號碼
print(res2.group("nend"))
上述代碼就是從原始字符串中,通過正則表達式匹配得到一個結(jié)果,但是使用了分組之后,就可以將結(jié)果數(shù)據(jù)通過分組進行細(xì)化處理,執(zhí)行結(jié)果如下:
請輸入座機號碼:021-6565789
<_sre.SRE_Match object; span=(0, 11), match='021-6565789'>
021-6565789
021
6565789<_sre.SRE_Match object; span=(0, 11), match='021-6565789'>
021-6565789
021
6565789
使用分組的同時,會有一些特殊的使用方式如下:
表達式 | 描述 |
---|---|
(?:expression) | 作為正則表達式的一部分,但是匹配結(jié)果丟棄 |
(?=expression) | 匹配expression表達式前面的字符,如 "How are you doing" ,正則"(? |
(?<=expression) | 匹配expression表達式后面的字符,如 "How are you doing" 正則"(? |
(?!expression) | 匹配字符串后面不是expression表達式字符,如 "123abc" 正則 "\d{3}(?!\d)"匹配3位數(shù)字后非數(shù)字的結(jié)果 |
(? | 匹配字符串前面不是expression表達式字符,如 "abc123 " 正則 "(? |
在某些情況下,我們匹配的字符串出現(xiàn)一些特殊的規(guī)律時,就會出現(xiàn)匹配結(jié)果不盡如人意的意外情況
如:在下面的字符串中,將div標(biāo)簽中的所有內(nèi)容獲取出來
<div>內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2div>
此時,我們想到的是,使用
regexp = r"
.*
"
本意是使用上述代碼來完成div開始標(biāo)簽和結(jié)束標(biāo)簽之間的內(nèi)容匹配,但是,匹配的結(jié)果如下
<div> [內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2] div>
我們可以看到,上面匹配的結(jié)果,是將字符串開頭的
內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2
上述就是我們要說的正則表達式的第一種模式:貪婪模式
貪婪模式:正則表達式匹配的一種模式,速度快,但是匹配的內(nèi)容會從字符串兩頭向中間搜索匹配(比較貪婪~),一旦匹配選中,就不繼續(xù)向字符串中間搜索了,過程如下:
開始:<div>內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2div>
第一次匹配:【<div>內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2div>】
第二次匹配<div>【內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2】div>
匹配到正則中需要的結(jié)果,不再繼續(xù)匹配,直接返回匹配結(jié)果如下:
內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2
明顯貪婪模式某些情況下,不是我們想要的,所以出現(xiàn)了另一種模式:懶惰模式
懶惰模式:正則表達式匹配的另一種模式,會首先搜索匹配正則表達式開始位置的字符,然后逐步向字符串的結(jié)束位置查找,一旦找到匹配的就返回,然后接著查找
regexp = r"
.*?
"
開始:<div>內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2div>
第一次匹配:【<div>】內(nèi)容1div><p>這本來是不需要的內(nèi)容p><div>內(nèi)容2div>
第二次匹配【<div>內(nèi)容1div>】<p>這本來是不需要的內(nèi)容p><div>內(nèi)容2div>
匹配到正則中需要的結(jié)果:內(nèi)容1
繼續(xù)向后查找
第三次匹配<div>內(nèi)容1div>【<p>這本來是不需要的內(nèi)容p>】<div>內(nèi)容2div>
第四次匹配<div>內(nèi)容1div><p>這本來是不需要的內(nèi)容p>【<div>內(nèi)容2div>】
匹配到正則中需要的結(jié)果:內(nèi)容2
查找字符串結(jié)束!
正則表達式匹配的兩種模式:貪婪模式、懶惰模式
貪婪模式:從目標(biāo)字符串的兩頭開始搜索,一次盡可能多的匹配符合條件的字符串,但是有可能會匹配到不需要的內(nèi)容,正則表達式中的元字符、量詞、范圍等都模式是貪婪匹配模式,使用的時候一定要注意分析結(jié)果,如:就是一個貪婪模式,用于匹配.*
和之間所有的字符
懶惰模式:從目標(biāo)字符串按照順序從頭到位進行檢索匹配,盡可能的檢索到最小范圍的匹配結(jié)果,語法結(jié)構(gòu)是在貪婪模式的表達式后面加上一個符號?即可,如就是一個懶惰模式的正則,用于僅僅匹配最小范圍的.*?
和之間的內(nèi)容
不論貪婪模式還是懶惰模式,都有適合自己使用的地方,大家一定要根據(jù)實際需求進行解決方案的確定
>>>更多VR/AR入門教程:VR入門