小豬的Python學習之旅 —— 3.正規表示式

coder-pig發表於2019-03-02

引言

上一節學習了一波urllib庫和BeautifulSoup的使用,爬取很多小網站
基本是得心應手的了,而一般我們想爬取的資料基本都是字串,圖片url,
或者是段落文字等,掌握字串的處理顯得尤為重要,說到字串處理,
除了瞭解字串相關的處理函式外,還需要 正規表示式 這枚字串處理神器!
對於正規表示式,很多人開發者貌似都很抗拒,老說學來幹嘛,要什麼
正規表示式上網一搜就是啦,對此我只能說2333,爬取網頁資料的時候,
你搜下給我看,不同的場景匹配字串的正規表示式都是不一樣的,掌握
正規表示式的編寫就顯得尤為重要了。本節通過一些有趣的例子幫你
快速上手正規表示式,其實真沒想象中那麼難!


re模組

Python中通過**re模組**使用正規表示式,該模組提供的幾個常用方法:

1.匹配

re.match(pattern, string, flags=0)

  • 引數匹配的正規表示式要匹配的字串標誌位(匹配方法)
  • 嘗試從字串的開頭進行匹配,匹配成功會返回一個匹配的物件,
    型別是:<class `_sre.SRE_Match`>
    group與groups

re.search(pattern, string, flags=0)

  • 引數:同上
  • 掃描整個字串,返回第一個匹配的物件,否則返回None

注意match方法和search的最大區別:match如果開頭就不和正規表示式匹配,
直接返回None,而search則是匹配整個字串!!

2.檢索與替換

re.findall(pattern, string, flags=0)

  • 引數:同上
  • 遍歷字串,找到正規表示式匹配的所有位置,並以列表的形式返回

re.finditer(pattern, string, flags=0)

  • 引數:同上
  • 遍歷字串,找到正規表示式匹配的所有位置,並以迭代器的形式返回

re.sub(pattern, repl, string, count=0, flags=0)

  • 引數:repl替換為什麼字串,可以是函式,把匹配到的結果做一些轉換;
    count替換的最大次數,預設0代表替換所有的匹配。
  • 找到所有匹配的子字串,並替換為新的內容

re.split(pattern, string, maxsplit=0, flags=0)

  • 引數:maxsplit設定分割的數量,預設0代表所有滿足匹配的都分割
  • 在正規表示式匹配的地方進行分割,並返回一個列表

3.編譯成Pattern物件

對於會多次用到的正規表示式,我們可以呼叫re的compile()方法編譯成
Pattern物件,呼叫的時候直接Pattern物件.xxx即可,從而提高執行效率。

附:flags(可選標誌位)表

多個標誌可通過按位OR(|)進行連線,比如:re.I|re.M

修飾符 描述
re.I 使匹配對大小寫不敏感
re.L 做本地化識別(locale-aware)匹配
re.M 多行匹配,影響 ^ 和 $
re.S 使 . 匹配包括換行在內的所有字元
re.U 根據Unicode字符集解析字元。這個標誌影響 w, W, , B.
re.X 該標誌通過給予你更靈活的格式以便你將正規表示式寫得更易於理解。

2.正則規則詳解


1.加在正則字串前的`r`

為了告訴編譯器這個string是個raw string(原字串),不要轉義反斜槓
比如在raw string裡
是兩個字元,“和`n`,不是換行!

2.字元

字元 作用
. 匹配任意一個字元(除了
)
[] 匹配[]中列舉的字元
[^...] 匹配不在[]中列舉的字元
d 匹配數字,0到9
D 匹配非數字
s 匹配空白,就是空格和tab
S 匹配非空白
w 匹配字母數字或下劃線字元,a-z,A-Z,0-9,_
W 匹配非字母數字或下劃線字元
- 匹配範圍,比如[a-f]

3.數量

字元 作用(前面三個做了優化,速度會更快,儘量優先用前三個)
* 前面的字元出現了0次或無限次,即可有可無
+ 前面的字元出現了1次或無限次,即最少一次
? 前面的字元出現了0次或者1次,要麼不出現,要麼只出現一次
{m} 前一個字元出現m次
{m,} 前一個字元至少出現m次
{m,n} 前一個字元出現m到n次

