Python Scrapy 爬蟲(二):scrapy 初試

雨林君發表於2018-08-13

接上篇,之前我們搭建好了執行環境,相當於我們搭好了炮臺,現在就差獵物和武器了。

一、選取獵物

此處選擇爬取西刺代理 IP 作為示例專案,原因有如下兩點:

  • 西刺代理資料規範,爬取簡單,作為演示專案比較合適
  • 代理 IP 在我們的爬蟲中也許還能派上用場(雖然可用率低了點,但如果你不是走量的,平時自己用一下還是不錯的)

獵物 URL

http://www.xicidaili.com/nn
複製程式碼

注:雖然西刺聲稱提供了全網唯一的免費代理 IP 介面,但似乎並沒有什麼用,因為根本不返回資料...我們自己做點小工作還是可以的。

二、我們的目標是

scrapy 初試計劃實現的效果是:

  • 從西刺代理網站上爬取免費的國內高匿代理 IP
  • 將爬取的代理 IP存入 MySQL 資料庫中
  • 通過迴圈爬取的方式獲取最新 IP,並通過設立資料庫唯一鍵的方式進行簡易版去重

三、目標分析

  正所謂知己知彼,至於勝多勝少,先不糾結。我們先開啟網站(使用 Chrome 開啟),看見的大概是下面的這個東西。

西刺代理

網頁結構分析

  • 大致瀏覽我們的目標網站,選取我們需要的資料。從網頁上我們可以看到西刺代理國內高匿 IP 展示了國家、IP、埠、伺服器地址、是否匿名、型別、速度、連線時間、存活時間、驗證時間這些資訊。
  • 在網頁的資料展示區的欄位名稱(藍色)區域點選右鍵 -> 檢查,我們發現該網頁的資料是由
    進行渲染布局的
  • 把網頁拖動到底部,發現網站的資料進行分佈展示,我們點選下一頁翻頁一觀察就發現很有規律的是頁碼引數就在 URL 的後面,當前第幾頁就傳數字幾,如:
http://www.xicidaili.com/nn/3
複製程式碼
  • 我們看到國家是顯示的國旗,速度與連線時間是顯示的兩個顏色塊,似乎不太好拿這三個資訊?

  此時,我們將滑鼠移至網頁中某一面小國旗的位置處點選右鍵 -> 檢查,我們發現這是一個 img 標籤,其中 alt 屬性有國家程式碼。明瞭了吧,這個國家資訊我們能拿到。

  我們再把滑鼠移至速度的色塊處點選右鍵 -> 檢查,我們可以發現有個 div 上有個 title 顯示類似 0.876秒這種資料,而連線時間也是這個套路,於是基本確定我們的資料項都能拿到,且能通過翻頁拿取更多 IP。

四、準備武器彈藥

4.1 準備武器

我們的武器當然是 scrapy

4.2 準備彈藥

彈藥包括

  • pymsql (Python 操作 MySQL 的庫)
  • fake-useragent (隱藏你的身份)
  • pywin32

五、開火

5.1 建立虛擬環境

C:\Users\jiang>workon

Pass a name to activate one of the following virtualenvs:
==============================================================================
test
C:\Users\jiang>mkvirtualenv proxy_ip
Using base prefix 'd:\\program files\\python36'
New python executable in D:\ProgramData\workspace\python\env\proxy_ip\Scripts\python.exe
Installing setuptools, pip, wheel...done.
複製程式碼

5.2 安裝第三方庫

注意:下文的命令中,只要命令前方有 "(proxy_ip)" 標識,即表示該命令是在上面建立的 proxy_ip 的 python 虛擬環境下執行的。

1 安裝 scrapy

(proxy_ip) C:\Users\jiang>pip install scrapy -i https://pypi.douban.com/simple
複製程式碼

如果安裝 scrapy 時出現如下錯誤:

error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
複製程式碼

可使用如下方式解決:

  • 手動下載 twisted 的 whl 包,下載地址如下
https://www.lfd.uci.edu/~gohlke/pythonlibs/
複製程式碼

開啟上方的 URL,搜尋 Twisted,下載最新的符合 python 版本與 windows 版本的 whl 檔案,如

twisted

