爬蟲入門到放棄系列05:從程式模組設計到代理IP池

前言

上篇文章吧啦吧啦講了一些有的沒的,現在還是回到主題寫點技術相關的。本篇文章作為基礎爬蟲知識的最後一篇,將以爬蟲程式的模組設計來完結。

在我漫(liang)長(nian)的爬蟲開發生涯中,我通常將爬蟲程式分為四大模組。

程式模組設計

如圖,除了代理模組是根據所需引入程式,請求、解析、儲存模組是必不可少的。

代理模組

代理模組主要是構建代理IP池。在第三篇中講過為什麼需要代理IP,因為很多網站是通過請求頻率來識別爬蟲,即記錄一個IP在一段時間內的請求次數,所以可以通過更換代理IP來提高爬取效率。

概念

什麼是代理IP池?

和執行緒池、連接池的理念一樣,預先將多個代理IP放入一個公共區域供多個爬蟲使用,每次用完之後再放回。

為什麼需要代理池?

正常情況下,我們在程式中是這樣添加代理IP的。

proxies = {
    'https': '//183.220.xxx.xx:80'
}
response = requests.get(url, proxies=proxies)

這樣我們就只能使用一個IP,這時候可能有人就會說:

就算使用集合可以存放多個代理IP,但是如果IP失效需要刪除,或者添加新的IP時,還是一樣需要終止程式修改程式碼。我在初學編程的時候,老師就經常說這麼一句話:

直到現在,這句話也時常在耳邊縈繞。而代理模組就是提供了靈活增刪代理IP、驗證IP有效性的功能。

實現

目前,一般使用MySQL來存放代理IP。先看一下代理池的表設計。

