在人生苦短我用Python,本文助你快速入門這篇文章中,學習了Python的語法知識。現在我們就拿Python做個爬蟲玩玩,如果中途個別API忘了可以回頭看看,別看我,我沒忘!(逃
網路程式設計
學習網路爬蟲之前,有必要了解一下如何使用Python進行網路程式設計。既然說到網路程式設計,對於一些計算機網路的基礎知識最好也有所瞭解。比如HTTP,在這裡就不講計算機基礎了,貼出我之前的一篇部落格。感興趣的可以看看圖解HTTP常見知識點總結。
網路程式設計是Python比較擅長的領域,內建了相關的庫,第三方庫也很豐富。下面主要介紹一下內建的urllib庫和第三方的request庫。
urllib庫
urllib是Python內建的HTTP請求庫,其使得HTTP請求變得非常方便。首先通過一個表格列出這個庫的內建模組:
模組 | 作用 |
---|---|
urllib.request | HTTP請求模組,模擬瀏覽器傳送HTTP請求 |
urllib.error | 異常處理模組,捕獲由於HTTP請求產生的異常,並進行處理 |
urllib.parse | URL解析模組,提供了處理URL的工具函式 |
urllib.robotparser | robots.txt解析模組,網站通過robots.txt檔案設定爬蟲可爬取的網頁 |
下面會演示一些常用的函式和功能,開始之前先import上面的幾個模組。
urllib.request.urlopen函式
這個函式的作用是向目標URL傳送請求,其主要有三個引數:url目標地址、data請求資料、timeout超時時間。該函式會返回一個HTTPResponse物件,可以通過該物件獲取響應內容,示例如下:
response = urllib.request.urlopen("https://www.baidu.com/")
print(response.read().decode("utf8")) # read()是讀取響應內容。decode()是按指定方式解碼
可以看到我們使用這個函式只傳入了一個URL,沒傳入data的話預設是None,表示是GET請求。接著再演示一下POST請求:
param_dict = {"key":"hello"} # 先建立請求資料
param_str = urllib.parse.urlencode(param_dict) # 將字典資料轉換為字串,如 key=hello
param_data=bytes(param_str,encoding="utf8") # 把字串轉換成位元組物件(HTTP請求的data要求是bytes型別)
response = urllib.request.urlopen("http://httpbin.org/post",data=param_data) #這個網址專門測試HTTP請求的
print(response.read())
timeout就不再演示了,這個引數的單位是秒。怎麼請求弄明白了,關鍵是要解析響應資料。比如響應狀態碼可以這麼獲取:response.status
。獲取整個響應頭:response.getheaders()
,也可以獲取響應頭裡面某個欄位的資訊:response.getheader("Date")
,這個是獲取時間。
urllib.request.Request類
雖然可以使用urlopen函式非常方便的傳送簡單的HTTP請求,但是對於一些複雜的請求操作,就無能為力了。這時候可以通過Request物件來構建更豐富的請求資訊。這個類的構造方法有如下引數:
引數名詞 | 是否必需 | 作用 |
---|---|---|
url | 是 | HTTP請求的目標URL |
data | 否 | 請求資料,資料型別是bytes |
headers | 否 | 頭資訊,可以用字典來構建 |
origin_req_host | 否 | 發起請求的主機名或IP |
unverifiable | 否 | 請求是否為無法驗證的,預設為False。 |
method | 否 | 請求方式,如GET、POST等 |
url = "http://httpbin.org/get"
method = "GET"
# ...其他引數也可以自己構建
request_obj = urllib.request.Request(url=url,method=method) # 把引數傳入Request的構造方法
response = urllib.request.urlopen(request_obj)
print(response.read())
urllib.error異常處理模組
該模組中定義了兩個常見的異常:URLEEror和HTTPError,後者是前者的子類。示例如下:
url = "https://afasdwad.com/" # 訪問一個不存在的網站
try:
request_obj = urllib.request.Request(url=url)
response = urllib.request.urlopen(request_obj)
except urllib.error.URLError as e:
print(e.reason) # reason屬性記錄著URLError的原因
產生URLError的原因有兩種:1.網路異常,失去網路連線。2.伺服器連線失敗。而產生HTTPError的原因是:返回的Response urlopen函式不能處理。可以通過HTTPError內建的屬性瞭解異常原因,屬性有:reason記錄異常資訊、code記錄響應碼、headers記錄請求頭資訊。
requests庫
requests庫是基於urllib開發的HTTP相關的操作庫,相比urllib更加簡潔、易用。不過requests庫是第三方庫,需要單獨安裝才能使用,可以通過這個命令安裝:pip3 install requests
。
使用urllib中的urlopen時,我們傳入data代表POST請求,不傳入data代表GET請求。而在requests中有專門的函式對應GET還是POST。這些請求會返回一個requests.models.Response
型別的響應資料,示例如下:
import requests
response = requests.get("http://www.baidu.com")
print(type(response)) #輸出 <class 'requests.models.Response'>
print(response.status_code) # 獲取響應碼
print(response.text) # 列印響應內容
上面的例子呼叫的是get函式,通常可以傳入兩個引數,第一個是URL,第二個是請求引數params。GET請求的引數除了直接加在URL後面,還可以使用一個字典記錄著,然後傳給params。對於其他的請求方法,POST請求也有個post函式、PUT請求有put函式等等。
返回的Response物件,除了可以獲取響應碼,它還有以下這些屬性:
- content:二進位制資料,如圖片視訊等
- url:請求的url
- encoding:響應資訊的編碼格式
- cookies:cookie資訊
- headers:響應頭資訊
其他的函式就不一一演示,等需要用到的時候大家可以查文件,也可以直接看原始碼。比如post函式原始碼的引數列表是這樣的:def post(url, data=None, json=None, **kwargs):
。直接看原始碼就知道了它需要哪些引數,引數名是啥,一目瞭然。不過接觸Python後,有個非常不好的體驗:雖然寫起來比其他傳統面嚮物件語言方便很多,但是看別人的原始碼時不知道引數型別是啥。不過一般寫的比較好的原始碼都會有註釋,比如post函式開頭就會說明data是字典型別的。
urllib庫中可以用Request類的headers引數來構建頭資訊。那麼最後我們再來說一下requests庫中怎麼構建headers頭資訊,這在爬蟲中尤為重要,因為頭資訊可以把我們偽裝成瀏覽器。
我們直接使用字典把頭資訊裡面對應的欄位都填寫完畢,再呼叫對應的get或post函式時,加上headers=dict就行了。**kwargs
就是接收這些引數的。
網路程式設計相關的API暫時就講這些,下面就拿小說網站和京東為例,爬取上面的資訊來練練手。
用爬蟲下載小說
在正式寫程式之前有必要說說爬蟲相關的基礎知識。不知道有多少人和我一樣,瞭解爬蟲之前覺得它是個高大上、高度智慧的程式。實際上,爬蟲能做的我們人類也能做,只是效率非常低。其爬取資訊的邏輯也很樸實無華:通過HTTP請求訪問網站,然後利用正規表示式匹配我們需要的資訊,最後對爬取的資訊進行整理。雖然過程千差萬別,但是大體的步驟就是這樣。其中還涉及了各大網站反爬蟲和爬蟲高手們的反反爬蟲。
再者就是,具體網站具體分析,所以除了必要的後端知識,學習爬蟲的基本前提就是起碼看得懂HTML和會用瀏覽器的除錯功能。不過這些就多說了,相信各位大手子都懂。
第一個實戰我們就挑選一個簡單點的小說網站:https://www.kanunu8.com/book3/6879/。 先看一下頁面:
我們要做的就是把每個章節的內容都爬取下來,並以每個章節為一個檔案,儲存到本地資料夾中。
我們首先要獲取每個章節的連結。按F12開啟調式頁面,我們通過HTML程式碼分析一下,如何才能獲取這些章節目錄?當然,如何找到章節目錄沒有嚴格限制,只要你寫的正規表示式能滿足這個要求即可。我這裡就從正文這兩個字入手,因為章節表格這個元素最開頭的是這兩字。我們來看一下原始碼:
我們要做的就是,寫一個正規表示式,從正文二字開頭,以</tbody>
結尾,獲取圖中紅色大括號括起來的這段HTML程式碼。獲取到章節目錄所在的程式碼後,我們再通過a
標籤獲取每個章節的連結。注意:這個連結是相對路徑,我們需要和網站URL進行拼接。
有了大概的思路後,我們開始敲程式碼吧。程式碼並不複雜,我就全部貼出來,主要邏輯我就寫在註釋中,就不在正文中說明了。如果忘了正規表示式就去上一篇文章裡回顧一下吧。
import requests
import re
import os
"""
傳入網站的html字串
利用正規表示式來解析出章節連結
"""
def get_toc(html,start_url):
toc_url_list=[]
# 獲取目錄(re.S代表把/n也當作一個普通的字元,而不是換行符。不然換行後有的內容會被分割,導致表示式匹配不上)
toc_block=re.findall(r"正文(.*?)</tbody>",html,re.S)[0]
# 獲取章節連結
# 囉嗦一句,Python中單引號和雙引號都可以表示字串,但是如果有多重引號時,建議區分開來,方便檢視
toc_url = re.findall(r'href="(.*?)"',toc_block,re.S)
for url in toc_url:
# 因為章節連結是相對路徑,所以得和網址進行拼接
toc_url_list.append(start_url+url)
return toc_url_list
"""
獲取每一章節的內容
"""
def get_article(toc_url_list):
html_list=[]
for url in toc_url_list:
html_str = requests.get(url).content.decode("GBK")
html_list.append(html_str)
# 先建立個資料夾,文章就儲存到這裡面,exist_ok=True代表不存在就建立
os.makedirs("動物莊園",exist_ok=True)
for html in html_list:
# 獲取章節名稱(只有章節名的size=4,我們根據這個特點匹配),group(1)表示返回第一個匹配到的子字串
chapter_name = re.search(r'size="4">(.*?)<',html,re.S).group(1)
# 獲取文章內容(全文被p標籤包裹),並且把<br />給替換掉,注意/前有個空格
text_block = re.search(r'<p>(.*?)</p>',html,re.S).group(1).replace("<br />","")
save(chapter_name,text_block)
"""
儲存文章
"""
def save(chapter_name,text_block):
# 以寫的方式開啟指定檔案
with open(os.path.join("動物莊園",chapter_name+".txt"),"w",encoding="utf-8") as f:
f.write(text_block)
# 開始
def main():
try:
start_url = "https://www.kanunu8.com/book3/6879/"
# 獲取小說主頁的html(decode預設是utf8,但是這個網站的編碼方式是GBK)
html = requests.get(start_url).content.decode("GBK")
# 獲取每個章節的連結
toc_url_list = get_toc(html,start_url)
# 根據章節連結獲取文章內容並儲存
get_article(toc_url_list)
except Exception as e:
print("發生異常:",e)
if __name__ == "__main__":
main()
最後看一下效果:
擴充:一個簡單的爬蟲就寫完了,但是還有很多可以擴充的地方。比如:改成多執行緒爬蟲,提升效率,這個小專案很符合多執行緒爬蟲的使用場景,典型的IO密集型任務。還可以優化一下入口,我們通過main方法傳入書名,再去網站查詢對應的書籍進行下載。
我以多執行緒爬取為例,我們只需要稍微修改兩個方法:
# 首先匯入執行緒池
from concurrent.futures import ThreadPoolExecutor
# 我們把main方法修改一下
def main():
try:
start_url = "https://www.kanunu8.com/book3/6879/"
html = requests.get(start_url).content.decode("GBK")
toc_url_list = get_toc(html,start_url)
os.makedirs("動物莊園",exist_ok=True)
# 建立一個有4個執行緒的執行緒池
with ThreadPoolExecutor(max_workers=4) as pool:
pool.map(get_article,toc_url_list)
except Exception as e:
print("發生異常:",e)
map()
方法中,第一個引數是待執行的方法名,不用加()。第二個引數是傳入到get_article
這個方法的引數,可以是列表、元組等。以本程式碼為例,map()
方法的作用就是:會讓執行緒池中的執行緒去執行get_article
,並傳入引數,這個引數就從toc_url_list
依次獲取。比如執行緒A拿了``toc_url_list`的第一個元素並傳入,那麼執行緒B就拿第二個元素並傳入。
既然我們知道了map()
方法傳入的是一個元素,而get_article
原來接收的是一個列表,所以這個方法也需要稍微修改一下:
def get_article(url):
html_str = requests.get(url).content.decode("GBK")
chapter_name = re.search(r'size="4">(.*?)<',html_str,re.S).group(1)
text_block = re.search(r'<p>(.*?)</p>',html_str,re.S).group(1).replace("<br />","")
save(chapter_name,text_block)
通過測試,在我的機器上,使用一個執行緒爬取這本小說花了24.9秒,使用4個執行緒花了4.6秒。當然我只測試了一次,應該有網路的原因,時間不是非常準確,但效果還是很明顯的。
爬取京東商品資訊
有了第一個專案練手,是不是有點感覺呢?其實也沒想象的那麼複雜。下面我們再拿京東試一試,我想達到的目的是:收集京東上某個商品的資訊,並儲存到Excel表格中。這個專案中涉及了一些第三方庫,不過大家可以先看我的註釋,過後再去看它們的文件。
具體問題具體分析,在貼爬蟲程式碼之前我們先分析一下京東的網頁原始碼,看看怎麼設計爬蟲的邏輯比較好。
我們先在京東商城的搜尋框裡輸入你想收集的商品,然後開啟瀏覽器的調式功能,進入到Network,最後再點選搜尋按鈕。我們找一下搜尋商品的介面連結是啥。
圖中選中的網路請求就是搜尋按鈕對應的介面連結。拿到這個連結後我們就可以拼接URL,請求獲取商品資訊了。我們接著看商品搜尋出來後,是怎麼呈現的。
通過原始碼發現,每個商品對應一個li標籤。一般商城網站都是由一些模板動態生成的,所以看上去很規整,這讓我們的爬取難度也降低了。
我們點進一個看看每個商品裡又包含什麼資訊:
同樣相當規整,最外層li的class叫gl-item,裡面每個div對應一個商品資訊。知道這些後,做起來就相當簡單了,就用這些class的名稱來爬取資訊。我還是直接貼出全部程式碼,該說的都寫在註釋裡。貼之前說說每個方法的作用。search_by_keyword
:根據傳入的商品關鍵詞搜尋商品。get_item_info
:根據網頁原始碼獲取商品資訊。skip_page
:跳轉到下一頁並獲取商品資訊。save_excel
:把獲取的資訊儲存到Excel。
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from pyquery import PyQuery
from urllib.parse import quote
import re
from openpyxl import Workbook
from fake_useragent import UserAgent
# 設定請求頭裡的裝置資訊,不然會被京東攔截
dcap = dict(DesiredCapabilities.PHANTOMJS)
# 使用隨機裝置資訊
dcap["phantomjs.page.settings.userAgent"] = (UserAgent().random)
# 構建瀏覽器物件
browser = webdriver.PhantomJS(desired_capabilities=dcap)
# 傳送搜尋商品的請求,並返回總頁數
def search_by_keyword(keyword):
print("正在搜尋:{}".format(keyword))
try:
# 把關鍵詞填入搜尋連結
url = "https://search.jd.com/Search?keyword=" + \
quote(keyword)+"&enc=utf-8"
# 通過瀏覽器物件傳送GET請求
browser.get(url)
# 等待請求響應
WebDriverWait(browser, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".gl-item"))
)
pages = WebDriverWait(browser, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > em:nth-child(1) > b"))
)
return int(pages.text)
except TimeoutException as e:
print("請求超時:"+e)
# 根據HTML獲取對應的商品資訊
def get_item_info(page):
# 獲取網頁原始碼
html = browser.page_source
# 使用 PyQuery解析網頁原始碼
pq = PyQuery(html)
# 獲取商品的li標籤
items = pq(".gl-item").items()
datas = []
# Excel中的表頭,如果當前是第一頁資訊就是新增表頭
if page==1:
head = ["商品名稱", "商品連結", "商品價格", "商品評價", "店鋪名稱", "商品標籤"]
datas.append(head)
# 遍歷當前頁所有的商品資訊
for item in items:
# 商品名稱,使用正規表示式將商品名稱中的換行符\n替換掉
p_name = re.sub("\\n", "", item.find(".p-name em").text())
href = item.find(".p-name a").attr("href") # 商品連結
p_price = item.find(".p-price").text() # 商品價錢
p_commit = item.find(".p-commit").text() # 商品評價
p_shop = item.find(".p-shop").text() # 店鋪名稱
p_icons = item.find(".p-icons").text()
# info代表某個商品的資訊
info = []
info.append(p_name)
info.append(href)
info.append(p_price)
info.append(p_commit)
info.append(p_shop)
info.append(p_icons)
print(info)
# datas是當前頁所有商品的資訊
datas.append(info)
return datas
# 跳轉到下一頁並獲取資料
def skip_page(page, ws):
print("跳轉到第{}頁".format(page))
try:
# 獲取跳轉到第幾頁的輸入框
input_text = WebDriverWait(browser, 10).until(
EC.presence_of_element_located(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > input"))
)
# 獲取跳轉到第幾頁的確定按鈕
submit = WebDriverWait(browser, 10).until(
EC.element_to_be_clickable(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > a"))
)
input_text.clear() # 清空輸入框
input_text.send_keys(page) # 在輸入框中填入要跳轉的頁碼
submit.click() # 點選確定按鈕
# 等待網頁載入完成,直到頁面下方被選中並且高亮顯示的頁碼,與頁碼輸入框中的頁碼相等
WebDriverWait(browser, 10).until(
EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, "#J_bottomPage > span.p-num > a.curr"), str(page))
)
# 獲取商品資訊
datas = get_item_info(page)
# 如果有資料就儲存到Excel中
if len(datas) > 0:
save_excel(datas, ws)
except TimeoutException as e:
print("請求超時:", e)
skip_page(page, ws) # 請求超時,重試
except Exception as e:
print("產生異常:", e)
print("行數:", e.__traceback__.tb_lineno)
# 儲存資料到Excel中
def save_excel(datas, ws):
for data in datas:
ws.append(data)
def main():
try:
keyword = "手機" # 搜尋關鍵詞
file_path = "./data.xlsx" # 檔案儲存路徑
# 建立一個工作簿
wb = Workbook()
ws = wb.create_sheet("京東手機商品資訊",0)
pages = search_by_keyword(keyword)
print("搜尋結果共{}頁".format(pages))
# 按照順序迴圈跳轉到下一頁(就不爬取所有的資料了,不然要等很久,如果需要爬取所有就把5改成pages+1)
for page in range(1, 5):
skip_page(page, ws)
# 儲存Excel表格
wb.save(file_path)
except Exception as err:
print("產生異常:", err)
wb.save(file_path)
finally:
browser.close()
if __name__ == '__main__':
main()
從main方法開始,藉助著註釋,即使不知道這些庫應該也能看懂了。下面是使用到的操作庫的說明文件:
selenium:Selenium庫是第三方Python庫,是一個Web自動化測試工具,它能夠驅動瀏覽器模擬輸入、單擊、下拉等瀏覽器操作。中文文件:https://selenium-python-zh.readthedocs.io/en/latest/index.html。部分內容還沒翻譯完,也可以看看這個:https://zhuanlan.zhihu.com/p/111859925。selenium建議安裝低一點的版本,比如
pip3 install selenium==2.48.0
,預設安裝的新版本不支援PhantomJS了。PhantomJS:是一個可程式設計的無介面瀏覽器引擎,也可以使用谷歌或者火狐的。這個不屬於Python的庫,所以不能通過pip3直接安裝,去找個網址http://phantomjs.org/download.html下載安裝包,解壓後,把所在路徑新增到環境變數中(新增的路徑要到bin目錄中)。文件:https://phantomjs.org/quick-start.html
openpyxl:Excel操作庫,可直接安裝,文件:https://openpyxl.readthedocs.io/en/stable/。
pyquery:網頁解析庫,可直接安裝,文件:https://pythonhosted.org/pyquery/
擴充:可以加上商品的選擇條件,比如價格範圍、銷量排行。也可以進入到詳情頁面,爬取銷量排行前幾的評價等。
今天就說到這裡了,有問題感謝指出。如果有幫助可以點個贊、點個關注。接下來會學更多爬蟲技巧以及其他的後端知識,到時候再分享給大家~
參考資料:《Python 3快速入門與實戰》、《Python爬蟲開發》、各種文件~