1. 程式人生 > >Python網路爬蟲(九):爬取頂點小說網站全部小說,並存入MongoDB

Python網路爬蟲(九):爬取頂點小說網站全部小說,並存入MongoDB

前言:本篇部落格將爬取頂點小說網站全部小說、涉及到的問題有:Scrapy架構、斷點續傳問題、Mongodb資料庫相關操作。

背景:

Python版本:Anaconda3

執行平臺:Windows

IDE:PyCharm

資料庫:MongoDB

瀏覽器工具: Chrome瀏覽器

前面的部落格中已經對Scrapy作了相當多的介紹所以這裡不再對Scrapy技術作過多的講解。

一、爬蟲準備工作:

此次我們爬取的是免費小說網站:頂點小說

我們要想把它全部的小說爬取下來,是不是得有全部

小說的連結?

我們看到頂點小說網站上有一個總排行榜。

這裡寫圖片描述

點選進入後我們看到,這裡有網站上所有的小說,一共有1144頁,每頁大約20本小說,算下來一共大約有兩萬兩千多本,是一個龐大的資料量,並且小說的數量還在不斷的增長中。

好!我們遇到了第一個問題,如何獲取總排行榜中的頁數呢?也就是現在的“1144”。

1、獲取排行榜頁面數:

最好的方法就是用Xpath。

我們先用F12審查元素,看到“1144”放在了“id”屬性為“pagestats”的em節點中。

這裡寫圖片描述

我們再用Scrapy Shell分析一下網頁。

注意:Scrapy Shell是一個非常好的工具,我們在編寫爬蟲過程中,可以用它不斷的測試我們編寫的Xpath語句,非常方便。

輸入命令:

scrapy shell "http://www.23us.so/top/allvisit_2.html"

然後就進入了scrapy shell
這裡寫圖片描述

因為頁數放在“id”屬性為“pagestats”的em節點中,所以我們可以在shell中輸入如下指令獲取。

response.xpath('//*[@id="pagestats"]/text()').extract_first()

這裡寫圖片描述

我們可以看到,Xpath一如既往的簡單高效,頁面數已經被擷取下來了。

2、獲取小說主頁連結、小說名稱:

接下來,我們遇到新的問題,如何獲得每個頁面上的小說的連結呢?我們再來看頁面的HTML程式碼。

這裡寫圖片描述

小說的連結放在了“a”節點裡,而且這樣的a節點區別其他的“a”節點的是,沒有“title”屬性。

所以我們用shell測試一下,輸入命令:

response.xpath('//td/a[not(@title)]/@href').extract()

這裡寫圖片描述
我們看到,小說的連結地址我們抓到了。

同樣還有小說名,

response.xpath('//td/a[not(@title)]/text()').extract()

這裡寫圖片描述

我們可以看到頁面上的小說名稱我們也已經抓取到了。

3、獲取小說詳細資訊:

我們點開頁面上的其中一個小說連結:

這裡寫圖片描述

這裡有小說的一些相關資訊和小說章節目錄的地址。

我們想要的資料首先是小說全部章節目錄的地址,然後是小說類別、小說作者、小說狀態、小說最後更新時間。

我們先看小說全部章節目錄的地址。用F12,我們看到:

這裡寫圖片描述

小說全部章節地址放在了“class”屬性為“btnlinks”的“p”節點的第一個“a”節點中。

我們還是用scrapy shell測試一下我們寫的xpath語句。

鍵入命令,進入shell介面

scrapy shell "http://www.23us.so/xiaoshuo/13007.html"

在shell中鍵入命令:

response.xpath('//p[@class="btnlinks"]/a[1]/@href').extract_first()

這裡寫圖片描述

小說的章節目錄頁面我們已經擷取下來了。

類似的還有小說類別、小說作者、小說狀態、小說最後更新時間,命令分別是:

#小說類別
response.xpath('//table/tr[1]/td[1]/a/text()').extract_first()    
#小說作者
response.xpath('//table/tr[1]/td[2]/text()').extract_first()   
#小說狀態
response.xpath('//table/tr[1]/td[3]/text()').extract_first()   
#小說最後更新時間
response.xpath('//table/tr[2]/td[3]/text()').extract_first()

這裡寫圖片描述

4、獲取小說全部章節:

我們點開“最新章節”,來到小說全部章節頁面。

這裡寫圖片描述

我們如何獲得這些連結呢?答案還是Xpath。

用F12看到,各章節地址和章節名稱放在了一個“table”中:

這裡寫圖片描述
退出上次的scrapy shell ,分析 全部章節頁面。

scrapy shell "http://www.23us.so/files/article/html/13/13007/index.html"

在shell中鍵入Xpath語句:

response.xpath('//table/tr/td/a/@href').extract()

這裡寫圖片描述

同樣還有各章節名稱

response.xpath('//table/tr/td/a/text()').extract()

這裡寫圖片描述

5、爬取小說章節內容:

好了,小說各個章節地址我們擷取下來了,接下來就是小說各個章節的內容。

我們用F12看到,章節內容放在了“id”屬性為“contents”的“dd”節點中。

這裡寫圖片描述

這裡我們再用Xpath看一下,鍵入Xpath語句:

Response.xpath('//dd[@id="contents"]').extract()

這裡寫圖片描述

我們看到,小說內容已經讓我們擷取到了!

二、編寫爬蟲:

整個流程上面已經介紹過了,還有一個非常重要的問題:

斷點續傳問題

我們知道,爬蟲不可能一次將全部網站爬取下來,網站的資料量相當龐大,在短時間內不可能完成爬蟲工作,在下一次啟動爬蟲時難道再將已經做過的工作再做一次?當然不行,這樣的爬蟲太不友好。那麼我們如何來解決斷點續傳問題呢?

我這裡的方法是,將已經爬取過的小說每一章的連結存入Mongodb資料庫的一個集合中。在爬蟲工作時首先檢測,要爬取的章節連結是否在這個集合中:

如果在,說明這個章節已經爬取過,不需要再次爬取,跳過;

如果不在,說明這個章節沒有爬取過,則爬取這個章節。爬取完成後,將這個章節連結存入集合中;

如此,我們就完美實現了斷點續傳問題,十分好用。

接下來貼出整個專案程式碼:

註釋我寫的相當詳細,熟悉一下就可以看懂。

items.py

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

# Define here the models for your scraped items
#
# See documentation in:
# https://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class DingdianxiaoshuoItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    #小說名字
    novel_name=scrapy.Field()
    #小說類別
    novel_family=scrapy.Field()
    #小說主頁地址
    novel_url=scrapy.Field()
    #小說作者
    novel_author=scrapy.Field()
    #小說狀態
    novel_status=scrapy.Field()
    #小說字數
    novel_number=scrapy.Field()
    #小說所有章節頁面
    novel_all_section_url= scrapy.Field()
    #小說最後更新時間
    novel_updatetime=scrapy.Field()

    #存放小說的章節地址,程式中存放的是一個列表
    novel_section_urls=scrapy.Field()

    #存放小說的章節地址和小說章節名稱的對應關係,程式中儲存的是一個字典
    section_url_And_section_name=scrapy.Field()

dingdian.py

# -*- coding: utf-8 -*-
import scrapy
from scrapy import Selector
from dingdianxiaoshuo.items import DingdianxiaoshuoItem

class dingdian(scrapy.Spider):
    name="dingdian"
    allowed_domains=["23us.so"]
    start_urls = ['http://www.23us.so/top/allvisit_1.html']
    server_link='http://www.23us.so/top/allvisit_'
    link_last='.html'

    #從start_requests傳送請求
    def start_requests(self):
        yield scrapy.Request(url = self.start_urls[0], callback = self.parse1)


    #獲取總排行榜每個頁面的連結
    def parse1(self, response):
        items=[]
        res = Selector(response)
        #獲取總排行榜小說頁碼數
        max_num=res.xpath('//*[@id="pagestats"]/text()').extract_first()
        max_num=max_num.split('/')[1]
        print("總排行榜最大頁面數為:"+max_num)
        #for i in max_num+1:
        for i in range(0,int(max_num)):
            #構造總排行榜中每個頁面的連結
            page_url=self.server_link+str(i)+self.link_last
            yield scrapy.Request(url=page_url,meta={'items':items},callback=self.parse2)


    #訪問總排行榜的每個頁面
    def parse2(self,response):
        print(response.url)
        items=response.meta['items']
        res=Selector(response)
        #獲得頁面上所有小說主頁連結地址
        novel_urls=res.xpath('//td/a[not(@title)]/@href').extract()
        #獲得頁面上所有小說的名稱
        novel_names=res.xpath('//td/a[not(@title)]/text()').extract()

        page_novel_number=len(novel_urls)
        for index in range(page_novel_number):
            item=DingdianxiaoshuoItem()
            item['novel_name']=novel_names[index]
            item['novel_url'] =novel_urls[index]
            items.append(item)

        for item in items:
            #訪問每個小說主頁,傳遞novel_name
            yield scrapy.Request(url=item['novel_url'],meta = {'item':item},callback = self.parse3)

    #訪問小說主頁,繼續完善item
    def parse3(self, response):
        #接收傳遞的item
        item=response.meta['item']
        #寫入小說類別
        item['novel_family']=response.xpath('//table/tr[1]/td[1]/a/text()').extract_first()
        #寫入小說作者
        item['novel_author']=response.xpath('//table/tr[1]/td[2]/text()').extract_first()
        #寫入小說狀態
        item['novel_status']=response.xpath('//table/tr[1]/td[3]/text()').extract_first()
        #寫入小說最後更新時間
        item['novel_updatetime']=response.xpath('//table/tr[2]/td[3]/text()').extract_first()
        #寫入小說全部章節頁面
        item['novel_all_section_url']=response.xpath('//p[@class="btnlinks"]/a[1]/@href').extract_first()
        url=response.xpath('//p[@class="btnlinks"]/a[@class="read"]/@href').extract_first()
        #訪問顯示有全部章節地址的頁面
        print("即將訪問"+item['novel_name']+"全部章節地址")
        #yield item
        yield  scrapy.Request(url=url,meta={'item':item},callback=self.parse4)

    #將小說所有章節的地址和名稱構造列表存入item
    def parse4(self, response):
        #print("這是parse4")
        #接收傳遞的item
        item=response.meta['item']
        #這裡是一個列表,存放小說所有章節地址
        section_urls=response.xpath('//table/tr/td/a/@href').extract()
        #這裡是一個列表,存放小說所有章節名稱
        section_names=response.xpath('//table/tr/td/a/text()').extract()

        item["novel_section_urls"]=section_urls
        #計數器
        index=0
        #建立雜湊表,儲存章節地址和章節名稱的對應關係
        section_url_And_section_name=dict(zip(section_urls,section_names))
        #將對應關係,寫入item
        item["section_url_And_section_name"]=section_url_And_section_name


        yield item

