天天看點

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

标題中的英文首字母大寫比較規範,但在python實際使用中均為小寫。

爬取伯樂線上網站所有文章的詳情頁面

1.網頁持久化

1.1 建立爬蟲工程

建立爬蟲工程指令:scrapy startproject BoleSave2

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

image.png

進入爬蟲工程目錄指令:cd BoleSave2

建立爬蟲檔案指令:scrapy genspider save blog.jobbole.com

1.2 編輯save.py檔案

網頁持久化隻需要編輯爬蟲檔案就可以,下面是save.py檔案的代碼。

第13行dirName變量的值可以設定網頁檔案儲存的位置,例如:

dirName = "d:/saveWebPage"将網頁檔案儲存在D盤的saveWebPage檔案夾中。

可以根據個人情況進行修改,不能将其設定為工程所在檔案夾,因為Pycharm對工程内大量新檔案進行索引會導緻卡頓。

import scrapy
import os
import re

def reFind(pattern,sourceStr,nth=1):
    if len(re.findall(pattern,sourceStr)) >= nth:
        return re.findall(pattern,sourceStr)[nth-1]
    else:
        return 1

def saveWebPage(response,id,prefix):
    # 持久化目錄頁面
    dirName = "d:/saveWebPage2"
    if not os.path.isdir(dirName):
        os.mkdir(dirName)
    html = response.text
    fileName = "%s%05d.html" %(prefix,id)
    filePath = "%s/%s" %(dirName, fileName)
    with open(filePath, 'w', encoding="utf-8") as file:
        file.write(html)
        print("網頁持久化儲存為%s檔案夾中的%s檔案" %(dirName,fileName))

class SaveSpider(scrapy.Spider):
    name = 'save'
    allowed_domains = ['blog.jobbole.com']
    start_urls = ['http://blog.jobbole.com/all-posts/']

    def parse(self, response):
        pageNum = response.xpath("//a[@class='page-numbers']/text()")[-1].extract()
        for i in range(1, int(pageNum) + 1):
            url = "http://blog.jobbole.com/all-posts/page/{}/".format(i)
            yield scrapy.Request(url, callback=self.parse1)

    def parse1(self, response):
        page_id = int(reFind("\d+", response.url))
        saveWebPage(response,page_id,'directory')
        #獲得詳情頁面的連結,并調用下一級解析函數
        article_list = response.xpath("//div[@class='post floated-thumb']")
        count = 0
        for article in article_list:
            url = article.xpath("div[@class='post-meta']/p/a[1]/@href").extract_first()
            count += 1
            article_id = (page_id - 1) * 20 + count
            yield scrapy.Request(url,self.parse2,meta={'id':article_id})

    def parse2(self, response):
        saveWebPage(response,response.meta['id'],'detail')
           

1.3 編輯settings.py檔案

改變并發請求數量,取消變量CONCURRENT_REQUESTS的注釋,并改變值為96。

CONCURRENT_REQUESTS = 96

1.4 運作結果

運作指令:scrapy crawl save

559個目錄頁面,11172個詳情頁面,兩種頁面相加共有11731個頁面。

而網頁持久化儲存的檔案個數也是11731個,說明已經完成頁面持久化。

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

從下圖中可以看出開始時間與結束時間相差12分鐘,則11731個頁面持久化耗時12分鐘。

持久化速度:977頁/分,16.29頁/秒

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

2.解析伯樂線上文章詳情頁面

已經把11731個網頁檔案打包成一個壓縮檔案,下載下傳連結:

https://pan.baidu.com/s/19MDHdwrqrSRTEgVWA9fMzg

密碼: x7nk

2.1 建立爬蟲工程

建立爬蟲工程指令:scrapy startproject BoleParse2

進入爬蟲工程目錄指令:cd BoleParse2

建立爬蟲檔案指令:scrapy genspider parse blog.jobbole.com

2.2 在Pycharm中導入工程

導入工程的按鈕位置如下圖所示:

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

