scrapy爬蟲具體案例步驟詳細分析


scrapy爬蟲具體案例詳細分析

scrapy,它是一個整合了的爬蟲框架, 有着非常健全的管理系統. 而且它也是分布式爬蟲, 它的管理體系非常復雜. 但是特別高效.用途廣泛,主要用於數據挖掘、檢測以及自動化測試。

本項目實現功能:模擬登錄、分頁爬取、持久化至指定數據源、定時順序執行多個spider

一、安裝

首先需要有環境,本案例使用
python 2.7,macOS 10.12,mysql 5.7.19

下載scrapy

pip install scrapy

下載Twisted

pip install Twisted

下載MySQLdb

pip install MySQLdb

二、構建項目

創建項目

*****@localhost:~$ scrapy startproject scrapy_school_insurance

在對應的目錄下面就會生成如下目錄格式

scrapy_school_insurance/
	spiders/                   		
		_init_.py
	_init_.py
	items.py					---- 實體(存儲數據信息)
	middlewares.py			---- 中間件(初級開發無需關心)
	pipelines.py				---- 處理實體,頁面被解析后的數據會發送到此(持久化、驗證實體有效性,去重)
	setting.py             ---- 設置文件
scrapy.cfg					---- configuration file

在spiders下創建school_insurance_spider.py 編寫具體爬取頁面信息的代碼

三、spider

首先,要做的是定義items,也就是你需要的數據項,數據存儲的地方。

定義Item非常簡單,只需要繼承scrapy.Item類,並將所有字段都定義為scrapy.Field類型即可。
Field對象用來對每個字段指定元數據。

定義子項目的items.py

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

import scrapy


class ScrapySchoolInsuranceItem(scrapy.Item):
    # 班級名稱
    classroom = scrapy.Field()
    # 學生身份證
    id_card = scrapy.Field()
    # 學生姓名
    student_name = scrapy.Field()
    # 家長姓名
    parent_name = scrapy.Field()
    # 練習電話
    phone = scrapy.Field()
    # 是否繳費
    is_pay = scrapy.Field()
    # 學校名稱
    school_id = scrapy.Field()
    

下面進行網絡爬取步驟:
school_insurance_spider.py 文件
此類需要繼承scrapy.Spider類
先分析一下基本結構和工作流程:

import scrapy


class SchoolInsuranceSpider(scrapy.Spider):
    name = "school_insurance"

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
    	pass

這是spider最基本的結構:
name:是你爬蟲的名字,后面啟動爬蟲的時候使用就是這個參數。
start_requests():是初始請求,爬蟲引擎會自動調取。
urls:是你定義需要爬取的url,可以是一個也可以是多個。
parse():函數主要進行網頁分析,爬取數據。當你的返回沒有指定回調函數的時候,默認回調parse()函數;

先來分析一下在迭代器中的這句話:

yield scrapy.Request(url=url, callback=self.parse)

yield 是一個python關鍵字,代表這個函數返回的是個生成器。

理解yield,你必須理解:當你僅僅調用這個函數的時候,函數內部的代碼並不立馬執行,這個函數只是返回一個生成器對象。只有當你進行迭代此對象的時候才會真正的執行其中的代碼,並且以后的迭代會從在函數內部定義的那個循環的下一次,再返回那個值,直到沒有可以返回的。

這里的start_requests函數就會被當做一個生成器使用,而scrapy引擎可以被看作是迭代器。scrapy會逐一獲取start_requests方法中生成的結果,並判斷該結果是一個什么樣的類型。當返回Request時會被加入到調度隊列中,當返回items時候會被pipelines調用。很顯然,此時迭代返回的是request對象。

引擎會將此請求發送到下載中間件,通過下載中間件下載網絡數據。一旦下載器完成頁面下載,將下載結果返回給爬蟲引擎。引擎將下載器的響應(response對象)通過中間件返回給爬蟲。此時request()使用了回調函數parse(),則response作為第一參數傳入,進行數據被提取操作。

當你直接使用scrapy.Request()方法時默認是get請求。

而此項目是要爬取一個需要登錄的網站,第一步要做的是模擬登錄,需要post提交form表單。所以就要使用scrapy.FormRequest.from_response()方法:

def start_requests(self):
        start_url = 'http://jnjybx.jnjy.net.cn/admin/login.aspx?doType=loginout'
        return [
            Request(start_url, callback=self.login)
        ]
        
# 模擬用戶登錄
def login(self, response):
        return scrapy.FormRequest.from_response(
            response,
            formdata={'username': ‘***’, 'password': ‘***’},
            meta={'school_id': ***},
            callback=self.check_login
        )
        

