第七色在线视频,2021少妇久久久久久久久久,亚洲欧洲精品成人久久av18,亚洲国产精品特色大片观看完整版,孙宇晨将参加特朗普的晚宴

首頁 慕課教程 Scrapy 入門教程 Scrapy 入門教程 Scrapy與 Selenium 的結合使用

Scrapy與 Selenium 的結合

今天我們來使用 Scrapy 和 Selenium 結合爬取京東商城中網絡爬蟲相關的書籍數據。

1. 需求分析與初步實現

今天我們的目的是使用 Scrapy 和 Selenium 結合來爬取京東商城中搜索 “網絡爬蟲” 得到的所有圖書數據,類似于下面這樣的數據:

圖片描述

京東商城搜索 網絡爬蟲

搜索出的結果有9800+條數據,共計100頁。我們現在要抓取所有的和網絡爬蟲相關的書籍數據。有一個問題需要注意,搜索的100頁數據中必定存在重復的結果,我們可以依據圖書的詳細地址來進行去重。此外,我們提取的圖書數據字段有:

  • 圖書名;
  • 價格;
  • 評價數;
  • 店鋪名稱;
  • 圖書詳細地址;

需求已經非常明確,現在開始使用 Selenium 和 Scrapy 框架結合來完成這一需求。來看看如果我們是單純使用 Selenium 工具,該如何完成數據爬取呢?這里會有一個問題需要注意:按下搜索按鈕后,顯示的數據只有30條,只有使用鼠標向下滾動后,才會加載更多數據,最終顯示60條結果,然后才會到達翻頁的地方。在 selenium 中我們可以使用如下兩行代碼實現滾動條滑到最底端:

height = driver.execute_script("return document.body.scrollHeight;")
driver.execute_script(f"window.scrollBy(0, {height})")
time.sleep(2)

可以看到,上面兩行代碼主要是執(zhí)行 js 語句。第一行代碼是得到頁面的底部位置,第二行代碼是使用 scrollBy() 方法控制頁面滾動條移動到底部。接下來,我們來看看頁面數據的提取,直接右鍵 F12,可以通過 xpath 表達式得到所有需要抓取的數據。為此,我編寫了一個根據頁面代碼提取圖書數據的方法,具體如下:

def parse_book_data(html):
    etree_html = etree.HTML(html)
    # 獲取列表
    gl_items = etree_html.xpath('//div[@id="J_goodsList"]/ul/li')
    print('總共獲取數據:{}'.format(len(gl_items)))
    res = []
    for item in gl_items:
        book_name_em = item.xpath('.//div[@class="p-name"]/a/em/text()')[0]
        book_name_font = item.xpath('.//div[@class="p-name"]/a/em/font/text()')
        book_name_font = "".join(book_name_font) if book_name_font else ""
        # 獲取圖書名
        book_name = f"{book_name_em}{book_name_font}"
        # 獲取圖書的詳細介紹地址
        book_detail_url = item.xpath('.//div[@class="p-name"]/a/@href')[0]
        # 獲取圖書價格
        price = item.xpath('.//div[@class="p-price"]/strong/i/text()')[0]
        # 獲取評論數
        comments = item.xpath('.//div[@class="p-commit"]/strong/a/text()')[0]
        # 獲取店鋪名稱
        shop_name = item.xpath('.//div[@class="p-shopnum"]/a/text()')
        shop_name = shop_name[0] if shop_name else ""

        data = {}

        data['book_name'] = book_name
        data['book_detail_url'] = book_detail_url
        data['price'] = price
        data['comments'] = comments
        data['shop_name'] = shop_name

        res.append(data)
    # 返回頁面解析的結果
    print('本頁獲取的結果:{}'.format(res))
    return res

現在來思考下如何能使用 selenium 一頁一頁訪問?我給出了如下代碼:

def get_page_data(driver, page):
    """
    :driver 驅動
    :page   第幾頁
    """
    # 請求當前頁
    if page > 1:
        WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located((By.ID, 'J_bottomPage'))
        )
        driver.find_element_by_xpath(f'//div[@id="J_bottomPage"]/span/a[text()="{page}"]').click()
        time.sleep(2)
    
    # 滾動到最下面,出現京東圖書剩余書籍數據
    height = driver.execute_script("return document.body.scrollHeight;")
    driver.execute_script(f"window.scrollBy(0, {height})")
    time.sleep(2)
    return parse_book_data(driver.page_source)

對于第一頁的訪問是在輸入關鍵字<網絡爬蟲>后點擊按鈕得到的,我們不需要放到這個函數來得到,只需要滾動到底部得到所有的圖書數據即可;而對于第2頁之后的頁面,我們需要使用 selenium 的模擬鼠標點擊功能,點擊下對應頁后便能跳轉得到該頁,然后再滾動到底部,就可以得到整頁的搜索結果。我們來看看完整的實現:

import time
import random
import re

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains

from lxml import etree

def get_page_data(driver, page):
    """
    :driver 驅動
    :page   第幾頁
    """
    # 具體代碼參考上面
    # ...
    
    
