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

人生苦短我用Python,本文助你快速入門這篇文章中,學習了Python的語法知識。如今咱們就拿Python作個爬蟲玩玩,若是中途個別API忘了能夠回頭看看,別看我,我沒忘!(逃html

網絡編程

​ 學習網絡爬蟲以前,有必要了解一下如何使用Python進行網絡編程。既然說到網絡編程,對於一些計算機網絡的基礎知識最好也有所瞭解。好比HTTP,在這裏就不講計算機基礎了,貼出我以前的一篇博客。感興趣的能夠看看圖解HTTP常見知識點總結python

​ 網絡編程是Python比較擅長的領域,內置了相關的庫,第三方庫也很豐富。下面主要介紹一下內置的urllib庫和第三方的request庫。web

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對象,能夠經過該對象獲取響應內容,示例以下:json

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,後者是前者的子類。示例以下:cookie

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爬蟲開發》、各類文檔~

相關文章
相關標籤/搜索