1. 程式人生 > 實用技巧 >爬蟲入門四 re

爬蟲入門四 re


title: 爬蟲入門四 re
date: 2020-03-14 16:49:00
categories: python
tags: crawler

正則表示式與re庫

1 正則表示式簡介

編譯原理學過的

正則表示式(Regular Expression,簡寫為regex或RE),使用單個字串來描述、匹配一系列匹配某個句法規則的字串。在很多文字編輯器裡,正則表示式通常被用來檢索、替換那些匹配某個模式的文字。

Python 中匯入 :import re
官方參考文件:https://docs.python.org/3/library/re.html

正則表示式元素可以歸為字元和操作符,操作符又包含了限定符和定位符

字元: 字元可以代表一個單獨的字元,或者一個字元集合構成的字串。
限定符:允許你在模式中決定字元或者字串出現的頻率。
定位符:允許你決定模式是否是一個獨立的單詞,或者出現的位置必須在句
子的開頭還是結尾。

http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html

1.1 正則表示式 常用操作符

. 表示任何單個字元
[] 字符集,對單個字元給出取值範圍  [abc]表示a、b、c, [a-z ]表示a到z單個字元 [\d\.] 表示包含數字和小數點
[^ ] 非字符集,對單個字元給出排除範圍 [^abc ]表示非a或b或c的單個字元
* 前一個字元0次或無限次擴充套件  abc*表示ab、abc、abcc. abccc等
+ 前一個字元1次或無限次擴充套件 abc+表示abc、abcc、 abccc等
? 前一個字元0次或1次擴充套件 abc?表示ab、abc
| 左右表示式任意-個 abc|def表示abc、 def
{m} 擴充套件前一個字元m次  ab{2}c表示abbc
{m,n} 擴充套件前一個字元m至n次(含n) ab{1, 2}c表示abC、abbc
^ 匹配字串開頭  ^abc表示abc且在一個字串的開頭
$ 匹配字串結尾 abc$表示abc且在一個字串的結尾
() 分組標記,內部只能使用|操作符  (abc )表示abc , (abc |def)表示abc、def
\d 數字,等價於[0-9]
\w 單詞字元,等價於[A-Za-z0-9]

" 表示轉義 "。比如 下面例項淘寶中price":"412.00"
r'"view_price":"[\d.]*"'

[\d.] 表示包含數字和小數點

1.2 re例項

正則表示式線上檢查網站:
http://tool.oschina.net/regex/

email地址:
只允許英文字母、數字、下劃線、英文句號、以及中劃線組成
[1]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$

PY[^TH]?ON  'PYON'、'PYaON'、 ' PYbON'、'PYcON'...
PY{ :3}N 'PN'、 'PYN'、 'PYYN'、 'PYYYN'...
^[A-Za-z]+$  由26個字母組成的字串
^[A-Za-z0-9]+$  由26個字母和數字組成的字串
^-?\d+$  整數形式的字串
^[0-9]*[1-9][0-9]*$  正整數形式的字串
[1-9]\d{5}  首位不為0 中國境內郵政編碼, 6位
[\u4e00-\u9fa5]  匹配中文字元
\d{3}-\d{8}|\d{4}-\d{7}  國內電話號碼, 010-68913536
IP地址字串形式的正則表示式( IP地址分4段,每段0-255 )
\d+.\d+.\d+.\d+或\d{1,3}. \d{1,3}.\d{1,3}. \d{1,3}
精確寫法
0-99: [1-9]?\d
100-199: 1\d{2}
200-249: 2[0-4]\d
250-255: 25[0-5]
(([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5]).){3}([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])

2 re庫

2.1 re庫中正則式的使用

re庫採用raw string(原生字串)型別表示正則表示式,表示為:
r'text'
例如: r'[1‐9]\d{5}'
r'\d{3}‐\d{8}|\d{4}‐\d{7}'

>>> import re
>>> m=re.search(r'[0-9]','wu1han2')
>>> print(m.group(0))
可以得到返回結果:1

2.2 re庫常用功能函式