def parse_book_data(html):
    """
    解析頁面圖書數據
    """
    # 具體代碼參考上面
    # ...
    

options = webdriver.ChromeOptions()
options.add_experimental_option("excludeSwitches", ['enable-automation'])
driver = webdriver.Chrome(options=options, executable_path="C:/Users/Administrator/AppData/Local/Google/Chrome/Application/chromedriver.exe")
driver.maximize_window()

driver.get("https://www.jd.com/")

# 輸入網絡爬蟲,然后點擊搜索
driver.find_element_by_id('key').send_keys('網絡爬蟲')
driver.find_elements_by_xpath('//div[@role="serachbox"]/button')[0].click()

time.sleep(2)

max_page = 100
for i in range(1, max_page + 1):
    get_page_data(driver, i)

下面來看看代碼執(zhí)行的效果,這里為了能盡快執(zhí)行完,我將 max_page 參數調整為10,只獲取10頁搜索結果,一共是600條數據:

從上面的演示中,可以看到最后每頁抓取的數據都是60條。

2. Scrapy 與 Selenium 結合爬取京東圖書數據

接下來我們對上面的代碼進行調整和 Scrapy 框架結合,而第一步需要做的就是建立好相應的工程:

# 創(chuàng)建爬蟲項目
PS D:\shencong\scrapy-lessons\code\chap17> scrapy startproject jdbooks
# ...
# 進入到spider目錄,使用genspider命令創(chuàng)建爬蟲文件
PS D:\shencong\scrapy-lessons\code\chap17\jd_books\jd_books\spiders> scrapy genspider jd www.jd.com

創(chuàng)建好工程后就是編寫 items.py 中的 JdBooksItem 類,這非常簡單,直接根據我們前面定義好的字段編寫相應的代碼即可:

class JdBooksItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    book_name = scrapy.Field()
    price = scrapy.Field()
    comments = scrapy.Field()
    shop_name = scrapy.Field()
    book_detail_url = scrapy.Field()

整個項目的難點是如何實現下一頁數據的爬?。壳懊婵梢允褂?selenium 去自動點擊頁號而進入下一個,然而在 Scrapy 中卻不太好這樣處理。我們通過分析京東搜索的 URL 后發(fā)現,其搜索的 URL 可以簡化為如下形式:https://search.jd.com/Search?keyword=搜索關鍵字&page=(頁號* 2 - 1),我們只需要提供搜索的關鍵字以及相應的請求頁號即可。例如下圖所示:

圖片描述

京東搜索 URL 參數

因此我們在 settings.py 中準備兩個參數:一個是搜索的關鍵字,另一個是爬取的最大頁數。具體的形式如下:

# settings.py
# ...
KEYWORD = "網絡爬蟲"
MAX_PAGE = 10

緊接著我們可以構造出請求不同頁的 URL 并交給 Scrapy 的引擎和調度器去處理,對應的 Spider 代碼如下:

# 代碼位置:jd_books/jd_books/spiders/jd.py

from urllib.parse import quote
from scrapy import Spider, Request

from jd_books.items import JdBooksItem


class JdSpider(Spider):
    name = 'jd'
    allowed_domains = ['www.jd.com']
    start_urls = ['http://www.jd.com/']
    base_url = "https://search.jd.com/Search?keyword={}&page={}"

    def start_requests(self):
        keyword = self.settings.get('KEYWORD', "Python")
        for page in range(1, self.settings.get('MAX_PAGE') + 1):
            url = self.base_url.format(quote(keyword), page * 2 - 1)
            yield Request(url=url, callback=self.parse_books, dont_filter=True)

    def parse_books(self, response):
        goods_list = response.xpath('//div[@id="J_goodsList"]/ul/li')
        print('本頁獲取圖書數目:{}'.format(len(goods_list)))
        for good in goods_list:
            book_name_em = good.xpath('.//div[@class="p-name"]/a/em/text()').extract()[0]
            book_name_font = good.xpath('.//div[@class="p-name"]/a/em/font/text()').extract()
            book_name_font = "".join(book_name_font) if book_name_font else ""
            book_name = f"{book_name_em}{book_name_font}"
            book_detail_url = good.xpath('.//div[@class="p-name"]/a/@href').extract()[0]
            price = good.xpath('.//div[@class="p-price"]/strong/i/text()').extract()[0]
            comments = good.xpath('.//div[@class="p-commit"]/strong/a/text()').extract()[0]
            shop_name = good.xpath('.//div[@class="p-shopnum"]/a/text()').extract()[0]
            
            item = JdBooksItem()
            item['book_name'] = book_name
            item['book_detail_url'] = book_detail_url
            item['price'] = price
            item['comments'] = comments
            item['shop_name'] = shop_name

            yield item