選中工程檔案夾,然後點選OK,如下圖所示:

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

工程檔案夾的結構如下圖所示:

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

2.3 編寫items.py檔案

共有12個字段,文章識别碼id、标題title、釋出時間publishTime、分類category、摘要digest、圖檔連結imgUrl、詳情連結detailUrl、原文出處originalSource、内容content、點贊數favourNumber、收藏數collectNumber、評論數commentNumber。

import scrapy
from scrapy import Field

class Boleparse2Item(scrapy.Item):
    id = Field()
    title = Field()
    publishTime = Field()
    category = Field()
    digest = Field()
    imgUrl = Field()
    detailUrl = Field()
    originalSource = Field()
    content = Field()
    favourNumber = Field()
    collectNumber = Field()
    commentNumber = Field()
           

2.4 編寫parse.py檔案

parse函數解析目錄頁面,得到7個字段的值添加進item中,并通過response攜帶meta傳遞給下一級解析函數。

parse2函數解析詳情頁面,通過item = response.meta['item']得到已經解析一部分内容的item,再對網頁解析得到剩餘的5個字段,最後yield item将item傳給管道進行處理。

注意:修改第13行變量dirName的值

import scrapy
import re
from ..items import Boleparse2Item

def reFind(pattern,sourceStr,nth=1):
    if len(re.findall(pattern,sourceStr)) >= nth:
        return re.findall(pattern,sourceStr)[nth-1]
    else:
        return ''

class ArticleSpider(scrapy.Spider):
    name = 'parse'
    dirName = "E:/saveWebPage2"
    start_urls = []
    for i in range(1,560):
        fileName = "directory%05d.html" %i
        filePath = "file:///%s/%s" %(dirName,fileName)
        start_urls.append(filePath)

    def parse(self, response):
        def find(xpath, pNode=response):
            if len(pNode.xpath(xpath)):
                return pNode.xpath(xpath).extract()[0]
            else:
                return ''
        article_list = response.xpath("//div[@class='post floated-thumb']")
        pattern = self.dirName + "/directory(\d+).html"
        page_id_str = reFind(pattern,response.url)
        page_id = int(page_id_str)
        count = 0
        for article in article_list:
            count += 1
            item = Boleparse2Item()
            item['id'] = (page_id - 1) * 20 + count
            item['title'] = find("div[@class='post-meta']/p[1]/a/@title",article)
            pTagStr = find("div[@class='post-meta']/p",article)
            item['publishTime'] = re.search("\d+/\d+/\d+",pTagStr).group(0)
            item['category'] = find("div[@class='post-meta']/p/a[2]/text()",article)
            item['digest'] = find("div[@class='post-meta']/span/p/text()",article)
            item['imgUrl'] = find("div[@class='post-thumb']/a/img/@src",article)
            item['detailUrl'] = find("div[@class='post-meta']/p/a[1]/@href", article)
            fileName = "detail%05d.html" %item['id']
            nextUrl = "file:///%s/%s" %(self.dirName,fileName)
            yield scrapy.Request(nextUrl,callback=self.parse1,meta={'item':item})

    def parse1(self, response):
        def find(xpath, pNode=response):
            if len(pNode.xpath(xpath)):
                return pNode.xpath(xpath).extract()[0]
            else:
                return ''
        item = response.meta['item']
        item['originalSource'] = find("//div[@class='copyright-area']"
                                      "/a[@target='_blank']/@href")
        item['content'] = find("//div[@class='entry']")
        item['favourNumber'] = find("//h10/text()")
        item['collectNumber'] = find("//div[@class='post-adds']"\
                    "/span[2]/text()").strip("收藏").strip()
        commentStr = find("//a[@href='#article-comment']/span")
        item['commentNumber'] = reFind("(\d+)\s評論",commentStr)
        yield item
           

2.5 編寫pipelines.py檔案

采用資料庫連接配接池提高往資料庫中插入資料的效率。