訪問網址后我們直接回調login方法,進行表單提交請求。表單結構需根據不同網站自己分析,此網站只需提交username和password,如果你需要傳遞自定義參數,可通過meta屬性進行定義傳遞,回調函數中使用response.meta['school_id']就可以獲取傳遞的參數了。一般網站登錄成功之后會直接返回,登錄后的頁面的response,就可以直接回調數據爬取的方法進行爬取了。
但此網站會單獨返回一段json來告訴我是否登錄成功,並且並不提供下一步的url,所以我這里多了一步check_login()方法,並在判斷登錄成功后的代碼段里重新請求了登錄成功后的url。(scrapy是默認保留cookie的!)

模擬登錄完整代碼:

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

import json
import logging
from scrapy.http import Request

from scrapy_school_insurance.items import ScrapySchoolInsuranceItem


class SchoolInsuranceSpider(scrapy.Spider):
    name = "school_insurance"

    allowed_domains = ['jnjy.net.cn']

	 # 在這里定義了登錄成功后的url,供再次請求使用
    target_url = 'http://jnjybx.jnjy.net.cn/admin/index.aspx'

    def __init__(self, account, **kwargs):
        super(SchoolInsuranceSpider, self).__init__(**kwargs)
        self.account = account

    def start_requests(self):
        start_url = 'http://jnjybx.jnjy.net.cn/admin/login.aspx?doType=loginout'

        return [
            Request(start_url, callback=self.login)
        ]

    # 模擬用戶登錄
    def login(self, response):
            return scrapy.FormRequest.from_response(
                response,
                formdata={'username': '***', 'password': '***'},
                meta={'school_id': ***},
                callback=self.check_login
            )

    # 檢查登錄是否成功
    def check_login(self, response):
        self.logger.info(response.body)
        body_json = json.loads(response.body)
        # 獲取參數
        school_id = response.meta['school_id']
        self.logger.info(school_id)
        if "ret" in body_json and body_json["ret"] != 0:
            self.logger.error("Login failed")
            return
        else:
            self.logger.info("Login Success")
            # 這里重新請求了一次
            yield scrapy.Request(self.target_url, meta={'school_id': school_id}, callback=self.find_student_manager)

因為主頁並不是我需要的頁面,所以find_student_manager()方法作用是找到我需要的那個鏈接,進行請求后再進行分頁爬取。

    # 檢驗登錄成功后 跳轉 學生管理連接
    def find_student_manager(self, response):
        self.logger.info(response.url)
        school_id = response.meta['school_id']
        self.logger.info(school_id)
        next_page = response.css('a[href="studentManage.aspx"]::attr(href)').extract_first()
        logging.info(next_page)
        if next_page is not None:
            self.logger.info("next_url:" + next_page)
            next_page = response.urljoin(next_page)
            self.logger.info("next_url:" + next_page)
            yield scrapy.Request(next_page, meta={'school_id': school_id})
        else:
            self.logger.error("not find href you needed")

解析html是需要選擇器的,和編寫css時給頁面加樣式的時候操作類似。

scrapy提供兩種選擇器xpath(),css();

XPath是用來在XML中選擇節點的語言,同時可以用在HTML上面。CSS是HTML文檔上面的樣式語言。

css選擇器語法請參考:https://www.cnblogs.com/ruoniao/p/6875227.html

我這里使用css選擇器來獲取a標簽的herf,分析一下這句話:

next_page=response.css('a[href="studentManage.aspx"]::attr(href)').extract_first()

簡單來講,.css()返回的是一個SelectorList對象,它是內建List的子類,我們並不能直接使用它得到數據。.extract()方法就是使SelectorList ——> List,變為單一化的unicode字符串列表,我們就可以直接使用了。.extract_first()顧名思義,就是取第一個值,也可寫為 .extract()[0],需要注意的是list為空的時候會報異常。

再看看這句話:

next_page = response.urljoin(next_page)

通過選擇器獲取到是一個href的字符串值,是相對url,需要使用.urljoin()方法來構建完整的絕對URL,便於再次請求。

進入需要爬取的頁面,開始分頁爬取:

# 爬取 需要 的數據
    def parse(self, response):
        school_id = response.meta['school_id']

        counter = 0
        # id_cards = response.css('tr[target="ID_CARD"]::attr(rel)')
        # i = 0
        view_state = response.css('input#__VIEWSTATE::attr(value)').extract_first()
        view_state_generator = response.css('input#__VIEWSTATEGENERATOR::attr(value)').extract_first()
        page_num = response.css('input[name="pageNum"]::attr(value)').extract_first()
        num_per_page = response.css('input[name="numPerPage"]::attr(value)').extract_first()
        order_field = response.css('input[name="orderField"]::attr(value)').extract_first()
        order_direction = response.css('input[name="orderDirection"]::attr(value)').extract_first()
        self.logger.info(page_num)

        for sel in response.css('tr[target="ID_CARD"]'):

            counter = counter + 1
            item = ScrapySchoolInsuranceItem()

            item['classroom'] = sel.css('td::text').extract()[0]
            item['id_card'] = sel.css('td::text').extract()[1]
            item['student_name'] = sel.css('td::text').extract()[2]
            item['parent_name'] = sel.css('td::text').extract()[3]
            item['phone'] = sel.css('td::text').extract()[4]
            item['is_pay'] = sel.css('span::text').extract_first()
            item['school_id'] = school_id
            yield item

        self.logger.info(str(counter)+" "+num_per_page)

        if counter == int(num_per_page):
            yield scrapy.FormRequest.from_response(
                response,
                formdata={'__VIEWSTATE': view_state, '__VIEWSTATEGENERATOR': view_state_generator,
                          'pageNum': str(int(page_num) + 1), 'numPerPage': num_per_page,
                          'orderField': order_field, 'orderDirection': order_direction},
                meta={'school_id': school_id},
                callback=self.parse
            )

分析頁面:

我所需要的數據是的結構,且每頁的結構都相同, 其中一小段html:

<tr target="ID_CARD" rel='1301*******0226'>
         <td >初中二年級7班(南京市***中學)</td>
         <td >130**********226</td>
         <td >李**</td>
         <td >蔣**</td>
         <td >139******61</td>
         <td ><span style='color:green'>已支付</span></td>
</tr>

所以只需要使用選擇器進行循環爬取付值給item,然后回調自己就可以把所有數據爬取下來了。

看Chrome的請求狀態,發現是post請求且有兩個陌生的參數。分析頁面發現有兩個隱藏參數__VIEWSTATE,__VIEWSTATEGENERATOR,且每次請求都會改變。

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="***" />

<input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="CCD96271" />

ViewState是ASP.NET中用來保存WEB控件回傳時狀態值一種機制。__EVENTVALIDATION只是用來驗證事件是否從合法的頁面發送,只是一個數字簽名。
對於我們爬蟲而需要做的就是,獲取本頁面的這兩個值,在請求下一頁面的時候作為參數進行請求。分頁爬取需要不斷回調自己進行遞歸,此時請求並不是get請求,而是url不變的post請求,我這是使用一個計數器counter防止其無限遞歸下去。

這里你會仔細發現,一開始自定義的school_id也被加入了item中。

前面提到,爬蟲引擎會檢索返回值,返回items時候會被pipelines調用,pipelines就是處理數據的類。

四、持久化操作

pipelines.py

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


class ScrapySchoolInsurancePipeline(object):

    def process_item(self, item, spider):

        db_name = ""
        if item["school_id"] == '1002':
            db_name = "scrapy_school_insurance"
        elif item["school_id"] == '1003':
            db_name = "db_mcp_1003"
        if db_name != "":
            conn = MySQLdb.connect("localhost", "root", "a123", db_name, charset='utf8')
            cursor = conn.cursor()
            # 使用cursor()方法獲取操作游標
            # 使用execute方法執行SQL語句
            sql = "insert into `insurance_info` " \
                  "(classroom,id_card,student,parent,phone,is_pay,school_id) " \
                  "values (%s, %s, %s, %s, %s, %s, %s)" \
                  "ON DUPLICATE KEY UPDATE is_pay = %s;"
            params = (item["classroom"], item["id_card"],
                      item["student_name"], item["parent_name"],
                      item["phone"], item["is_pay"], item["school_id"], item["is_pay"])
            cursor.execute(sql, params)
       、conn.commit()
       cursor.close()
       conn.close()

process_item()方法,是pipeline默認調用的。因為需要根據school_id進行分庫插入並沒有進行setting設置,而是使用MySQLdb庫動態鏈接數據庫。執行sql操作和java類似。sql使用“ON DUPLICATE KEY UPDATE”去重更新。

此時我們已經完成了三步,定義items,編寫spider邏輯,pipeline持久化。
下一步就是如何讓程序正確的跑起來。

五、運行

談運行,首先要說一下本案例setting.py的書寫,它設計運行的方方面面。

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

BOT_NAME = 'scrapy_school_insurance'

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

