如果說要把 python 作爲專業開發工具,那麼就必須理解爬蟲;如果想要悟透爬蟲,那麼就必須掌握正則表達式。
突破常規,先吟一曲小詩表達一下我作本文的那份信心:
爬蟲路漫遠兮,
python求索嘆奇。
一文看盡正則,
嘴揚心笑生焉。本文將用最直白的描述、最簡單的實例詳盡講解正則表達式的入門思想;
待你讀完本文,怡然回首,定會嘆曰:“呵!正則!不過如此~~~”
目錄
1 項目概覽
1.1 概念引入
首先我們理解兩個概念:
①爬蟲:說白了,爬蟲就是能夠按照制定規則自動瀏覽網絡信息的程序,並且能夠存儲我們需要的信息。
②正則表達式:簡單而言,就是對字符串過濾用的;
具體而言,就是對字符串的一種邏輯公式,即用事先定義好的特定字符,以及這些字符的組合,組合成一個“規則字符串”,並用這個“規則字符串”表達對字符串的過濾。
事實上,正則表達式不只限用於python爬蟲;比如高效判別身份證號碼真僞、驗證Email地址、正確匹配ip地址等都是離不開對於正則表達式的理解和掌握的。廣義上來講,所有的編程開發人員都必須掌握正則表達式。
那麼我們爲什麼要掌握正則表達式呢?
正則表達式目的:①判斷字符串是否符合正則表達式的邏輯;
②通過正則表達式從特定字符串中獲取我們需要的特定部分。
1.2 實例開發準備
①介紹正則表達式所用實例開發語言爲 python語言 ;
②python環境: python 3.8.2 ;
③python編譯器:JetBrains PyCharm 2018.1.2 x64 ;
④主要用到的包庫有:URL處理模塊——urllib 模塊包、re 正則表達式模塊 等。
⑤主體內容及簡介:
- 第一部分(實戰解說):任務是下載小說《斗羅大陸》。具體而言主要通過正則表達式將HTML文件中每一章節的 URL 獲取到並通過for循環將每一章節內的文字部分提取並下載到指定文件夾中的 .txt 文件中。
- 第二部分(學以致用):目標是爬取b站中的視頻彈幕並生成詞雲。具體而言,是爬取名爲我的NBA手辦真的會打球!!!視頻中的上千條實時彈幕,並通過jieba庫和wordcloud庫生成詞云然後以圖片形式輸出到本地。
2 實戰解說
這一部分我們以小說網站全書網爲例,解析並下載網站內的連載小說《斗羅大陸》。
2.1 獲取目標頁面及對應的HTML文件。
獲取網站對應HTML文件我們的核心代碼是用對象.函數(參數).調用對象返回的方法()實現的。具體代碼如下:
from urllib import request # 導入url處理模塊urllib包
# 獲取目標頁面 url ,並存入first_url中
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面,打印輸出相應的HTML文件
html = request.urlopen(first_url).read() # html = 對象.函數(參數).調用對象返回的方法()
print(html)
上圖是通過代碼得到的目標頁面對應HTML文件在PyCharm中的實現結果,
下圖是在Google Chrome瀏覽器中F12鍵顯示的Elements。
仔細對比,我們發現獲得的就是目標頁面對應的HTML文件,只不過按照橫排排列罷了
細心的小夥伴們還會注意到輸出內容最前面有小寫字母b,其含義是提示我們輸出的全部都是二進制數據
因此我們獲得的是二進制文件,而不是字符串文件,這是就涉及到了轉碼問題
二進制轉化爲字符串是需要編碼的,具體而言用decode()即可實現
轉碼格式是什麼呢?這時就需要知道該HTML文件的編碼
如下圖所示,在瀏覽器頁面Elements對應元素髮現charset=gbk,其中的gbk就是轉碼格式。
按照這一思路,在覈心代碼裏面加上decode('gbk'),輸出發現小寫字母b沒了,也就意味着二進制文件轉字符串成功啦!
html = request.urlopen(first_url).read().decode('gbk')
轉碼爲字符串格式後HTML文件輸出結果截圖(部分)
2.2 引入正則表達式
獲得到HTML文件意味着本文講解重點纔剛剛開始。
開篇提到,這一部分我們的任務是獲取每個章節對應的 url 並下載到指定文件夾下的 .txt 文件中。
在輸出地HTML文件中我們可以清晰看到每個章節的 url,但是你要知道,將近700章的內容,也就是有近700個 url,很顯然,一個一個手動獲取是不現實的。
爲了快速獲取近700個< href >標籤中的url,這時我們正式引入正則表達式。
接下來我們採取步步深入策略帶領大家探索奧妙奇趣的正則表達式世界!
引子
在正式講解正則表達式之前,我們先對相關知識做一鋪墊;“地基”打好了,蓋樓就容易多了。
在python中,re 模塊是不需要另外下載安裝的,也就是生來就有的。
這裏我們介紹一下 re 模塊中的 search() 方法和 findall() 方法。
search() 方法介紹:
該方法從前到後遍歷整個字符串,尋找特定位置,找到則立即返回。
import re
string = 'llabcdabcs'
res = re.search('abc',string)
print(res)
我們發現,在字符串 'llabcdabcs' 中尋找有沒有子串 'abc' ,有,即返回字符串對應位置且只返回一個。
注:字符串下標從 0 開始。
findall() 方法介紹:
該方法輸出結果是列表 ,且把所有匹配的子串全部返回回來(有多少個返回多少個)。
import re
string = 'llabcdabcs'
res = re.findall('abc',string)
print(res)
有了鋪墊的這部分知識,我們在剛剛解析出來的HTML文件中打印出指定的字符串部分就水到渠成了。
正則表達式實例詳解
假設我們想要獲取部分爲下圖藍色背景下的作者名及小說書名,怎麼做呢?
首先,獲取小說書名:
第一步,將想要獲取的內容複製粘貼到代碼中的 novel_info 部分,就像這樣(僅展示核心代碼):
novel_info['title'] = re.findall('<div class="chapName"><span class="r">作者:1416338685</span><strong>斗羅大陸</strong>',html)
如果至此就要運行的話,會出什麼問題呢?
事實上這樣是無法解析的,因爲複製過去的內容中含有許多符號,需要轉義;
這時,我們只需在前面加上 r 即可,這樣就完美避免了一一轉義的繁瑣步驟。
同樣,這裏展示核心代碼中改進後的代碼片段:
novel_info['title'] = re.findall(r'<div class="chapName"><span class="r">作者:1416338685</span><strong>斗羅大陸</strong>',html)
第二步,解析並獲取正則表達式的匹配模式,並替換掉上一步中 novel_info 的對應部分
首先給出獲取小說書名的最終代碼及結果截圖,以便進一步講解:
from urllib import request
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面
html = request.urlopen(first_url).read().decode('gbk')
# 小說的信息
novel_info = {}
# 小說的名字
novel_info['title'] = re.findall(r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
print(novel_info)
#print(html)
心心念唸的正則表達式它終於來啦,就是這麼一串:
'<div class="chapName">.*?<strong>(.*?)</strong>'
我們就是要通過這麼一串字符,輸出想要獲取的指定內容(在這裏即獲取小說書名)。
其中有一個 .*? 帶有括號,含義是分組並返回括號內所匹配的內容。
事實上,.*? 組合是最常用到的一種組合形式,那麼具體是什麼意思呢?
. 代表可以匹配一個任意字符(不包括不可見的換行字符)
* 代表可以任意次()地匹配它前面的子表達式(即“貪婪匹配”)
*? 代表“非貪婪匹配”
() 代表子表達式,把指定的內容放入緩存並返回
與前面講解思路類似,先通過一個簡單字符實例闡明一下其中的扼要:
import re
string = 'zoooo'
res = re.match('zo*',string)
print(res)
由下圖運行結果所示可知,我們匹配並得到了 * 號之前的以 zo 開頭的所有且任意長度的值(有多少幾把這些全部匹配出來),即得到了 zooo 。
還是上述這個例子,僅在其中加一 ?號看一下“非貪婪匹配”是什麼結果,並與“貪婪匹配”做一對比。
import re
string = 'zoooo'
res = re.match('zo*?',string)
print(res)
通過對比我們發現,在“非貪婪匹配”情況下,返回的值是貪婪匹配情況下返回的最小值(即任意數=0時的值)
即僅輸出 z 一個字符。
這時我們就很好理解本本分首先引入的含正則表達式的代碼了
我們只想得到小說書名,即“斗羅大陸”這幾個字,我們關鍵通過下面這一句程序實現的
novel_info['title'] = re.findall(r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
爲什麼要寫兩個 .*? 呢?
因爲整個 HTML 文件中的 <strong> …… </strong>標籤可能是非常多的,只寫一個的話可能還匹配到其他具有同樣標籤的值,這種情況是我們不想看到的,所以寫了兩個,令其僅特定指到我們想要的那部分。
而且稍前部分提到過,加括號 () 的原因是我們只想要獲得輸出括號內指定的數據。
哦,對了,有一點要特別注意,那就是輸出是以列表形式輸出的。不注意這點的話,編寫的代碼很可能報錯。
下面給出僅輸出小說書名的代碼(列表形式輸出):
from urllib import request
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面
html = request.urlopen(first_url).read().decode('gbk')
# 小說的信息
novel_info = {}
# 小說的名字
novel_info['title'] = re.findall(r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
print(novel_info['title'])
#print(html)
若不想以列表形式輸出,僅想得到列表中的值,應該怎麼做呢?
想必大家都已經想到了,就是在覈心代碼部分加 [0] 取出列表中的第一個值。
改進後的代碼如下(輸出列表中的值,即“斗羅大陸”這四個字:)
from urllib import request
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面
html = request.urlopen(first_url).read().decode('gbk')
# 小說的信息
novel_info = {}
# 小說的名字
novel_info['title'] = re.findall(r'<div class="chapName">.*?<strong>(.*?)</strong>',html)[0]
print(novel_info['title'])
#print(html)
按照獲取思路,我們再試一下獲取小說作者:
思路完全一樣,這裏就不再贅述了,僅給出代碼供大家參考:
(提示:(.*?)中內容取自 <div class="chapName"><span class="r"> 和 </span>之間)
from urllib import request
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面
html = request.urlopen(first_url).read().decode('gbk')
# 小說的信息
novel_info = {}
# 作者名字
novel_info['author'] = re.findall(r'<div class="chapName"><span class="r">(.*?)</span>',html)[0]
print(novel_info['author'])
3.3 正則表達式在實例中的應用
前面介紹了這麼多,不要忘了我們最初的目的:爬取並下載整本小說的所有內容
分析HTML文件,我們發現,近七百章節的內容在<DIV> …… </DIV>中存放
於是乎,這裏採取逐步縮小查找區間的方法查找我們想要獲取的所有URL。
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面
html = request.urlopen(first_url).read().decode('gbk')
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html)
print(div_info)
運行結果爲空列表(說明這則表達式不對),那麼爲什麼不對呢?
事實上,前面已經提到過,因爲 . 只能匹配任意一個字符,因此爲我們還需要把缺的參數補上,即缺少 re.S 參數。
re.S 可以讓 . 匹配到任意字符(包括換行符)
於是代碼變成了這樣:
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面
html = request.urlopen(first_url).read().decode('gbk')
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S)
print(div_info)
細心的讀者會發現,兩個div大小寫是不一樣的;事實上,這樣得到的依舊是一個空列表。
這時我們就需要忽略大小寫,即在參數部分後面再加 re.I , 並用 | 隔開。
下面是再一次改進後的代碼:
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
# 獲取小說主頁面HTML
html = request.urlopen(first_url).read().decode('gbk')
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)
print(div_info)
我們進一步發現,返回的列表是不爲空了,但是得到的列表中還有好多沒用的標籤,
於是想要把它們剔除掉,即只保留其中的<a>……</a>標籤。
這時我們只需加一句代碼就可以實現,原理和前面正則表達式獲取字符串一樣,這裏給出核心部分代碼:
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[0]
tag_a = re.findall(r'<a.*?</a>',div_info)
這時輸出結果達到了我們的預期,即僅獲取所有(近700章節)的<a>……</a>標籤。
接下來要做的就是在得到的此列表中取出每一個<a>……</a>標籤,並返回對應章節名字及其url 。
首先獲得第一個<a>……</a>標籤中的章節url,這裏給出核心代碼:
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[0]
tag_a = re.findall(r'<a.*?</a>',div_info)
chapter_url = re.findall(r'href="(.*?)"',tag_a[0])[0]
print(chapter_url)
爲了顯示更清晰一點的話,我們還要獲得對應 url 的章節名字,這裏亦給出核心代碼:
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[0]
tag_a = re.findall(r'<a.*?</a>',div_info)
chapter_url = re.findall(r'href="(.*?)"',tag_a[0])[0]
chapter_title = re.findall(r'title="(.*?)"',tag_a[0])[0]
print(chapter_title)
print(chapter_url)
下面獲取近七百個章節的 url
使用for 循環遍歷核心代碼段即可(這裏循環650次獲取前六百五十章的所有章節名字及url)
from urllib import request
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
#訪問小說首頁面 對象.函數(參數).調用對象返回的方法
html = request.urlopen(first_url).read().decode('gbk')
# 小說的信息
novel_info = {}
# 小說的名字
novel_info['title'] = re.findall(r'<div class="chapName">.*?<strong>(.*?)</strong>',html)
# 作者名字
#novel_info['author'] = re.findall(r'<div class="chapName"><span class="r">(.*?)</span>',html)[0]
for i in range(650):
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[0]
tag_a = re.findall(r'<a.*?</a>',div_info)
chapter_url = re.findall(r'href="(.*?)"',tag_a[i])[0]
chapter_title = re.findall(r'title="(.*?)"',tag_a[i])[0]
print(chapter_title)
print(chapter_url)
再進一步,進入到每篇文章內部,同樣分析網址,可以得到該章節內所有文字
這裏以《引子》爲例,部分提取其中文字,這裏直接給出代碼
from urllib import request
import re
first_url = "http://www.quanshuwang.com/book/44/44683/15379609.html"
html = request.urlopen(first_url).read().decode('gbk')
for i in range(10):
div_info = re.findall(r'<div class="mainContenr" id="content">(.*?) <div style="margin:6px 1px 6px 1px;text-align:center;">', html, re.S | re.I)[0]
tag_a = re.findall(r'</script> (.*?)<script type="text/javascript">', div_info,re.S)
chapter_contant = re.findall(r'\r\n<br />\r\n  (.*?)<br />\r\n<br />',tag_a[0],re.S)[i]
print(chapter_contant)
就按照此思路一步一步分析,由於目標程序太多,運行要得到最終結果需要等較長時間,故這裏只給出最終代碼,最後結果即文章全部內容輸出到顯示頻上,讀者感興趣可親自嘗試。
from urllib import request
import re
first_url = "http://www.quanshuwang.com/book/44/44683"
html = request.urlopen(first_url).read().decode('gbk')
for i in range(650):
div_info = re.findall(r'<DIV class="clearfix dirconone">(.*?)</div>',html,re.S|re.I)[0]
tag_a = re.findall(r'<a.*?</a>',div_info)
chapter_url = re.findall(r'href="(.*?)"',tag_a[i])[0]
chapter_title = re.findall(r'title="(.*?)"',tag_a[i])[0]
for j in range(15):
content_html = request.urlopen(chapter_url).read().decode('gbk')
content_div_info = re.findall(r'<div class="mainContenr" id="content">(.*?) <div style="margin:6px 1px 6px 1px;text-align:center;">',content_html, re.S | re.I)[0]
contant_tag_a = re.findall(r'</script> (.*?)<script type="text/javascript">', content_div_info, re.S)
chapter_contant = re.findall(r'\r\n<br />\r\n  (.*?)<br />\r\n<br />', contant_tag_a[0], re.S)[j]
print(chapter_contant)
至此,正則表達式的入門已經講解完畢了,由於正則表達式元字符着實太多,僅僅這點篇幅是不可能全部介紹完的,所以這裏只介紹思想,感興趣讀者可自行查閱相關元字符,進一步操作。
這裏給出常見的部分元字符供大家參考:
3 學以致用
學習了正則表達式,並系統學習了正則表達式在應用中的實例。接下來,我們再舉一個例子對正則表達式做進一步的理解。
b站視頻選取的是5月7日的熱門視頻:我的NBA手辦真的會打球!!!
第一步,F12鍵找到彈幕對應的list標籤。
打開list標籤 Request URL,得到一條條的彈幕,這正是我們所謂的目標頁面。
第二步,獲取彈幕網URL,採用正則表達式的匹配模式,得到所有彈幕並輸出到指定文件夾的指定文件中。
import requests #發出請求
import re #內置庫 用於匹配正則表達式
import csv #文件格式
# 獲取到的彈幕網URL
url = 'https://api.bilibili.com/x/v1/dm/list.so?oid=186621670'
# 設置請求頭 僞裝瀏覽器
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
# 發起請求 獲得響應
response = requests.get(url,headers=headers)
html_doc = response.content.decode('utf-8')
# 正則表達式的匹配模式
res = re.compile('<d.*?>(.*?)</d>')
# 根據模式提取網頁數據
danmu = re.findall(res,html_doc)
print(danmu)
for i in danmu:
with open('F:/b站視頻彈幕.txt','a',newline='',encoding='utf-8-sig') as file:
writer = csv.writer(file)
danmu = []
danmu.append(i)
writer.writerow(danmu)
第三步,採用jieba庫分詞,並用wordcloud庫美化得到圖片文件。
import jieba #中文分詞
import wordcloud #繪製詞雲
f = open('F:/b站視頻彈幕.txt',encoding='utf-8') # 打開剛剛獲取到的所有彈幕包含在的txt文件
txt = f.read()
txt_list = jieba.lcut(txt)
string = ' '.join((txt_list))
print(string)
w = wordcloud.WordCloud(width=1000,
height=700,
background_color='white',
font_path='C:/Windows/SIMLI.TTF',
scale=15,
stopwords={' '},
contour_width=5,
contour_color='red'
)
w.generate(string)
w.to_file('bzhanwordcloud.png')
當然,我們還可以按照已有的圖片繪製出圖片內圖形樣式的詞雲圖:
比如通過下圖的藍球圖片,得到其對應樣式的詞雲圖。
下面給出實現上述操作的全部代碼,謹供參考:
import imageio as imageio #加載圖片
import requests #發出請求
import re #內置庫 用於匹配正則表達式
import csv #文件格式
import jieba #中文分詞
import wordcloud #繪製詞雲
# 目標網站(即我們獲取到的URL)
url = 'https://api.bilibili.com/x/v1/dm/list.so?oid=186803402'
# 設置請求頭 僞裝瀏覽器
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
# 發起請求 獲得響應
response = requests.get(url,headers=headers)
html_doc = response.content.decode('utf-8')
# 正則表達式的匹配模式
res = re.compile('<d.*?>(.*?)</d>')
# 根據模式提取網頁數據
danmu = re.findall(res,html_doc)
# 保存數據
for i in danmu:
with open('b站彈幕.csv','a',newline='',encoding='utf-8-sig') as file:
writer = csv.writer(file)
danmu = []
danmu.append(i)
writer.writerow(danmu)
# 顯示數據
f = open('F:/b站視頻彈幕.txt',encoding='utf-8')
txt = f.read()
txt_list = jieba.lcut(txt)
# print(txt_list)
string = ' '.join((txt_list))
print(string)
# 很據得到的彈幕數據繪製詞雲圖
mk = imageio.imread(r'F:/basketball.png')
w = wordcloud.WordCloud(width=1000,
height=700,
background_color='white',
font_path='C:/Windows/SIMLI.TTF',
mask=mk,
scale=15,
stopwords={' '},
contour_width=5,
contour_color='red'
)
w.generate(string)
w.to_file('gaijinwordcloud.png')
代碼運行最終效果展示:
這裏附錄一篇我寫的關於爬蟲的原創文章:Python爬蟲:10行代碼真正實現“可見即可爬”
以及在安裝python第三方庫方面還迷茫的小夥伴可以參考我的原創博文:一文教你安遍所有python第三方庫
大家也可以關注我原創的分類專欄:
①在王者榮耀角度下看程序設計模式(共25篇,已更新完)
②《數字圖像處理》學習筆記(更新中……)
③《機器學習》學習筆記(更新中……)
更多原創文章請點擊我的→主頁
★版權聲明:本文爲CSDN博主「IT_change」的原創文章,遵循CC 4.0 BY-SA版權協議。
轉載請附上原文出處鏈接及本聲明。
感謝閱讀 ! 感謝支持 ! 感謝關注 !
希望本文能對讀者學習正則表達式和使用爬蟲技術有所幫助,並請讀者批評指正!
2020年5月于山西大同
END