re. search( )
在一個字串中搜索匹配正則表示式的第一個位置,返回match物件
re. match( )
從一個字串的開始位置起匹配正則表示式,返回match物件
re. findall()
搜尋字串,以列表型別返回全部能匹配的子串
re. split( )
將一個字串按照正則表示式匹配結果進行分割,返回列表型別
re. finditer( )
搜尋字串,返回-一個匹配結果的迭代型別,每個迭代元素是match物件
re.sub( )
在一個字串中替換所有匹配正則表示式的子串,返回替換後的字串

2.2.1 re.search()

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

在一個字串中搜索匹配正則表示式的第一個位置
pattern : 正則表示式的字串或原生字串表示
string : 待匹配字串
flags : 正則表示式使用時的控制標記

re.I re. IGNORECASE
忽略正則表示式的大小寫, [A- Z]能夠匹配小寫字元
re.M re . MULTILINE
正則表示式中的^操作符能夠將給定字串的每行當作匹配開始
re.S re. DOTALL
正則表示式中的.操作符能夠匹配所有字元,預設匹配除換行外的所有字元

2.2.2 re.match()

re.match(pattern, string, flags=0)
從一個字串的開始位置起匹配正則表示式

import re
search=re.search(r'[1-9]\d{5}','WuHan 430073')
match1=re.match(r'[1-9]\d{5}','WuHan 430073')
match2=re.match(r'[1-9]\d{5}','430073 WuHan')
print("search結果")
if search:    
    print(search.group(0))
print("\nmatch1結果")
if match1:    
    print(match1.group(0))
else:    
    print("未匹配到結果")
print("\nmatch2結果")
if match2:    
    print(match2.group(0))

上面輸出

430073
未匹配
430073

2.2.3 re.findall .finditer

re.findall(pattern, string, flags=0)
搜尋字串,以列表型別返回全部能匹配的子串
re.finditer(pattern, string, flags=0)
搜尋字串,返回一個匹配結果的迭代型別,每個結果為match物件

import re
findall=re.findall(r'[1-9]\d{5}','WuHan 430073  Beijing 100010')
print("findall匹配結果")
if findall:    
    print(findall)
print("finditer匹配結果")
for m in re.finditer(r'[1-9]\d{5}','WuHan 430073  Beijing 100010'):
    if m:        
        print(m.group(0))

# 輸出
[430073,100010]

430073
100010

2.2.4 re.split()

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

將一個字串按照正則表示式匹配結果進行分割,返回列表型別
pattern : 正則表示式的字串或原生字串表示
string : 待匹配字串
maxsplit: 最大分割數,剩餘部分作為最後一個元素輸出
flags : 正則表示式使用時的控制標記

就是把pattern當做分隔符

import re
split1=re.split(r'[1-9]\d{5}','WuHan 430073  Beijing 100010')
split2=re.split(r'[1-9]\d{5}','WuHan 430073  Beijing 100010',maxsplit=1)
print("split1分割結果")
print(split1)
print("split2分割結果")
print(split2)

split1分割結果
['WuHan ', '  Beijing ', '']
split2分割結果
['WuHan ', '  Beijing 100010']

2.2.5 re.sub

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

在一個字串中替換所有匹配正則表示式的子串返回替換後的字串
pattern : 正則表示式的字串或原生字串表示
repl : 替換匹配字串的字串
string : 待匹配字串
count : 匹配的最大替換次數
flags : 正則表示式使用時的控制標記

sub = re.sub(r'[1-9]\d{5}', '123456', 'WuHan 430073  Beijing 100010')
print("替換結果")
print(sub)

替換結果
WuHan 123456  Beijing 123456

2.3 re庫面向物件用法

regex = re.compile(pattern, flags=0)
pattern : 正則表示式的字串或原生字串表示
flags : 正則表示式使用時的控制標記

regex = re.compile(r‘[1‐9]\d{5}’)
將正則表示式的字串形式編譯成正則表示式物件,編譯後,可進行多次操作

如下,就相當於用pat代表了這個re,方便了書寫

pat = re.compile(r'[1-9]\d{5}')
a = pat.search('wuhan430073')
print("搜尋結果")
print(a)
b = pat.sub('...','bj100010 wh430073')
print("替換結果")
print(b)

搜尋結果
<re.Match object; span=(5, 11), match='430073'>
替換結果
bj... wh...

這裡regex就算自定義的物件,如上面的pat.search()

