超貼心的,手把手教你寫爬蟲

耶low發表於2021-01-14

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

相關文章