CREATE TABLE `proxy` (
  `ip` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我的表結構設計比較簡單粗暴,只有一個欄位。在開發中可以根據自己的需要來進行細分。

再看一下表裡面的數據:

從圖中看著,代理IP是由支援的協議 + IP + port組成。

最後,代理池程式碼奉上:

import requests
import pymysql

class proxyPool:
    # 初始化數據連接
    def __init__(self, host, db, user, password, port):
        self.conn = pymysql.connect(host=host,
                                    database=db,
                                    user=user,
                                    password=password,
                                    port=port,
                                    charset='utf8')
    # 從資料庫獲取ip
    def get_ip(self):
        cursor = self.conn.cursor()
        cursor.execute('select ip from proxy order by rand() limit 1')
        ip = cursor.fetchone()
        # 如果代理ip表中有數據,則進行判斷,沒有則返回空
        if ip:
            judge = self.judge_ip(ip[0])
            # 如果ip可用直接將其放回,不可用再重新調用此方法從資料庫獲取一個IP
            if judge:
                return ip[0]
            else:
                self.get_ip()
        else:
            return ''

    # 判斷ip是否可用
    def judge_ip(self, ip):
        http_url = '//www.baidu.com'
        try:
            proxy_dict = {
                "http": ip,
            }
            response = requests.get(http_url, proxies=proxy_dict)
        except Exception:
            self.delete_ip(ip)
            return False
        else:
            code = response.status_code
            if code in (200, 299):
                return True
            else:
                self.delete_ip(ip)
                return False

    # 從資料庫中刪除無效的ip
    def delete_ip(self, ip):
        delete_sql = f"delete from proxy where ip='{ip}'"
        cursor = self.conn.cursor()
        cursor.execute(delete_sql)
        self.conn.commit()

代理池工作流程主要分為兩部分:

  1. 從數據中獲取IP。如果資料庫沒有可用IP,則表示不使用代理,返回空;如果有IP,則進入下一步
  2. 對IP進行有效性驗證。如果IP無效,刪除IP並重複第一步;如果IP有效,則返回IP

使用

代理池最終的目的還是提供有效代理IP。玩的比較花的可以將代理池與爬蟲程式分離,將代理池獨立成一個web介面,通過url來獲取代理IP,需要使用Flask或者Django來搭建一個web服務。

我一般就是直接放在爬蟲程式中。樣例程式碼如下:

pool = proxyPool('47.102.xxx.xxx', 'test', 'root', 'root', 3306)
proxy_ip = pool.get_ip()
url = '//v.qq.com/detail/m/m441e3rjq9kwpsc.html'
proxies = {
    'http': proxy_ip
}
if proxy_ip:
    response = requests.get(url, proxies=proxies)
else:
    response = requests.get(url)
print(response.text)

代理IP的來源

這個之前也講過,代理IP可以付費購買或者從網上使用免費的。因為免費IP存活率低,所以代理池主要是面向於免費IP。

一般都是單獨開發一個爬蟲程式來爬取免費的IP,並放入到資料庫中,然後驗證可用性。

請求/解析模組

在前幾篇寫的爬蟲樣例中,都是對單個url進行的爬取。而爬蟲程式往往都是以網站為單位進行的爬取。歸根結底,都是基於請求模組和解析模組來設計實現的。

如果想爬取整個網站,首先必須確定一個網站入口,即爬蟲程式第一個訪問的url。然後接著對返回的網頁進行解析,獲取數據或者獲取下一層url繼續請求。

這裡就拿騰訊影片舉個栗子,我們來**爬取動漫的資訊*。

1. 選擇網站入口

分析需求,選取網站入口。此時,需要明確的是:動漫頻道url就是網站入口

動漫首頁

我們對網站入口,即動漫頻道進行請求後,解析返回的網頁內容。我們從頁面中可以發現,動漫頻道下有國漫、日漫、戰鬥等分類。

查看網頁源碼:

分類URL

如上圖,我們可以從動漫首頁解析出來各個分類的url

2.分類請求

在獲取到各個分類的url之後,繼續發起請求。這裡首先對國漫的url進行請求,返回的網頁內容如下:

國漫

如圖,都是國漫分類下的動漫列表。在瀏覽器中,我們點擊哪個動漫就能進入它的播放頁,所以在這個頁面上我們可以解析到這些國漫的播放頁鏈接

我們查看此頁面的網頁源碼:

如圖,我們可以獲取到各個國漫播放頁的url

3.定向到資訊頁

以第一個國漫斗羅大陸為例,我們獲取到它的播放頁url,進行請求並返回播放頁內容。

播放頁

我們發現,點擊右上角的斗羅大陸就會進入詳情頁。所以我們需要解析右上角詳情頁的url進行請求,來獲取詳情頁的網頁內容。

詳情頁

4.獲取數據

對詳情頁的網頁內容進行解析,得出自己想要的數據,具體程式碼在第一篇文章的樣例中。

從上面的四個步驟來看,爬蟲對網站的爬取就是層層遞進,逐級訪問。我們要找准網站入口,明確想要獲取的數據內容,規劃好網站入口到獲取數據的路徑

當然其中還是有很多可以優化的地方,例如從第二步可以略過第三步,直接請求第四步的詳情頁。我們比對一下播放頁和詳情頁的url。

# 斗羅大陸的播放頁和詳情頁
//v.qq.com/x/cover/m441e3rjq9kwpsc.html
//v.qq.com/detail/m/m441e3rjq9kwpsc.html
# 狐妖小紅娘的播放頁和詳情頁
//v.qq.com/x/cover/0sdnyl7h86atoyt.html
//v.qq.com/detail/0/0sdnyl7h86atoyt.html

從上面兩對url中我們不難看出其中的規律。所以我們在第二步解析出國漫播放頁的url之後,經過處理,就可以直接得到詳情頁的url。

備註:上面對騰訊影片的爬取分析僅做流程參考,實際開發可能涉及非同步請求等方面的知識。

存儲模組

爬取的數據只有存儲下來,爬蟲才變得更有意義。

通常爬取數據格式有文本、圖片等,這裡先看圖片如何下載並保存到本地目錄。

圖片下載

之前我用scrapy內置的ImagesPipeline下載GIF動圖的時候,折騰了老半天,下載下來的還不是動圖。於是回歸原始,終成功,一行程式碼慰平生。

程式碼如下:

urllib.request.urlretrieve(imageUrl, filename)

所以,不論以後你用原生爬蟲還是scrapy的時候,下載圖片就記住一行程式碼就行了!

找個圖片鏈接測試一下:

右鍵圖片,選擇拷貝影像地址。

將圖片地址放入程式中,程式碼如下:

import urllib.request
urllib.request.urlretrieve('//puui.qpic.cn/vcover_vt_pic/0/m441e3rjq9kwpsc1607693898908/0', './1.jpg')

我將圖片下來,保存到當前目錄並命名為1.jpg,運行程式。

文本數據

  1. 存放於文件中
with open("/path/file.txt", 'a', encoding='utf-8') as f:
  f.write(data + '\n')
  1. 使用pymsql模組將數據存放到MySQL的數據表中

  2. 使用pandas或者xlwt模組將數據存放到excel中

結語

本篇文章主要寫了一下自己對爬蟲程式模組設計的理解,也是對爬蟲基礎知識的一個總結和收尾。期待下一次相遇。


寫的都是日常工作中的親身實踐,置身自己的角度從0寫到1,保證能夠真正讓大家看懂。

文章會在公眾號 [入門到放棄之路] 首發,期待你的關注。

感謝每一份關注