4.邊界

字元 作用
^ 字串開頭
$ 字串結尾
單詞邊界,即單詞和空格間的位置,比如`er`
可以匹配”never” 中的 `er`,但不能匹配 “verb” 中的 `er`
B 非單詞邊界,和上面的相反
A 匹配字串的開始位置
匹配字串的結束位置

5.分組

用**()表示的就是要提取的分組**,一般用於提取子串,
比如:^(d{3})-(d{3,8})$:從匹配的字串中提取出區號和本地號碼

字元 作用
小豬的Python學習之旅 —— 3.正規表示式
匹配左右任意一個表示式
(re) 匹配括號內的表示式,也表示一個組
(?:re) 同上,但是不表示一個組
(?P<name>) 分組起別名,group可以根據別名取出,比如(?P<first>d)
match後的結果調m.group(`first`)可以拿到第一個分組中匹配的記過
(?=re) 前向肯定斷言,如果當前包含的正規表示式在當前位置成功匹配,
則代表成功,否則失敗。一旦該部分正規表示式被匹配引擎嘗試過,
就不會繼續進行匹配了;剩下的模式在此斷言開始的地方繼續嘗試。
(?!re) 前向否定斷言,作用與上面的相反
(?<=re) 後向肯定斷言,作用和(?=re)相同,只是方向相反
(?<!re) 後向否定斷言,作用於(?!re)相同,只是方向想法

附:group()方法與其他方法詳解

不引入括號,增個表示式作為一個組,是group(0)

不引入**()的話,代表整個表示式作為一個組,group = group(0)
如果
引入()**的話,會把表示式分為多個分組,比如下面的例子:

小豬的Python學習之旅 —— 3.正規表示式

輸出結果

小豬的Python學習之旅 —— 3.正規表示式

除了group方法外還有三個常用的方法:

  • groups(): 從group(1)開始往後的所有的值,返回一個元組
  • start():返回匹配的開始位置
  • end():返回匹配的結束位置
  • span():返回一個元組組,表示匹配位置(開始,結束)

貪婪與非貪婪

正則匹配預設是貪婪匹配,也就是匹配儘可能多的字元
比如:ret = re.match(r`^(d+)(0*)$`,`12345000`).groups()ß
我們的原意是想得到**(`12345`,`000`)這樣的結果,但是輸出
ret我們看到的卻是:

小豬的Python學習之旅 —— 3.正規表示式

,由於貪婪,直接把後面的
0全給匹配了,結果0*只能匹配空字串了,如果想盡可能少的
匹配,可以在d+後加上一個
?問號*採用非貪婪匹配,改成:
r`^(d+?)(0)$`
,輸出結果就變成了:

小豬的Python學習之旅 —— 3.正規表示式

3.正則練習

例子1:簡單驗證手機號碼格式

流程分析:

  • 1.開頭可能是帶0(長途),86(天朝國際區號),17951(國際電話)中的一個或者一個也沒有:
    小豬的Python學習之旅 —— 3.正規表示式
  • 2.接著1xx,有13x,14x,15x,17x,18x,然後這個x也是取值範圍也是不一樣的:
    13x:0123456789
    14x:579
    15x:012356789
    17x:01678
    18x:0123456789
    然後修改下正規表示式,可以隨便輸個字串驗證下:
    小豬的Python學習之旅 —— 3.正規表示式
  • 3.最後就是剩下部分的8個數字了,很簡單:[0-9]{8} 加上:
    小豬的Python學習之旅 —— 3.正規表示式
^(0|86|17951)?(13[0-9]|14[579]|15[0-35-9]|17[01678]|18[0-9])[0-9]{8}$
複製程式碼

例子2:驗證身份證

流程分析:

身份證號碼分為一代和二代,一代由15位號碼組成,而二代則是由18個號碼組成:
十五位:xxxxxx    yy mm dd   pp s
十八位:xxxxxx yyyy mm dd ppp s