下面代碼有2個地方要修改:1.資料庫名;2.連接配接資料庫的密碼。

設定資料庫編碼方式,default charset=utf8mb4建立表預設編碼為utf8mb4,因為插入字元可能是4個位元組編碼。

item['content'] = my_b64encode(item['content'])将網頁内容進行base64編碼防止發生異常。

from twisted.enterprise import adbapi
import pymysql
import time
import os
import base64

def my_b64encode(content):
    byteStr = content.encode("utf-8")
    encodeStr = base64.b64encode(byteStr)
    return encodeStr.decode("utf-8")

class Boleparse2Pipeline(object):
    def __init__(self):
        self.params = dict(
            dbapiName='pymysql',
            cursorclass=pymysql.cursors.DictCursor,
            host='localhost',
            db='bole',
            user='root',
            passwd='...your password',
            charset='utf8mb4',
        )
        self.tableName = "article_details"
        self.dbpool = adbapi.ConnectionPool(**self.params)
        self.startTime = time.time()
        self.dbpool.runInteraction(self.createTable)

    def createTable(self, cursor):
        drop_sql = "drop table if exists %s" %self.tableName
        cursor.execute(drop_sql)
        create_sql = "create table %s(id int primary key," \
                     "title varchar(200),publishtime varchar(30)," \
                     "category varchar(30),digest text," \
                     "imgUrl varchar(200),detailUrl varchar(200)," \
                     "originalSource varchar(500),content mediumtext," \
                     "favourNumber varchar(20)," \
                     "collectNumber varchar(20)," \
                     "commentNumber varchar(20)) " \
                     "default charset = utf8mb4" %self.tableName
        cursor.execute(create_sql)
        self.dbpool.connect().commit()

    def process_item(self, item, spider):
        self.dbpool.runInteraction(self.insert, item)
        return item

    def insert(self, cursor, item):
        try:
            if len(item['imgUrl']) >= 200:
                item.pop('imgUrl')
            item['content'] = my_b64encode(item['content'])
            fieldStr = ','.join(['`%s`' % k for k in item.keys()])
            valuesStr = ','.join(['"%s"' % v for v in item.values()])
            insert_sql = "insert into %s(%s) values(%s)"\
                         % (self.tableName,fieldStr, valuesStr)
            cursor.execute(insert_sql)
            print("往mysql資料庫中插入第%d條資料成功" %item['id'])
        except Exception as e:
            if not os.path.isdir("Log"):
                os.mkdir("Log")
            filePath = "Log/" + time.strftime('%Y-%m-%d-%H-%M.log')
            with open(filePath, 'a+') as file:
                datetime = time.strftime('%Y-%m-%d %H:%M:%S')
                logStr = "%s log:插入第%d條資料發生異常\nreason:%s\n"
                file.write(logStr % (datetime, item['id'], str(e)))

    def close_spider(self, spider):
        print("程式總共運作%.2f秒" % (time.time() - self.startTime))
           

2.6 編寫settings.py檔案

BOT_NAME = 'BoleParse2'
SPIDER_MODULES = ['BoleParse2.spiders']
NEWSPIDER_MODULE = 'BoleParse2.spiders'
ROBOTSTXT_OBEY = False
CONCURRENT_REQUESTS = 96
CONCURRENT_ITEMS = 200
ITEM_PIPELINES = {
   'BoleParse2.pipelines.Boleparse2Pipeline': 300,
}
           

2.7 運作結果

運作指令:scrapy crawl parse

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

從上圖可以看出,插入資料總共需要花費420秒,即25條/秒,1558條/分。

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值

從上圖可以看出插入資料總共使用硬碟容量679.5M,條數共11172條,成功插入每一條資料。

3.查找插入異常原因

mysql中檢視字元集指令:show variables like "character%"

基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值
content中有組合字元\"導緻發生SQL syntax error
           
基于Scrapy爬取伯樂線上網站(進階版)注意:修改第13行變量dirName的值