1. 程式人生 > >python 3.x 爬蟲基礎---正則表示式(案例:爬取貓眼資訊,寫入txt,csv,下載圖片)

python 3.x 爬蟲基礎---正則表示式(案例:爬取貓眼資訊,寫入txt,csv,下載圖片)

python 3.x 爬蟲基礎

前言

  正則表示式是對字串的一種邏輯公式,用事先定義好的一些特定字元、及這些特定字元的組合,組成一個“規則的字串”,此字串用來表示對字串的一種“過濾”邏輯。正在在很多開發語言中都存在,而非python獨有。對其知識點進行總結後,會寫一個demo。

1.正則表示式

  python是自1.5開始引進re模組進行處理正則的。我先把正則的匹配規則總結一下,再總結re模組相應的方法。

1.1匹配規則

語法解釋表示式成功匹配物件
一般字元 匹配自身相對應的字元 abc abc
. 匹配除換行符(\n)以外的任意字元 a.c abc
\ 轉義字元,可以改變原字元的意思 a.c a.c
\d 匹配數字:0~9 \dabc 1abc
\w 匹配單詞字元,a~z;A~Z;0~9 \w\w\w oX2
\s 匹配空格字元(\t,\n,\r,\f,\v) a\sc a c
\D 匹配非數字字元 \Dabc aabc
\W 匹配非單詞字元 a\Wc a c
\S 匹配非空格字元 \S\Sc 1bc
[] 字符集,對應位置上可以是字符集裡的任意字元 a[def]c aec
[^] 對字符集當中的內容進行取反 a[^def]c a2c
[a-z] 指定一個範圍字符集 a[A-Z]c aBc
* 允許前一個字元可以出現0次或者無限次 a*b aaab或b
+ 前一個字元至少出現1次 a+b aaab或ab
? 前一個字元只能出現一次或者不出現 a?b ab或b
{m} 允許前一個字元只能出現m次 a{3}b aaab
{m,n} 允許前一個字元至少出現m次,最多出現n次(如果不寫n,則代表至少出現m次) a{3,5}b和a{3,} aaaab和aaaaaab
^ 匹配字串的開始,多行內容時匹配每一行的開始 ^abc abc
$ 匹配字串的結尾,多行內容時匹配每一行的結尾 abc& abc
\A 匹配字串開始位置,忽略多行模式 \Aabc abc
\Z 匹配字串結束位置,忽略多行模式 abc\Z abc
\b 匹配位於單詞開始或結束位置的空字串 hello \bworld hello world
\B 匹配不位於單詞開始或結束位置的空字串 he\Bllo hello
| 表示左右表示式任意滿足一種即可 abc|cba abc或cba
(…) 將被括起來的表示式作為一個分組,可以使用索引單獨取出 (abc)d abcd
(?P<name>…) 為該分組起一個名字,可以用索引或名字去除該分組 (?P<id>abc)d abcd
\number 引用索引為number中的內容 (abc)d\1 abcdabc
(?P=name) 引用該name分組中的內容 (?P<id>abc)d(?P=id) abcdabc
(?:…) 分組的不捕獲模式,計算索引時會跳過這個分組 (?:a)b(c)d\1 abcdc
(?iLmsux) 分組中可以設定模式,iLmsux之中的每個字元代表一個模式 (?i)abc Abc
(?#…) 註釋,#後面的內容會被忽略 ab(?#註釋)123 ab123
(?=…) 順序肯定環視,表示所在位置右側能夠匹配括號內正則 a(?=\d) a1最後的結果得到a
(?!…) 順序否定環視,表示所在位置右側不能匹配括號內正則 a(?!\w) a c最後的結果得到a
(?<=…) 逆序肯定環視,表示所在位置左側能夠匹配括號內正則 1(?<=\w)a 1a
(?<!…) 逆序否定環視,表示所在位置左側不能匹配括號內正則 1 (?<!\w)a 1 a
(?(id/name)yes|no) 如果前面的索引為id或者名字為name的分組匹配成功則匹配yes區域的表示式,否則匹配no區域的表示式,no可以省略 (\d)(?(1)\d|a) 32

  上面表格中(?iLmsux)這裡的”i”, “L”, “m”, “s”, “u”, “x”,它們不匹配任何字串,而對應re模組中(re.S|re.S):

I:re.I# 忽略大小寫
L:re.L# 字符集本地化,為了支援多語言版本的字符集使用環境
U :re.U# 使用\w,\W,\b,\B這些元字元時將按照UNICODE定義的屬性
M:re.M # 多行模式,改變 ^ 和 $ 的行為
S:re.S  # '.' 的匹配不受限制,包括換行符
X:re.X # 冗餘模式,可以忽略正則表示式中的空白和#號的註釋

對於一個特殊字元在正則表示式中是不能正常識別的,如果接觸過其他語言我們就這到有一個叫做轉移字元的東西的存在,在特殊字元前加用反斜槓介面。比如\n換行\\為反斜槓,在這不再累述。下面來介紹一下re這個模組。

1.2.re模組

 此模組主要方法如下

re.match()#嘗試從字串的起始位置匹配一個模式(pattern),如果不是起始位置匹配成功的話,match()就返回None
re.search()#函式會在字串內查詢模式匹配,只要找到第一個匹配然後返回,如果字串沒有匹配,則返回None。
re.findall()#遍歷匹配,可以獲取字串中所有匹配的字串,返回一個列表。
re.compile()#編譯正則表示式模式,返回一個物件的模式。(可以把那些常用的正則表示式編譯成正則表示式物件,這樣可以提高一點效率。)
re.sub()#使用re替換string中每一個匹配的子串後返回替換後的字串。
re.subn()#返回替換次數
re.split()#按照能夠匹配的子串將string分割後返回列表。

1.2.1.re.match()

方法: re.match(pattern, string, flags=0)#pattern:正則表示式(或者正則表示式物件)string:要匹配的字串flags:修飾符

  先看一個最簡單的用法

import re
content ='Hello 123 4567 wangyanling REDome'
print(len(content))
result = re.match('^Hello\s\d\d\d\s\d{4}\s\w{10}.*Dome$', content)
print(result)
print(result.group())
print(result.span())

  結果:

匹配規則就不在累述,以上需要注意的是

(1).group()表示的是返回正則匹配的結果

(2).span()表示返回正則匹配的範圍

使用:

以上我們已經知道re.matcha()的具體方法,那麼接下我來看一下具體使用,對此我們要理解以下幾種匹配的感念。

  1.泛匹配(.*):匹配所有字元

import re
content ='Hello 123 4567 wangyanling REDome'
result = re.match('^Hello.*Dome$', content)
print(result)
print(result.group())
print(result.span())

它的結果是和上面的輸出結果完全一樣的。

  2.目標匹配(()):將需要的字元匹配出來

import re
content ='Hello 123 4567 wangyanling REDome'
result = re.match('^Hello\s\d\d(\d)\s\d{4}\s\w{10}.*Dome$', content)
print(result)
print(result.group(1))
import re
content ='Hello 123 4567 wangyanling REDome'
result = re.match('^Hello\s(\d+)\s\d{4}\s\w{10}.*Dome$', content)
print(result)
print(result.group(1))

結果

以上可以看出:

(1)()匹配括號內的表示式,也表示一個組 (2)+ 匹配1個或多個的表示式 * 匹配0個或多個的表示式 (3).group(1)—輸出第一個帶有()的目標

   3.貪婪匹配(.*()):匹配儘可能少的的結果

import re
content ='Hello 123 4567 wangyanling REDome'
result = re.match('^H.*(\d+).*Dome$', content)
print(result)
print(result.group(1))

結果

     4.貪婪匹配(.*?()):匹配儘可能多的結果

import re
content ='Hello 123 4567 wangyanling REDome'
result = re.match('^H.*?(\d+).*?Dome$', content)
print(result)
print(result.group(1))

結果

以上3,4兩個匹配方式請儘量採用非貪婪匹配

    5.其他

換行:

import re
content ='''Hello 123 4567 
         wangyanling REDome'''

result = re.match('^H.*?(\d+).*?Dome$', content,re.S)#re.S
print(result.group(1))
result = re.match('^H.*?(\d+).*?Dome$', content)
print(result.group(1))

結果:

轉義字元:

import re
content = 'price is $5.00'
result = re.match('price is $5.00', content)
print(result)
result = re.match('price is \$5\.00', content)
print(result)

結果:

 其中re.I使匹配對大小不敏感,re.S匹配包括換行符在內的所有字元,\進行處理轉義字元。匹配規則中有詳細介紹。

1.2.2.re.search()

方法: 

re.search(pattern, string, flags=0)#pattern:正則表示式(或者正則表示式物件)string:要匹配的字串flags:修飾符
    #re.match()和re.search()用法類似唯一的區別在於re.match()從字串頭開始匹配,若頭匹配不成功,則返回None    

對比一下與match()

import re
content ='Hello 123 4567 wangyanling REDome'
result = re.match('(\d+)\s\d{4}\s\w{10}.*Dome$', content)
print(result)#從開頭開始查詢,不能匹配返回None
result = re.search('(\d+)\s\d{4}\s\w{10}.*Dome$', content)
print(result)
print(result.group())

 結果:

可以看出兩個使用基本一致,search從頭開始匹配,如果匹配不到就返回none.

1.2.3.re.findall()

方法: re.finditer(pattern, string, flags=0)#pattern:正則表示式(或者正則表示式物件)string:要匹配的字串flags:修飾符 

與re.search()類似區別在於re.findall()搜尋string,返回一個順序訪問每一個匹配結果(Match物件)的迭代器。找到 RE 匹配的所有子串,並把它們作為一個迭代器返回。

import re

html = '''
  <div>
  <li><a href="" singer="魯迅">吶喊</a></li>
  <li><a href="#" singer="賈平凹">廢都</a></li>
  <li class="active"><a href="#" singer="路遙">平凡世界</a></li>
  <span class="rightSpan">謝謝支援</span>
  </div>
'''
regex_4='<a.*?>(.*?)</a>'
results=re.findall(regex_4,html,re.S)
print(results)
for result in results:
    print(result)

結果:

1.2.4.re.compile()

編譯正則表示式模式,返回一個物件的模式。

方法: re.compile(pattern,flags=0)#pattern:正則表示式(或者正則表示式物件);flags:修飾符 

看一個demo

import re
content ='Hello 123 4567 wangyanling REDome wangyanling 那小子很帥'
rr = re.compile(r'\w*wang\w*')
result =rr.findall(content)
print(result)

結果:

我們可以看出compile 我們可以把它理解為封裝了一個公用的正則,類似於方法,然後功用。

1.2.5.其他

re.sub 替換字元

方法: re.sub(pattern, repl, string, count=0, flags=0)#pattern:正則表示式(或者正則表示式物件)repl:替換的字串string:要匹配的字串count:要替換的個數flags:修飾符 

re.subn 替換次數

方法: re.subn(pattern, repl, string, count=0, flags=0)#pattern:正則表示式(或者正則表示式物件)repl:替換的字串string:要匹配的字串count:要替換的個數flags:修飾符 

re.split()分隔字元

方法

re.split(pattern, string,[maxsplit])#正則表示式(或者正則表示式物件)string:要匹配的字串;maxsplit:用於指定最大分割次數,不指定將全部分割

2.案例:爬取貓眼資訊,寫入txt,csv,下載圖片

2.1.獲取單頁面資訊

def get_one_page(html):
    pattern= re.compile('<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name"><a.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime'
                         + '.*?>(.*?)</p>.*?score.*?integer">(.*?)</i>.*?>(.*?)</i>.*?</dd>',re.S)#這裡就用到了我們上述提到的一些知識點,非貪婪匹配,物件匹配,修飾符
    items = re.findall(pattern,html)
    for item in  items:
        yield {
            'rank' :item[0],
            'img':  item[1],
            'title':item[2],
            'actor':item[3].strip()[3:] if len(item[3])>3 else '',  
            'time' :item[4].strip()[5:] if len(item[4])>5 else '',
            'score':item[5] + item[6]
        }

對於上面的資訊我們可以看出是存到一個物件中那麼接下來我們應該把它們存到檔案當中去。

2.2.儲存檔案

我寫了兩種方式儲存到txt和csv這些在python都有涉及,不懂得可以去翻看一下。

2.2.1.儲存到txt

def write_txtfile(content):
    with open("Maoyan.txt",'a',encoding='utf-8') as f:
        #要引入json,利用json.dumps()方法將字典序列化,存入中文要把ensure_ascii編碼方式關掉
        f.write(json.dumps(content,ensure_ascii=False) + "\n")
        f.close()

結果:

以上看到並非按順序排列因為我用的是多執行緒。

2.2.2.儲存到csv

def write_csvRows(content,fieldnames):
    '''寫入csv檔案內容'''
    with open("Maoyao.csv",'a',encoding='gb18030',newline='') as f:
        #將欄位名傳給Dictwriter來初始化一個字典寫入物件
        writer = csv.DictWriter(f,fieldnames=fieldnames)
        #呼叫writeheader方法寫入欄位名
        writer.writerows(content)
        f.close()

結果:

那麼還有一部就是我們要把圖片下載下來。

2.2.3.下載圖片

def download_img(title,url):
   r=requests.get(url)
   with open(title+".jpg",'wb') as f:
        f.write(r.content)

2.3.整體程式碼

這裡面又到了多執行緒在這不在敘述後面會有相關介紹。這個demo僅做一案例,主要是對正則能有個認知。上面寫的知識點有不足的地方望大家多多指教。

#抓取貓眼電影TOP100榜
from multiprocessing import Pool
from requests.exceptions import RequestException
import requests
import json
import time
import csv
import re
def get_one_page(url):
    '''獲取單頁原始碼'''
    try:
        headers = {
            "User-Agent":"Mozilla/5.0(WindowsNT6.3;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/68.0.3440.106Safari/537.36"
        }
        res = requests.get(url, headers=headers)
        # 判斷響應是否成功,若成功列印響應內容,否則返回None
        if res.status_code == 200:
            return res.text
        return None
    except RequestException:
        return None
def parse_one_page(html):
    '''解析單頁原始碼'''
    pattern = re.compile('<dd>.*?board-index.*?>(\d+)</i>.*?data-src="(.*?)".*?name"><a.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime'
                         + '.*?>(.*?)</p>.*?score.*?integer">(.*?)</i>.*?>(.*?)</i>.*?</dd>',re.S)
    items = re.findall(pattern,html)
    #採用遍歷的方式提取資訊
    for item in  items:
        yield {
            'rank' :item[0],
            'img':  item[1],
            'title':item[2],
            'actor':item[3].strip()[3:] if len(item[3])>3 else '',  #判斷是否大於3個字元
            'time' :item[4].strip()[5:] if len(item[4])>5 else '',
            'score':item[5] + item[6]
        }

def write_txtfile(content):
    with open("Maoyan.txt",'a',encoding='utf-8') as f:
        #要引入json,利用json.dumps()方法將字典序列化,存入中文要把ensure_ascii編碼方式關掉
        f.write(json.dumps(content,ensure_ascii=False) + "\n")
        f.close()
def write_csvRows(content,fieldnames):
    '''寫入csv檔案內容'''
    with open("Maoyao.csv",'a',encoding='gb18030',newline='') as f:
        #將欄位名傳給Dictwriter來初始化一個字典寫入物件
        writer = csv.DictWriter(f,fieldnames=fieldnames)
        #呼叫writeheader方法寫入欄位名
        #writer.writeheader()            ###這裡寫入欄位的話會造成在抓取多個時重複.
        writer.writerows(content)
        f.close()
def download_img(title,url):
   r=requests.get(url)
   with open(title+".jpg",'wb') as f:
        f.write(r.content)
def main(offset):
    fieldnames = ["rank","img", "title", "actor", "time", "score"]
    url = "http://maoyan.com/board/4?offset={0}".format(offset)
    html = get_one_page(url)
    rows = []
    for item in parse_one_page(html):
        #download_img(item['rank']+item['title'],item['img'])
        write_txtfile(item)
        rows.append(item)
    write_csvRows(rows,fieldnames)

if __name__ == '__main__':
    pool = Pool()
    #map方法會把每個元素當做函式的引數,建立一個個程序,在程序池中執行.
    pool.map(main,[i*10 for i in range(10)])