regex. search( )
在一個字串中搜索匹配正則表示式的第一個位置,返回match物件
regex. match( )
從一個字串的開始位置起匹配正則表示式,返回match物件
regex . findall()
搜尋字串,以列表型別返回全部能匹配的子串
regex.split()
將一個字串按照正則表示式匹配結果進行分割,返回列表型別
regex. finditer()
搜尋字串,返回一個匹配結果的迭代型別,每個迭代元素是match物件
regex. sub( ) 
在一個字串中替換所有匹配正則表示式的子串,返回替換後的字串

2.4 re庫的match物件

match物件包含了關於此次匹配的資訊,可以使用Match提供的可讀屬性或方法來獲取這些資訊。

.string
待匹配的文字
.re
匹配時使用的patter物件(正則表示式)
.pos
正則表示式搜尋文字的開始位置 就是待匹配串從哪裡開始搜尋
.endpos
正則表示式搜尋文字的結束位置 就是待匹配串從哪裡借結束搜尋,比如19,說明搜尋到18(含18)
.group(0)
獲得匹配後的字串
.start()
匹配字串在原始字串的開始位置
.end()
匹配字串在原始字串的結束位置
.span()
返回(.start(), .end()

    for m in re.finditer(r'[1-9]\d{5}', 'WuHan 430073  Beijing 100010'):
        if m:
            print(m.group(0))
            print(m.pos)
            print(m.endpos)
            print(m.start())
            print(m.end())
            print(m.span())

123456
430073
0
28
6
12
(6, 12)
100010
0
28
22
28
(22, 28)

2.5 re庫的貪婪匹配,最小匹配

match = re. search(r'PY.*N', ' PYANBNCNDN' )
match. group(0)
同時匹配長短不同的多項,返回哪個呢?(PYAN PYANBN PYANBNCN ...)
結果:
'PYANBNCNDN'

Re庫預設採用貪婪匹配,即輸出匹配最長的子串

如何輸出最短的子串呢?

match = re. search(r'PY.*?N', ' PYANBNCNDN' )
match. group(0)
'PYAN '

只要長度輸出可能不同的,都可以通過在操作符後增加?變成最小匹配

*? 前一個字元0次或無限次擴充套件,最小匹配
+? 前一個字元1次或無限次擴充套件,最小匹配
?? 前一個字元0次或1次擴充套件,最小匹配
{m,n}?  擴充套件前一個字元m至n次(含n),最小匹配

3 例項一 淘寶商品定向爬蟲

3.1 url的確定

比如搜鍵盤
第一頁:
https://s.taobao.com/search?q=鍵盤&imgfile=&commend=all&ssid=s5-e&search_type=item&sourceId=tb.index&spm=a21bo.2017.201856-taobao-item.1&ie=utf8&initiative_id=tbindexz_20170306

第二頁:
https://s.taobao.com/search?q=鍵盤&imgfile=&commend=all&ssid=s5-e&search_type=item&sourceId=tb.index&spm=a21bo.2017.201856-taobao-item.1&ie=utf8&initiative_id=tbindexz_20170306&bcoffset=3&ntoffset=3&p4ppushleft=1%2C48&s=44
第三頁:
https://s.taobao.com/search?q=鍵盤&imgfile=&commend=all&ssid=s5-e&search_type=item&sourceId=tb.index&spm=a21bo.2017.201856-taobao-item.1&ie=utf8&initiative_id=tbindexz_20170306&bcoffset=0&ntoffset=6&p4ppushleft=1%2C48&s=88

然後切回第一頁
https://s.taobao.com/search?q=鍵盤&imgfile=&commend=all&ssid=s5-e&search_type=item&sourceId=tb.index&spm=a21bo.2017.201856-taobao-item.1&ie=utf8&initiative_id=tbindexz_20170306&bcoffset=0&ntoffset=0&s=0

觀察發現:
每頁44個商品。

然後
&bcoffset=3&ntoffset=3&p4ppushleft=1%2C48

&imgfile=&commend=all&ssid=s5-e&search_type=item&sourceId=tb.index&spm=a21bo.2017.201856-taobao-item.1&ie=utf8&initiative_id=tbindexz_20170306

這些資訊不用,比如
https://s.taobao.com/search?q=鍵盤&s=44

%E9%94%AE%E7%9B%98 是搜尋的物品。
就可以訪問第二頁

這裡我不清楚,可能是是這些是類似時間戳一類的資訊,呼叫的引數等待??,不明白

第一頁可以直接https://s.taobao.com/search?q='goods'訪問。

所以url的有效部分就是
https://s.taobao.com/search?q= &s=
https://s.taobao.com/search?q='goods'&s='str(44 * i)'

    goods = '鍵盤'
    depth = 3
    start_url = 'https://s.taobao.com/search?q='+ goods
    infoList = []
    for i in range(depth):
        try:
            url = start_url + '&s=' + str(44 * i)

3.2 商品價格和名稱

然後找到商品和價格格式
這裡用檢視原始碼,而不是檢查搜尋一款鍵盤的價格//名字
"raw_title":"黑爵AK35i機械鍵盤青軸黑軸茶軸吃雞巨集電競"
"view_price":"812.42"

可見格式,用re表達
plt = re.findall(r'"view_price":"[\d.]"', html)
tlt = re.findall(r'"raw_title":".
?"', html)

然後,對搜尋結果進行分割,以“:”為分割點,價格和格式,為分割點後的內容
新增至列表中

for i in range(len(plt)):
    price = eval(plt[i].split(':')[1])
    title = eval(tlt[i].split(':')[1])
    ilt.append([price, title])

eval():將字串當成有效的表示式來求值並返回計算結果
增加eval()後,返回結果無引號
"99.12"->99.12

3.3 code

步驟1:提交商品搜尋請求,迴圈獲取頁面
getHTMLText()
步驟2:對於每個頁面,提取商品名稱和價格資訊
parsePage()
步驟3:將資訊輸出到螢幕上
printGoodsList()

# 淘寶商品定向爬蟲,序號,價格,名稱

# 問題是在getHTMLText的時候需要登入。待解決。(用cookie,request.session更新cookie?)
import requests
import re
def getHTMLText(url):
    try:
        r = requests.get(url, timeout=30)
        r.raise_for_status()
        r.encoding = r.apparent_encoding
        #print(r.text)
        return r.text
    except:
        print("gethtmlfailed")
        return ""

def parsePage(ilt, html):
    try:
        plt = re.findall(r'\"view_price\"\:\"[\d\.]*\"', html)
        tlt = re.findall(r'\"raw_title\"\:\".*?\"', html)
        for i in range(len(plt)):
            price = eval(plt[i].split(':')[1])
            title = eval(tlt[i].split(':')[1])
            ilt.append([price, title])
    except:
            print("parsePagefailed")
def printGoodsList(ilt):
    tplt = "{:4}\t{:8}\t{:16}"
    print(tplt.format("序號", "價格", "商品名稱"))
    count = 0
    for g in ilt:
        count = count + 1
        print(tplt.format(count, g[0], g[1]))

def main():
    goods ="鍵盤"
    depth = 3
    start_url = 'https://s.taobao.com/search?q='+ goods
    infoList = []
    for i in range(depth):
        try:
            url = start_url + '&s=' + str(44 * i)
            html = getHTMLText(url)
            parsePage(infoList,html)
        except:
            continue
    printGoodsList(infoList)

if __name__=='__main__':
    main()

3.4 問題

跑不出來,發現淘寶需要登入。
網路上有一些模擬登陸的方法。

需要request.session

4 例項二 股票資訊定向爬取 找尋爬取網址

4.1 問題

例項程式碼不知道是什麼時候的了,股市通web版都倒了

下面的內容看看就好

4.2 視覺化進度,處理個別情況

在爬取到個股資訊後,需要將其寫入檔案中,
with open(fpath, 'a', encoding='utf-8') as f:
f.write(str(infoDict) + '\n')
由於爬取資料量大,爬取速度較慢,為了方便使用者看到爬取進度,可以定義全域性變數count,每爬取一支個股後count=count+1 。總進度為count除以list的長度。
為了顯示更新效果,可以在print內容中加入”\r”
使新列印的一行,沖掉之前舊的一行,達到顯示進度不斷增大的效果。
count = count + 1
print("\r當前進度: {:.2f}%".format(count * 100 / len(lst)), end="")
同時,使用try:
***
except:
continue
的結構,使個別爬取結果空時不會影響總的爬取過程

4.3 編碼加速

在獲取HTML時,用到了r.encoding= r.apparent_encoding
r.apparent_encoding需要分析文字,因此執行較慢
因此,當已知目標網頁的編碼的方式時,可以直接定義code=“編碼方式“
令r.encoding = code
通過檢視東方財富網的原始碼,
head中獲取到它的編碼方式為“gb2312”
而百度股市通編碼方式為“utf-8”

優化後爬取速度仍然較慢,因此,當爬取較大資料量的網頁資訊時,可採取多執行緒技術、或使用更適合大規模資料爬取的scrapy框架

4.4 股票標號

檢視東方財富網網頁原始碼,可以看到,股票標號可以在a標籤的href值中找到

用BeautifulSoup的find_all方法找到所有a標籤,得到href中的內容,
再使用正則表示式,找到:
以s開頭,第二個字母為h或者z之後是六位數字 // sh 上海 sz 深圳
正則表示式為:r"[s][hz]\d{6}“
的物件加入到lst中

4.5 updata split

根據股票列表逐個到百度股票獲取個股資訊
for stock in lst:
url = stockURL + stock + ".html"
html = getHTMLText(url)

檢視個股網頁原始碼
可以看出,股票資訊都在class為stock-bets的
div標籤下,將其存入stockinfo:
stockInfo = soup.find('div', attrs={'class': 'stock-bets'})
股票名儲存在class為bets-name的
name = stockInfo.find_all(attrs={'class': 'bets-name'})[0]
infoDict.update({'股票名稱': name.text.split()[0]})

update()方法語法:dict.update(dict2),把字典dict2的鍵/值對更新到dict裡。
split():當不帶引數時以空格進行分割,當代引數時,以該引數進行分割。

4.6 key-value

股票當日數值均在

標籤的
標籤中,而這些數值的含義,均在
標籤的
標籤中。
因此,可以構建多組鍵值對:
keyList = stockInfo.find_all(‘dt’) #鍵
valueList = stockInfo.find_all(‘dd’) #值
for i in range(len(keyList)):
key = keyList[i].text
val = valueList[i].text
infoDict[key] = val #將這對鍵值對存入字典中

4.7 code

#CrawBaiduStocksB.py

# 這個例子不知道是啥時候的了,股市通web都倒了(哭腔)
#可以換成同花順?
import requests
from bs4 import BeautifulSoup
import traceback
import re
def getHTMLText(url, code="utf-8"):
    try:
        r = requests.get(url)
        r.raise_for_status()
        r.encoding = code
        return r.text
    except:
        return ""

def getStockList(lst, stockURL):
    html = getHTMLText(stockURL, "GB2312")
    soup = BeautifulSoup(html, 'html.parser')
    a = soup.find_all('a')
    for i in a:
        try:
            href = i.attrs['href']
            lst.append(re.findall(r"[s][hz]\d{6}", href)[0])
        except:
            continue
def getStockInfo(lst, stockURL, fpath):
    count = 0   #進度
    for stock in lst:
        url = stockURL + stock + ".html"
        html = getHTMLText(url)
        try:
            if html == "":
                continue
            infoDict = {}
            soup = BeautifulSoup(html, 'html.parser')
            stockInfo = soup.find('div', attrs={'class': 'stock-bets'})
            name = stockInfo.find_all(attrs={'class': 'bets-name'})[0]
            infoDict.update({'股票名稱': name.text.split()[0]})
            keyList = stockInfo.find_all('dt')
            valueList = stockInfo.find_all('dd')
            for i in range(len(keyList)):
                key = keyList[i].text
                val = valueList[i].text
                infoDict[key] = val
            with open(fpath, 'a', encoding='utf-8') as f:
                f.write(str(infoDict) + '\n')
                count = count + 1
                print("\r當前進度: {:.2f}%".format(count * 100 / len(lst)))
        except:
            count = count + 1
            print("\r當前進度: {:.2f}%".format(count * 100 / len(lst)))
            continue

def main():
    stock_list_url = 'http://quote.eastmoney.com/stocklist.html'
    stock_info_url = 'https://gupiao.baidu.com/stock/'
    output_file = 'D:/BaiduStockInfo.txt'
    slist = []
    getStockList(slist, stock_list_url)
    getStockInfo(slist, stock_info_url, output_file)

main()


  1. a-zA-Z0-9_- ↩︎