# 編碼格式
FEED_EXPORT_ENCODING = 'utf-8'

# Obey robots.txt rules 不遵循網絡規范
ROBOTSTXT_OBEY = False

EXTENSIONS = {
    'scrapy.telnet.TelnetConsole': None
}

# 設置log級別
# LOG_LEVEL = 'INFO'

# 本項目帶登陸需要開啟cookies,一般爬取不需要cookie
COOKIES_ENABLED = True

 'scrapy_school_insurance.middlewares.ScrapySchoolInsuranceSpiderMiddleware': 543,
 
# 開啟 后pipelines才生效 后面的數字表示的是pipeline的執行順序
ITEM_PIPELINES = {
   'scrapy_school_insurance.pipelines.ScrapySchoolInsurancePipeline': 300,
}

完成如上操作后,在終端中到達此項目根目錄下運行:

scrapy crawl school_insurance

想生成json文件:

scrapy crawl school_insurance -o test.json -t json

現在我有多個賬號存在數據庫中,想分別登入讀取信息該如何操作?

構思了兩個方案:

1.讀取所有賬號,存入一個spider中,每次用一個賬號爬取完后退出登錄,清除cookie,再拿第二個賬號登入,進行爬取工作。
2.動態配置spider,每個賬號對應一個spider,進行順序執行。

僅從描述上看,第二個方案就比第一個方案靠譜,可行。

采用第二個方案,需要動態配置完之后告訴spider要運行了,也就是使用編程的方式運行spider:Scrapy是構建於Twisted異步網絡框架基礎之上,因此可以啟動Twisted reactor並在reactor中啟動spider。CrawlerRunner就會為你啟動一個Twisted reactor。 需先新建一個run.py:

#!/bin/env python
#  -*- coding: utf-8 -*-
import logging

import MySQLdb.cursors
from twisted.internet import reactor


from scrapy.utils.project import get_project_settings
from scrapy.utils.log import configure_logging
from scrapy.crawler import CrawlerRunner

import sys
sys.path.append("../")
from scrapy_school_insurance.spiders.school_insurance_spider import SchoolInsuranceSpider

if __name__ == '__main__':
    settings = get_project_settings()
    configure_logging(settings)
    db_names = ['scrapy_school_insurance', 'db_mcp_1003']
    results = list()
    for db_name in db_names:
        logging.info(db_name)
        db = MySQLdb.connect("localhost", "root", "a123", db_name, charset='utf8',
                             cursorclass=MySQLdb.cursors.DictCursor)
        # 使用cursor()方法獲取操作游標
        cursor = db.cursor()
        # 使用execute方法執行SQL語句
        cursor.execute("select * from school_account")
        # 使用 fetchone() 方法獲取一條數據
        result = cursor.fetchone()
        logging.info(result)
        results.append(result)
        db.close()
    logging.info(results)
    runner = CrawlerRunner(settings)

    for result in results:
        runner.crawl(SchoolInsuranceSpider, account=result)

    d = runner.join()
    d.addBoth(lambda _: reactor.stop())

    reactor.run()

    logging.info("all findAll")

需要注意的是數據庫

獲取賬號信息,啟動reactor,啟動spider

runner = CrawlerRunner(settings)

for result in results:
    runner.crawl(SchoolInsuranceSpider, account=result)

這句話在spider時,將賬號信息作為參數傳遞過去了,所以我們的spider需要修改一下接收參數
加入構造器,供內部調用:

 def __init__(self, account, **kwargs):
        super(SchoolInsuranceSpider, self).__init__(**kwargs)
        self.account = account

此時login()可修改為:

    def login(self, response):
        return scrapy.FormRequest.from_response(
            response,
            formdata={'username': self.account['username'], 'password': self.account['password']},
            meta={'school_id': self.account['school_id']},
            callback=self.check_login
        )

完成后在終端運行run.py即可:

@localhost:~/project-workspace/scrapy_school_insurancescrapy_school_insurance/$ python run.py

此時會報 not import SchoolInsuranceSpider,但是已經明明import了,因為路徑的問題,run.py啟動時並找不到它,需在import前加 sys.path.append("../")python才可以通過路徑找到它

最后需要做的就是定時啟動python腳本:
使用crontab
詳細語法參考:https://blog.csdn.net/netdxy/article/details/50562864

主要是兩步

crontab -e

添加定時任務,每天3點執行python腳本,wq,ok

* */3 * * * python ~/project-workspace/scrapy_school_insurance/scrapy_school_insurance/run.py

六、梳理

Scrapy架構組件、運行流程,結合實例理解一下


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM