在人生苦短我用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爬蟲開發》、各種文檔~