settings.py

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

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

BOT_NAME = 'dingdianxiaoshuo'

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


# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'dingdianxiaoshuo (+http://www.yourdomain.com)'

# 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 https://doc.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 0.25


#CLOSESPIDER_TIMEOUT = 60 # 後結束爬蟲


# 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 https://doc.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    'dingdianxiaoshuo.middlewares.DingdianxiaoshuoSpiderMiddleware': 543,
#}

# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
#    'dingdianxiaoshuo.middlewares.DingdianxiaoshuoDownloaderMiddleware': 543,
#}

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

# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'dingdianxiaoshuo.pipelines.DingdianxiaoshuoPipeline': 300,
}

# Enable and configure the AutoThrottle extension (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5

pipeline.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

#因為爬取整個網站時間較長,這裡為了實現斷點續傳,我們把每個小說下載完成的
#章節地址存入資料庫一個單獨的集合裡,記錄已完成抓取的小說章節

from pymongo import MongoClient
from urllib import request
from bs4 import BeautifulSoup

#在pipeline中我們將實現下載每個小說,存入MongoDB資料庫

class DingdianxiaoshuoPipeline(object):
    def process_item(self, item, spider):
        #print("馬衍碩")
        #如果獲取章節連結進行如下操作
        if "novel_section_urls" in item:
            # 獲取Mongodb連結
            client = MongoClient("mongodb://127.0.0.1:27017")
            #連線資料庫
            db =client.dingdian
            #獲取小說名稱
            novel_name=item['novel_name']
            #根據小說名字,使用集合,沒有則建立
            novel=db[novel_name]

            #使用記錄已抓取網頁的集合,沒有則建立
            section_url_downloaded_collection=db.section_url_collection

            index=0
            print("正在下載:"+item["novel_name"])


            #根據小說每個章節的地址,下載小說各個章節
            for section_url in item['novel_section_urls']:

                #根據對應關係,找出章節名稱
                section_name=item["section_url_And_section_name"][section_url]
                #如果將要下載的小說章節沒有在section_url_collection集合中,也就是從未下載過,執行下載
                #否則跳過
                if  not section_url_downloaded_collection.find_one({"url":section_url}):
                    #使用urllib庫獲取網頁HTML
                    response = request.Request(url=section_url)
                    download_response = request.urlopen(response)
                    download_html = download_response.read().decode('utf-8')
                    #利用BeautifulSoup對HTML進行處理,擷取小說內容
                    soup_texts = BeautifulSoup(download_html, 'lxml')
                    content=soup_texts.find("dd",attrs={"id":"contents"}).getText()


                    #向Mongodb資料庫插入下載完的小說章節內容
                    novel.insert({"novel_name": item['novel_name'], "novel_family": item['novel_family'],
                                  "novel_author":item['novel_author'], "novel_status":item['novel_status'],
                                  "section_name":section_name,
                                  "content": content})
                    index+=1
                    #下載完成,則將章節地址存入section_url_downloaded_collection集合
                    section_url_downloaded_collection.insert({"url":section_url})


        print("下載完成:"+item['novel_name'])
        return item

三、啟動專案,檢視執行結果:

程式編寫完成後,我們進入專案所在目錄,鍵入命令啟動專案:

scrapy crawl dingdian 

啟動專案後,我們通過Mongodb視覺化工具–RoBo看到,我們成功爬取了小說網站,接下來的問題交給時間。
這裡寫圖片描述

這裡寫圖片描述

當想中斷爬蟲時,直接關掉控制檯。下次開啟爬蟲時將不會重複上次的工作,這就是斷點續傳的美妙之處。(嚴格意義上不會在上次終止的地點開始爬取,但是不會重複已經爬取的工作)

後續將會開闢scrapy系列部落格,專門記錄scrapy架構的爬蟲工作。