上面的代碼就是單純的生成多頁的 Request 請求 (start_requests() 方法) 和解析網頁數據 (parse_books() 方法)。這個解析數據完全依賴于我們獲取完整的頁面源碼,那么如何在 Scrapy 中使用 selenium 去請求 URL 然后獲取頁面源碼呢?答案就是下載中間件。我們在編寫一個下載中間件,攔截發(fā)送的 request 請求,對于請求京東圖書數據的請求我們會切換成 selenium 的方式去獲取網頁源碼,然后將得到的頁面源碼封裝成 Response 響應并返回。在生成 Scrapy 項目中已經為我們準備好了一個 middleware.py 文件,我們按照上面的思路來完成相應代碼,具體內容如下:

import time

from scrapy import signals
from scrapy.http.response.html import HtmlResponse

from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By

# useful for handling different item types with a single interface
from itemadapter import is_item, ItemAdapter

options = webdriver.ChromeOptions()
# 注意,使用這個參數我們就不會看到啟動的google瀏覽器,無界面運行
options.add_argument('-headless')
options.add_experimental_option("excludeSwitches", ['enable-automation'])

class JdBooksSpiderMiddleware:
    # 保持不變
    # ...
    

class JdBooksDownloaderMiddleware:
    # Not all methods need to be defined. If a method is not defined,
    # scrapy acts as if the downloader middleware does not modify the
    # passed objects.

    def __init__(self):
        self.driver = webdriver.Chrome(options=options, executable_path="C:/Users/Administrator/AppData/Local/Google/Chrome/Application/chromedriver.exe")

    # ...

    def process_request(self, request, spider):
        # Called for each request that goes through the downloader
        # middleware.

        # Must either:
        # - return None: continue processing this request
        # - or return a Response object
        # - or return a Request object
        # - or raise IgnoreRequest: process_exception() methods of
        #   installed downloader middleware will be called
        print('使用 selenium 請求頁面:{}'.format(request.url))
        if request.url.startswith("https://search.jd.com/Search"):
            # 如果是獲取京東圖書數據的請求,使用selenium方式獲取頁面
            self.driver.get(request.url)
            time.sleep(2)
            # 將滾動條拖到最底端,獲取一頁完整的60條數據
            height = self.driver.execute_script("return document.body.scrollHeight;")
            self.driver.execute_script(f"window.scrollBy(0, {height})")
            time.sleep(2)
            # 將最后渲染得到的頁面源碼作為響應返回
            return HtmlResponse(url=request.url, body=self.driver.page_source, request=request, encoding='utf-8', status=200)
        
    # ...

緊接著,我們需要將這個下載中間件在 settings.py 中啟用:

DOWNLOADER_MIDDLEWARES = {
   'jd_books.middlewares.JdBooksDownloaderMiddleware': 543,
}

最后我們來完成下數據的存儲,繼續(xù)使用 mongodb 來保存抓取到的數據。從實際測試中發(fā)現京東的搜索結果在100頁中肯定會有不少重復的數據。因此我們的 item pipelines 需要完成2個處理,分別是去重和保存。來直接看代碼:

import pymongo
from scrapy.exceptions import DropItem

from itemadapter import ItemAdapter


class JdBooksPipeline:
    def open_spider(self, spider):
        self.client = pymongo.MongoClient(host='47.115.61.209', port=27017)
        self.client.admin.authenticate("admin", "shencong1992")
        db = self.client.scrapy_manual
        self.collection = db.jd_books

    def process_item(self, item, spider):
        try:
            book_info = {
                'book_name': item['book_name'],
                'comments': item['comments'],
                'book_detail_url': item['book_detail_url'],
                'shop_name': item['shop_name'],
                'price': item['price'],
            }
            self.collection.insert_one(book_info)
        except Exception as e:
            print("插入數據異常:{}".format(str(e)))
        return item

    def close_spider(self, spider):
        self.client.close()


class DuplicatePipeline:
    """
    去除重復的數據,重復數據直接拋出異常,不會進入下一個流水線處理
    """
    def __init__(self):
        self.book_url_set = set() 

    def process_item(self, item, spider):
        if item['book_detail_url'] in self.book_url_set:
            print('重復搜索結果:book={}, url={}'.format(item['book_name'], item['book_detail_url']))
            raise DropItem('duplicate book info, drop it')
        self.book_url_set.add(item['book_detail_url'])
        return item

我們直接使用 Item 的 book_detail_url 字段來判斷數據是否重復。此外,同樣需要將這兩個 Item Pipelines 在 settings.py 中啟用,且保證 DuplicatePipeline 需要先于 JdBooksPipeline 處理:

ITEM_PIPELINES = {
   'jd_books.pipelines.DuplicatePipeline': 200,
   'jd_books.pipelines.JdBooksPipeline': 300,
}

最后剩下一步就是禁止遵守 Robot 協(xié)議:

ROBOTSTXT_OBEY = True

至此,我們的 Scrapy 和 Selenium 結合爬取京東圖書數據的項目就算完成了。為了快速演示效果,我們將最大請求頁設置為10,然后運行代碼看看實際的爬取效果:

3. 小結

本小節(jié)中我們使用 scrapy 和 selenium 結合完成了一個京東圖書的爬取案例,從這個案例中我們能看到了 Scrapy 強大的第三方結合能力,包括前面的 Splash 服務。

4. 參考文獻