如上圖所示,Twisted‑18.4.0‑cp36‑cp36m‑win_amd64.whl 表示匹配 Python 3.6 的 Win 64 檔案。這是與我環境匹配的。因此,下載該檔案,然後找到檔案下載的位置,把其拖動到 CMD 命令列視窗進行安裝,如下示例:

(proxy_ip) C:\Users\jiang>pip install D:\ProgramData\Download\Twisted-18.4.0-cp36-cp36m-win_amd64.whl
複製程式碼

離線安裝 twisted

再執行如下命令安裝 scrapy 即可成功安裝

(proxy_ip) C:\Users\jiang>pip install scrapy -i https://pypi.douban.com/simple
複製程式碼

2 安裝 pymysql

(proxy_ip) C:\Users\jiang>pip install pymysql -i https://pypi.douban.com/simple
複製程式碼

3 安裝 fake-useragent

(proxy_ip) C:\Users\jiang>pip install fake-useragent -i https://pypi.douban.com/simple
複製程式碼

4 安裝 pywin32

(proxy_ip) C:\Users\jiang>pip install pywin32 -i https://pypi.douban.com/simple
複製程式碼

5.3 建立 scrapy 專案

1 建立一個工作目錄(可選)

我們可以建立一個專門的目錄用於存放 python 的專案檔案,例如:

我在使用者目錄下建立了一個 python_projects,也可以建立任何名稱的目錄或者選用一個自己知道位置的目錄。

(proxy_ip) C:\Users\jiang>mkdir python_projects
複製程式碼

2 建立 scrapy 專案

進入工作目錄,執行命令建立一個 scrapy 的專案

(proxy_ip) C:\Users\jiang>cd python_projects

(proxy_ip) C:\Users\jiang\python_projects>scrapy startproject proxy_ip
New Scrapy project 'proxy_ip', using template directory 'd:\\programdata\\workspace\\python\\env\\proxy_ip\\lib\\site-packages\\scrapy\\templates\\project', created in:
    C:\Users\jiang\python_projects\proxy_ip

You can start your first spider with:
    cd proxy_ip
    scrapy genspider example example.com
複製程式碼

至此,已經完成了建立一個 scrapy 專案的工作。接下來,開始我們的狩獵計劃吧...

5.4 關鍵配置編碼

1 開啟專案

使用 PyCharm 開啟我們剛剛建立好的 scrapy 專案,點選 "Open in new window" 開啟專案

開啟專案

scrapy 專案初始結構

2 配置專案環境

File -> Settings -> Project: proxy_ip -> Project Interpreter -> 齒輪按鈕 -> add ...

設定專案環境

選擇 "Existing environment" -> "..." 按鈕

選擇虛擬環境位置

找到之前建立的 proxy_ip 的虛擬環境的 Scripts/python.exe,選中並確定

選擇 proxy_ip 虛擬環境的 python.exe

虛擬環境 proxy_ip 的位置預設位於 C:\Users\username\envs,此處我的虛擬機器位置已經通過修改 WORK_ON 環境變數更改。

3 配置 items.py

items 中定義了我們爬取的欄位以及對各欄位的處理,可以簡單地類似理解為這是一個 Excel 的模板,我們定義了模組的表頭欄位及欄位的屬性等等,然後我們按照這個模組往表格裡填數。

class ProxyIpItem(scrapy.Item):
    country = scrapy.Field()
    ip = scrapy.Field()
    port = scrapy.Field( )
    server_location = scrapy.Field()
    is_anonymous = scrapy.Field()
    protocol_type = scrapy.Field()
    speed = scrapy.Field()
    connect_time = scrapy.Field()
    survival_time = scrapy.Field()
    validate_time = scrapy.Field()
    source = scrapy.Field()
    create_time = scrapy.Field()

    def get_insert_sql(self):
        insert_sql = """
            insert into proxy_ip(
                country, ip, port, server_location,
                is_anonymous, protocol_type, speed, connect_time,
                survival_time, validate_time, source, create_time
                )
            VALUES (%s, %s, %s, %s, %s,  %s, %s, %s, %s, %s,  %s, %s)
            """

        params = (
                    self["country"], self["ip"], self["port"], self["server_location"],
                    self["is_anonymous"], self["protocol_type"], self["speed"], self["speed"],
                    self["survival_time"], self["validate_time"], self["source"], self["create_time"]
                  )
        return insert_sql, params
複製程式碼

4 編寫 pipelines.py

  pipeline 直譯為管道,而在 scrapy 中的 pipelines 的功能與管道也非常相似,我們可以在 piplelines.py 中定義多個管道。你可以這麼來理解,比如:有一座水庫,我們要從這個水庫來取水到不同的地方,比如自來水廠、工廠、農田...

  於是我們建了三條管道1、2、3,分別連線到自來水廠,工廠、農田。當管道建立好之後,只要我們需要水的時候,開啟開關(閥門),水庫裡的水就能源源不斷的流向不同的目的地。

  而此處的場景與上面的水庫取水有很多相似性。比如,我們要爬取的網站就相當於這個水庫,而我們在 pipelines 中建立的管道就相當於建立的取水管道,只不過 pipelines 中定義的管道不是用來取水,而是用來存取我們爬取的資料,比如,你可以在 pipelines 中定義一個流向檔案的管道,也可以建立一個流向資料庫的管道(比如MySQL/Mongodb/ElasticSearch 等等),而這些管道的閥門就位於 settings.py 檔案中,當然,你也可以多個閥門同時開啟,你甚至還可以定義各管道的優先順序。

  如果您能看到這兒,您是否會覺得很有意思。原來,那些程式界的大佬們,他們在設計這個程式框架的時候,其實參照了很多現實生活中的例項。在此,我雖然不敢肯定編寫出 scrapy 這樣優秀框架的前輩們是不是參考的現實生活中的水庫的例子在設計整個框架,但我能確定的是他們一定參考了和水庫模型類似的場景。

  在此,我也向這些前輩致敬,他們設計的框架非常優秀而且簡單好用,感謝!

前面說了這麼多,都是我的一些個人理解以及感觸,下面給出 pipelines.py 中的示例程式碼:

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html

import pymysql
from twisted.enterprise import adbapi


class ProxyIpPipeline(object):
    """
            xicidaili
    """
    def __init__(self, dbpool):
        self.dbpool = dbpool

    @classmethod
    def from_settings(cls, settings):
        dbparms = dict(
            host=settings["MYSQL_HOST"],
            db=settings["MYSQL_DBNAME"],
            user=settings["MYSQL_USER"],
            passwd=settings["MYSQL_PASSWORD"],
            charset='utf8',
            cursorclass=pymysql.cursors.DictCursor,
            use_unicode=True,
        )
        dbpool = adbapi.ConnectionPool("pymysql", **dbparms)
        # 例項化一個物件
        return cls(dbpool)

    def process_item(self, item, spider):
        # 使用twisted將mysql插入變成非同步執行
        query = self.dbpool.runInteraction(self.do_insert, item)
        query.addErrback(self.handle_error, item, spider)  # 處理異常


    def handle_error(self, failure, item, spider):
        # 處理非同步插入的異常
        print(failure)


    def do_insert(self, cursor, item):
        # 執行具體的插入
        # 根據不同的item 構建不同的sql語句並插入到mysql中
        insert_sql, params = item.get_insert_sql()
        print(insert_sql, params)
        try:
            cursor.execute(insert_sql, params)
        except Exception as e:
            print(e)
複製程式碼

注:

  • 以上程式碼定義的一個管道,是一個注入 MySQL 的管道,其中的寫法模式完全固定,只有其中的 dbpool = adbapi.ConnectionPool("pymysql", **dbparms) 此處需要與你使用的連線 mysql 的第三方庫一致,比如此處我使用的是 pymysql,就設定 pymysql,如果使用的是其他第三方庫,如 MySQLdb,更改為 MySQLdb 即可...
  • 其中的 dbparams 中的 mysql 資訊,是從 settings 檔案中讀取的,我們只需要配置 settings 檔案中的 MySQL 資訊即可

配置 settings.py 中的 pipeline 設定

由於我們的 pipelines.py 中定義的這唯一一條管道的管道名稱是建立 scrapy 專案時預設的。因此,settings.py 檔案中已經預設了會使用該管道。

pipelines 預設設定

在此為了突出演示效果,我在下方列舉了預設的 pipeline 與 其他自定義的 pipeline 的設定

pipeline 自定義設定

上面表示對於我們設定了兩個 pipeline,其中一個是寫入 MySQL,而另一個是寫入 MongoDB,其中後面的 300,400 表示 pipeline 的執行優先順序,其中數值越大優先順序越小。

由於 pipelines 中用到的 MySQL 資訊在 settings 中配置,在此也列舉出,在 settings.py 檔案的末尾新增如下資訊即可

MYSQL_HOST = "localhost"
MYSQL_DBNAME = "crawler"
MYSQL_USER = "root"
MYSQL_PASSWORD = "root123"
複製程式碼

settings 中的 MySQL 設定

5 編寫 spider

  接著使用水庫放水的場景作為示例,水庫放水給下游,但並不是水庫裡的所有東西都需要,比如水庫裡有雜草,有泥石等等,這些東西如果不需要,那麼就要把它過濾掉。而過濾的過程就類似於我們的 spider 的處理過程。

  前面的配置都是輔助型的,spider 裡面才是我們爬取資料的邏輯,在 spiders 目錄下建立一個 xicidaili.py 的檔案,在裡面編寫我們需要的 spider 邏輯。

  由於本文篇幅已經過於冗長,此處我打算省略 spider 中的詳細介紹,可以從官網獲取到 spider 相關資訊,其中官方首頁就有一個簡單的 spider 檔案的標準模板,而我們要做的是,只需要按照模板,在其中編寫我們提取資料的規則即可。

# -*- coding: utf-8 -*-
import scrapy

from scrapy.http import Request
from proxy_ip.items import ProxyIpItem
from proxy_ip.util import DatetimeUtil


class ProxyIp(scrapy.Spider):
    name = 'proxy_ip'
    allowed_domains = ['www.xicidaili.com']
    # start_urls = ['http://www.xicidaili.com/nn/1']

    def start_requests(self):
        start_url = 'http://www.xicidaili.com/nn/'

        for i in range(1, 6):
            url = start_url + str(i)
            yield Request(url=url, callback=self.parse)

    def parse(self, response):
        ip_table = response.xpath('//table[@id="ip_list"]/tr')
        proxy_ip = ProxyIpItem()

        for tr in ip_table[1:]:
            # 提取內容列表
            country = tr.xpath('td[1]/img/@alt')
            ip = tr.xpath('td[2]/text()')
            port = tr.xpath('td[3]/text()')
            server_location = tr.xpath('td[4]/a/text()')
            is_anonymous = tr.xpath('td[5]/text()')
            protocol_type = tr.xpath('td[6]/text()')
            speed = tr.xpath('td[7]/div[1]/@title')
            connect_time = tr.xpath('td[8]/div[1]/@title')
            survival_time = tr.xpath('td[9]/text()')
            validate_time = tr.xpath('td[10]/text()')

            # 提取目標內容
            proxy_ip['country'] = country.extract()[0].upper() if country else ''
            proxy_ip['ip'] = ip.extract()[0] if ip else ''
            proxy_ip['port'] = port.extract()[0] if port else ''
            proxy_ip['server_location'] = server_location.extract()[0] if server_location else ''
            proxy_ip['is_anonymous'] = is_anonymous.extract()[0] if is_anonymous else ''
            proxy_ip['protocol_type'] = protocol_type.extract()[0] if type else ''
            proxy_ip['speed'] = speed.extract()[0] if speed else ''
            proxy_ip['connect_time'] = connect_time.extract()[0] if connect_time else ''
            proxy_ip['survival_time'] = survival_time.extract()[0] if survival_time else ''
            proxy_ip['validate_time'] = '20' + validate_time.extract()[0] + ':00' if validate_time else ''
            proxy_ip['source'] = 'www.xicidaili.com'
            proxy_ip['create_time'] = DatetimeUtil.get_current_localtime()

            yield proxy_ip
複製程式碼

6 middlewares.py

  很多情況下,我們只需要編寫或配置 items,pipeline,spidder,settings 這四個部分即可完整執行一個完整的爬蟲專案,但 middlewares 在少數情況下會有用到。

  再用水庫放水的場景為例,預設情況下,水庫放水的流程大概是,自來水廠需要用水,於是他們發起一個請求給水庫,水庫收到請求後把閥門開啟,按照過濾後的要求把水放給下游。但如果自來水廠有特殊要求,比如說自來水廠他可以只想要每天 00:00 - 7:00 這段時間放水,這就屬於自定義情況了。

  而 middlewares.py 中就是定義的這些資訊,它包括預設的請求與響應處理,比如預設全天放水... 而如果我們有特殊需求,在 middlewares.py 定義即可... 以下附本專案中使用 fake-useragent 來隨機切換請求的 user-agent 的程式碼:

class RandomUserAgentMiddleware(object):
    """
        隨機更換 user-agent
    """
    def __init__(self, crawler):
        super(RandomUserAgentMiddleware, self).__init__()
        self.ua = UserAgent()
        self.ua_type = crawler.settings.get("RANDOM_UA_TYPE", "random")

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request, spider):
        def get_ua():
            return getattr(self.ua, self.ua_type)
        random_ua = get_ua()
        print("current using user-agent: " + random_ua)
        request.headers.setdefault("User-Agent", random_ua)
複製程式碼

settings.py 中配置 middleware 資訊

# Crawl responsibly by identifying yourself (and your website) on the user-agent
RANDOM_UA_TYPE = "random"  # 可以配置 {'ie', 'chrome', 'firefox', 'random'...}

# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    'proxy_ip.middlewares.RandomUserAgentMiddleware': 100,
}
複製程式碼

7 settings.py

settings.py 中配置了專案的很多資訊,用於統一管理配置,下方給出示例:

# -*- coding: utf-8 -*-

import os
import sys
# Scrapy settings for proxy_ip project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#     http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#     http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html

BOT_NAME = 'proxy_ip'

SPIDER_MODULES = ['proxy_ip.spiders']
NEWSPIDER_MODULE = 'proxy_ip.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
RANDOM_UA_TYPE = "random"  # 可以配置 {'ie', 'chrome', 'firefox', 'random'...}

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# Configure maximum concurrent requests performed by Scrapy (default: 16)
#CONCURRENT_REQUESTS = 32

# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 10
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16

# Disable cookies (enabled by default)
COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False

# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#   'Accept-Language': 'en',
#}

# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    'proxy_ip.middlewares.ProxyIpSpiderMiddleware': 543,
#}

# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    'proxy_ip.middlewares.RandomUserAgentMiddleware': 100,
}

# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'proxy_ip.pipelines.ProxyIpPipeline': 300,
}

BASE_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
sys.path.insert(0, os.path.join(BASE_DIR, 'proxy_ip'))


# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
AUTOTHROTTLE_DEBUG = True

# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'


MYSQL_HOST = "localhost"
MYSQL_DBNAME = "crawler"
MYSQL_USER = "root"
MYSQL_PASSWORD = "root123"
複製程式碼

六、執行測試

在 proxy_ip 專案的根目錄下,建立一個 main.py 作為專案執行的入口檔案,其中程式碼如下:

# -*- coding:utf-8 -*-

__author__ = 'jiangzhuolin'

import sys
import os
import time

while True:
    os.system("scrapy crawl proxy_ip")  # scrapy spider 的啟動方法 scrapy crawl spider_name
    print("程式開始休眠...")
    time.sleep(3600)  # 休眠 1 小時後繼續爬取

複製程式碼

右鍵 "run main" 檢視執行效果

程式執行效果

七、總結

  我的原來打算是寫一篇 scrapy 簡單專案的詳細介紹,把裡面各種細節都通過個人的理解分享出來。但非常遺憾,由於經驗不足,導致越寫越覺得篇幅會過於冗長。因此裡面有大量資訊被我簡化或者直接沒有寫出來,本文可能不適合完全小白的新手,如果你寫過簡單的 scrapy 專案,但對其中的框架不甚理解,我希望能通過本文有所改善。

  本專案程式碼已提交到我個人的 github 與 碼雲上,如果訪問 github 較慢,可以訪問碼雲獲取完整程式碼,其中,包括了建立 MySQL 資料庫表的 SQL 程式碼

github 程式碼地址:github.com/jiangzhuoli…

碼雲程式碼地址:gitee.com/jzl975/prox…

相關文章