為了方便了解,把這兩種情況分開,先是十八位的:

  • 1.前6位地址編碼(省市縣),第一位從1開始,其他五位0-9
    小豬的Python學習之旅 —— 3.正規表示式
  • 2.第7到10(接著的兩位或者四位有):,範圍是1800到2099:
    小豬的Python學習之旅 —— 3.正規表示式
  • 3.第11到12,1-9月需要補0,10,11,12
    小豬的Python學習之旅 —— 3.正規表示式
  • 4.第13到14,首位可能是012,第二位為0-9,還要補上10,20,30,31
    小豬的Python學習之旅 —— 3.正規表示式
  • 5.第15到17順序碼,這裡就是三個數字,對同年、同月、同日出生的人
    編定的順序號,奇數分給男的,偶數分給女的:
    小豬的Python學習之旅 —— 3.正規表示式
  • 6.第18位校驗碼,0到9或者x和X
    小豬的Python學習之旅 —— 3.正規表示式

能推算出18的,那麼推算出15的也不難了:

小豬的Python學習之旅 —— 3.正規表示式

最後用|組合下:

小豬的Python學習之旅 —— 3.正規表示式
^[1-9]d{5}(18|19|20)d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)d{3}[0-9Xx]|[1-9]d{5}d{2}(0[1-9]|10|11|12)([012][1-9]|10|20|30|31)d{2}[0-9Xx]$
複製程式碼

另外,這裡的正則匹配出的身份證不一定是合法的,判斷身份是否
合法還需要通過程式進行校驗,校驗最後的校驗碼是否正確

擴充套件閱讀:身份證的最後一位是怎麼算出來的?
更多可見:第二代身份證號碼編排規則

首先有個加權因子的表:(沒弄懂怎麼算出來的..)
[7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]

然後位和值想乘,結果相加,最後除11求餘,比如我隨便網上找的
一串身份證:411381199312150167,我們來驗證下最後的7是對的嗎?

sum = 47 + 19 + 110 + 35 +88 + 1 4 … + 6 * 2 = 282
sum % 11 = 7,所以這個是一個合法的身份證號。


例子3:驗證ip是否正確

流程分析

ip由4段組成,xxx.xxx.xxx.xxx,訪問從0到255,因為要考慮上中間的.
所以我們把第一段和後面三段分開,然後分析下ip的結構,可能是這幾種情況:
一位數[1-9]
兩位數[1-9][0-9]
三位數(100-199):1[0-9][0-9]
三位數(200-249):2[0-4][0-9]
三位數(250-255): 25[0-5]
理清了第一段的正則怎麼寫就一清二楚了:

小豬的Python學習之旅 —— 3.正規表示式

然後後面三段,需要在前面加上一個一個**.**,然後這玩意是元字元,
需要加上一個反斜槓,讓他失去作用,後面三段的正則就是:

小豬的Python學習之旅 —— 3.正規表示式

把兩段拼接下即可得出完整的驗證ip的正規表示式了:

小豬的Python學習之旅 —— 3.正規表示式
^([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(.([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$
複製程式碼

例子4:匹配各種亂七八糟的

  • 匹配中文[u4e00-u9fa5]

  • 匹配雙位元組字元[^x00-xff]

  • 匹配數字並輸出示例

    小豬的Python學習之旅 —— 3.正規表示式

    輸出結果

    小豬的Python學習之旅 —— 3.正規表示式
  • 匹配開頭結尾示例

    小豬的Python學習之旅 —— 3.正規表示式

    輸出結果

    小豬的Python學習之旅 —— 3.正規表示式

4.正則實戰

實戰:抓一波城市編碼列表

本來想著就抓抓中國氣象局的天氣就好了,然後呢,比如深圳天氣的網頁是:
www.weather.com.cn/weather1dn/…
然後這個101280601是城市編碼,然後網上搜了下城市編碼列表,發現要麼
很多是錯的,要麼就缺失很多,或者連結失效,想想自己想辦法寫一個採集
的,先搞一份城市編碼的列表,不過我去哪裡找資料來源呢?中國氣象局
肯定是會有的,只是應該不會直接全部暴露出來,想想能不能通過一些間接
操作來實現。對著中國氣象局的網站瞎點,結果不負有心人,我在這裡:
www.weather.com.cn/forecast/
發現了這個:

小豬的Python學習之旅 —— 3.正規表示式

點進去後:www.weather.com.cn/textFC/hb.s…
然後,我覺得這可能是入手點:

小豬的Python學習之旅 —— 3.正規表示式

F12開啟開發者工具,不出所料:

小豬的Python學習之旅 —— 3.正規表示式

這裡有個超連結,難不成是北京所有的地區的列表,點選下進去看看:
www.weather.com.cn/textFC/beij…

小豬的Python學習之旅 —— 3.正規表示式

臥槽,果然是北京所有的地區,然後每個地區的名字貌似都有一個超連結,
F12看下指向哪裡?

小豬的Python學習之旅 —— 3.正規表示式

到這裡就豁(huo)然開朗了,我們來捋一捋實現的流程:

  • 1.先拿到第一層的城市列表連結用列表存起來
  • 2.接著遍歷列表去訪問不同的城市列表連結,擷取不同城市的城市名,城市編碼存起來

流程看上去很簡單,接著來實操一波。

先是拿城市列表url

小豬的Python學習之旅 —— 3.正規表示式

這個很容易拿,就直接貼程式碼了:

小豬的Python學習之旅 —— 3.正規表示式

拿到需要的城市列表url:

小豬的Python學習之旅 —— 3.正規表示式

接著隨便點開一個,比如beijing.shtml,頁面結構是這樣的:
想要的內容是這裡的超連結:

小豬的Python學習之旅 —— 3.正規表示式

F12看下頁面結構,層次有點多,不過沒關係,這樣更能夠鍛鍊我們

小豬的Python學習之旅 —— 3.正規表示式

入手點一般都是離我們想要資料最近地方下手,我看上了:conMidtab3
全域性搜了一下,也就八個:

小豬的Python學習之旅 —— 3.正規表示式

第一個直接就可以排除了:

小豬的Python學習之旅 —— 3.正規表示式

接著其餘的七個,然後發現都他麼是一樣的…,那就直接抓到第一個吧:

小豬的Python學習之旅 —— 3.正規表示式

輸出下:

小豬的Python學習之旅 —— 3.正規表示式

是我們想要的內容,接著裡面的tr是我們需要內容,找一波:

小豬的Python學習之旅 —— 3.正規表示式

輸出下:

小豬的Python學習之旅 —— 3.正規表示式

繼續細扒,我們要的只是a這個東西:

小豬的Python學習之旅 —— 3.正規表示式

輸出下:

小豬的Python學習之旅 —— 3.正規表示式

重複出現了一堆詳情,很明顯是我們不想要的,我們可以在迴圈的時候
執行一波判斷,重複的不加入到列表中:

小豬的Python學習之旅 —— 3.正規表示式

然後我們想拿到城市編碼和城市名稱這兩個東西:

小豬的Python學習之旅 —— 3.正規表示式

城市的話還好,直接呼叫tag物件的string直接就能拿到,
而城市編碼的話,按照以前的套路,我們需要先[`href`]拿到
再做字串裁剪,挺繁瑣的,既然本節學習了正則,為何不用
正則來一步到位,不難寫出這樣的正則:

小豬的Python學習之旅 —— 3.正規表示式

匹配拿到**group(1)**就是我們要的城市編碼:

小豬的Python學習之旅 —— 3.正規表示式

輸出內容:

小豬的Python學習之旅 —— 3.正規表示式

臥槽,就是我們想要的結果,美滋滋,接著把之前拿到所有
的城市列表都跑一波,存字典裡返回,最後賽到一個大字典
裡,然後寫入到檔案中,完成。


========= BUG的分割線 =========

最後把資料列印出來發現只有428條資料,後面才發現conMidtab3那裡處理有些
問題,漏掉了一些,限於篇幅,就不重新解釋了,直接貼上修正完後的程式碼把…

import urllib.request
from urllib import error
from bs4 import BeautifulSoup
import os.path
import re
import operator

# 通過中國氣象局抓取到所有的城市編碼

# 中國氣象網基地址
weather_base_url = "http://www.weather.com.cn"
# 華北天氣預報url
weather_hb_url = "http://www.weather.com.cn/textFC/hb.shtml#"


# 獲得城市列表連結
def get_city_list_url():
    city_list_url = []
    weather_hb_resp = urllib.request.urlopen(weather_hb_url)
    weather_hb_html = weather_hb_resp.read().decode(`utf-8`)
    weather_hb_soup = BeautifulSoup(weather_hb_html, `html.parser`)
    weather_box = weather_hb_soup.find(attrs={`class`: `lqcontentBoxheader`})
    weather_a_list = weather_box.findAll(`a`)
    for i in weather_a_list:
        city_list_url.append(weather_base_url + i[`href`])
    return city_list_url


# 根據傳入的城市列表url獲取對應城市編碼
def get_city_code(city_list_url):
    city_code_dict = {}  # 建立一個空字典
    city_pattern = re.compile(r`^<a.*?weather/(.*?).s.*</a>$`)  # 獲取城市編碼的正則

    weather_hb_resp = urllib.request.urlopen(city_list_url)
    weather_hb_html = weather_hb_resp.read().decode(`utf-8`)
    weather_hb_soup = BeautifulSoup(weather_hb_html, `html.parser`)
    # 需要過濾一波無效的
    div_conMidtab = weather_hb_soup.find_all(attrs={`class`: `conMidtab`, `style`: ``})

    for mid in div_conMidtab:
        tab3 = mid.find_all(attrs={`class`: `conMidtab3`})
        for tab in tab3:
            trs = tab.findAll(`tr`)
            for tr in trs:
                a_list = tr.findAll(`a`)
                for a in a_list:
                    if a.get_text() != "詳情":
                        # 正則拿到城市編碼
                        city_code = city_pattern.match(str(a)).group(1)
                        city_name = a.string
                        city_code_dict[city_code] = city_name
        return city_code_dict


# 寫入檔案中
def write_to_file(city_code_list):
    try:
        with open(`city_code.txt`, "w+") as f:
            for city in city_code_list:
                f.write(city[0] + ":" + city[1] + "
")
    except OSError as reason:
        print(str(reason))
    else:
        print("檔案寫入完畢!")


if __name__ == `__main__`:
    city_result = {}  # 建立一個空字典,用來存所有的字典
    city_list = get_city_list_url()

    # get_city_code("http://www.weather.com.cn/textFC/guangdong.shtml")

    for i in city_list:
        print("開始查詢:" + i)
        city_result.update(get_city_code(i))

    # 根據編碼從升序排列一波
    sort_list = sorted(city_result.items(), key=operator.itemgetter(0))

    # 儲存到檔案中
    write_to_file(sort_list)

複製程式碼

執行結果

小豬的Python學習之旅 —— 3.正規表示式

5.小結和幾個API

本節對Python中了正規表示式進行了一波學習,練手,發現和Java裡的正則
多了一些規則,正則在字串匹配的時候是挺爽的,但是正則並不是全能
的,比如閏年二月份有多少天的那個問題,還需要程式另外去做判斷!
正則還需要多練手啊,限於篇幅,就沒有另外去抓各種天氣資訊了,
而且不是剛需,順道提供兩個免費可用三個和能拿到天氣資料的API吧:

還有個中國氣象局提供的根據經緯度獲取天氣的:
e.weather.com.cn/d/town/inde…

人生苦短,我用Python,爬蟲真好玩!期待下節爬蟲框架scrapy學習~

小豬的Python學習之旅 —— 3.正規表示式

來啊,Py交易啊

想加群一起學習Py的可以加下,智障機器人小Pig,驗證資訊裡包含:
PythonpythonpyPy加群交易屁眼 中的一個關鍵詞即可通過;

小豬的Python學習之旅 —— 3.正規表示式

驗證通過後回覆 加群 即可獲得加群連結(不要把機器人玩壞了!!!)~~~
歡迎各種像我一樣的Py初學者,Py大神加入,一起愉快地交流學♂習,van♂轉py。

小豬的Python學習之旅 —— 3.正規表示式

相關文章