scrapy 實戰(zhàn)練習(xí)

前一篇文章介紹了很多關(guān)于scrapy的進階知識,不過說歸說,只有在實際應(yīng)用中才能真正用到這些知識。所以這篇文章就來嘗試利用scrapy爬取各種網(wǎng)站的數(shù)據(jù)。

爬取百思不得姐

首先一步一步來萤捆,我們先從爬最簡單的文本開始。這里爬取的就是百思不得姐的的段子俗批,都是文本俗或。

首先打開段子頁面,用F12工具查看元素岁忘。然后用下面的命令打開scrapy shell辛慰。

scrapy shell http://www.budejie.com/text/

稍加分析即可得到我們要獲取的數(shù)據(jù),在介紹scrapy的第一篇文章中我就寫過一次了干像。這次就給上次那個爬蟲加上一個翻頁功能帅腌。

要獲取的是用戶名和對應(yīng)的段子,所以在items.py中新建一個類麻汰。

class BudejieItem(scrapy.Item):
    username = scrapy.Field()
    content = scrapy.Field()

爬蟲本體就這樣寫速客,唯一需要注意的就是段子可能分為好幾行,這里我們要統(tǒng)一合并成一個大字符串五鲫。選擇器的extract()方法默認會返回一個列表溺职,哪怕數(shù)據(jù)只有一個也是這樣。所以如果數(shù)據(jù)是單個的,使用extract_first()方法浪耘。

import scrapy
from scrapy_sample.items import BudejieItem


class BudejieSpider(scrapy.Spider):
    """百思不得姐段子的爬蟲"""
    name = 'budejie'
    start_urls = ['http://www.budejie.com/text/']
    total_page = 1

    def parse(self, response):
        current_page = int(response.css('a.z-crt::text').extract_first())
        lies = response.css('div.j-r-list >ul >li')
        for li in lies:
            username = li.css('a.u-user-name::text').extract_first()
            content = '\n'.join(li.css('div.j-r-list-c-desc a::text').extract())
            yield BudejieItem(username=username, content=content)
        if current_page < self.total_page:
            yield scrapy.Request(self.start_urls[0] + f'{current_page+1}')

導(dǎo)出到文件

利用scrapy內(nèi)置的Feed功能乱灵,我們可以非常方便的將爬蟲數(shù)據(jù)導(dǎo)出為XML、JSON和CSV等格式的文件七冲。要做的只需要在運行scrapy的時候用-o參數(shù)指定導(dǎo)出文件名即可痛倚。

scrapy crawl budejie -o f.json
scrapy crawl budejie -o f.csv
scrapy crawl budejie -o f.xml

如果出現(xiàn)導(dǎo)出漢字變成Unicode編碼的話,需要在配置中設(shè)置導(dǎo)出編碼癞埠。

FEED_EXPORT_ENCODING = 'utf-8'

保存到MongoDB

有時候爬出來的數(shù)據(jù)并不想放到文件中状原,而是存在數(shù)據(jù)庫中聋呢。這時候就需要編寫管道來處理數(shù)據(jù)了苗踪。一般情況下,爬蟲只管爬取數(shù)據(jù)削锰,數(shù)據(jù)是否重復(fù)是否有效都不是爬蟲要關(guān)心的事情通铲。清洗數(shù)據(jù)、驗證數(shù)據(jù)器贩、保存數(shù)據(jù)這些活颅夺,都應(yīng)該交給管道來處理。當然爬個段子的話蛹稍,肯定是用不到清洗數(shù)據(jù)這些步驟的吧黄。這里用的是pymongo,所以首先需要安裝它唆姐。

pip install pymongo

代碼其實很簡單拗慨,用scrapy官方文檔的例子稍微改一下就行了。由于MongoDB的特性奉芦,所以這部分代碼幾乎是無縫遷移的赵抢,如果希望保存其他數(shù)據(jù),只需要改一下配置就可以了声功,其余代碼部分幾乎不需要更改烦却。

import pymongo


class BudejieMongoPipeline(object):
    "將百思不得姐段子保存到MongoDB中"
    collection_name = 'jokes'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DATABASE', 'budejie')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

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

    def process_item(self, item, spider):
        self.db[self.collection_name].insert_one(dict(item))
        return item

這個管道需要從配置文件中讀取數(shù)據(jù)庫信息,所以還需要在settings.py中增加以下幾行先巴。別忘了在ITEM_PIPELINES中吧我們的管道加進去其爵。

MONGO_URI = 'mongodb://localhost:27017/'
MONGO_DATABASE = 'budejie'

ITEM_PIPELINES = {
    'scrapy.pipelines.images.ImagesPipeline': 1,
    'scrapy_sample.pipelines.BudejieMongoPipeline': 2
}

最后運行一下爬蟲,應(yīng)該就可以看到MongoDB中保存好的數(shù)據(jù)了伸蚯。這里我用的MongoDB客戶端是Studio 3T醋闭,我個人覺得比較好用的一個客戶端。

scrapy crawl budejie
MongoDB截圖

保存到SQL數(shù)據(jù)庫

原來我基本都是用MySQL數(shù)據(jù)庫朝卒,不過重裝系統(tǒng)之后证逻,我選擇了另一個非常流行的開源數(shù)據(jù)庫PostgreSQL。這里就將數(shù)據(jù)保存到PostgreSQL中。不過說起來囚企,SQL數(shù)據(jù)庫確實更加麻煩一些丈咐。MongoDB基本上毫無配置可言,一個數(shù)據(jù)庫龙宏、數(shù)據(jù)集合不需要定義就能直接用棵逊,如果沒有就自動創(chuàng)建。而SQL的表需要我們手動創(chuàng)建才行银酗。

首先需要安裝PostgreSQL的Python驅(qū)動程序辆影。

pip install Psycopg2

然后建立一個數(shù)據(jù)庫test和數(shù)據(jù)表joke。在PostgreSQL中自增主鍵使用SERIAL來設(shè)置黍特。

CREATE TABLE joke (
  id      SERIAL PRIMARY KEY,
  author  VARCHAR(128),
  content TEXT
);

管道基本上一樣蛙讥,只不過將插入數(shù)據(jù)換成了SQL形式的。由于默認情況下需要手動調(diào)用commit()函數(shù)才能提交數(shù)據(jù)灭衷,于是我索性打開了自動提交次慢。

import psycopg2


class BudejiePostgrePipeline(object):
    "將百思不得姐段子保存到PostgreSQL中"

    def __init__(self):
        self.connection = psycopg2.connect("dbname='test' user='postgres' password='12345678'")
        self.connection.autocommit = True

    def open_spider(self, spider):
        self.cursor = self.connection.cursor()

    def close_spider(self, spider):
        self.cursor.close()
        self.connection.close()

    def process_item(self, item, spider):
        self.cursor.execute('insert into joke(author,content) values(%s,%s)', (item['username'], item['content']))
        return item

別忘了將管道加到配置文件中。

ITEM_PIPELINES = {
    'scrapy.pipelines.images.ImagesPipeline': 1,
    'scrapy_sample.pipelines.BudejiePostgrePipeline': 2
}

再次運行爬蟲翔曲,就可以看到數(shù)據(jù)成功的放到PostgreSQL數(shù)據(jù)庫中了迫像。


pyadmin4截圖

以上就是抽取文本數(shù)據(jù)的例子了。雖然我只是簡單的爬了百思不得姐瞳遍,不過這些方法可以應(yīng)用到其他方面闻妓,爬取更多更有用的數(shù)據(jù)。這就需要大家探索了掠械。

爬美女圖片

爬妹子圖網(wǎng)站

說完了抽取文本由缆,下面來看看如何下載圖片。這里以妹子圖為例說明一下份蝴。

首先定義一個圖片Item犁功。scrapy要求圖片Item必須有image_urls和images兩個屬性。另外需要注意這兩個屬性類型都必須是列表婚夫,我就因為沒有將image_urls設(shè)置為列表而卡了好幾個小時浸卦。

class ImageItem(scrapy.Item):
    image_urls = scrapy.Field()
    images = scrapy.Field()

然后照例對網(wǎng)站用F12和scrapy shell這兩樣工具進行測試,找出爬取圖片的方式案糙。這里我只是簡單的爬取一個頁面的上的圖片限嫌,不過只要熟悉了scrapy可以很快的修改成跨越多頁爬取圖片。再次提醒时捌,爬蟲中生成Item的時候切記image_urls屬性是一個列表怒医,就算只有一個URL也得是列表。

import scrapy
from scrapy_sample.items import ImageItem


class MeizituSpider(scrapy.Spider):
    name = 'meizitu'
    start_urls = ['http://www.meizitu.com/a/5501.html']

    def parse(self, response):
        yield ImageItem(image_urls=response.css('div#picture img::attr(src)').extract())

然后在配置文件中添加圖片管道的設(shè)置奢讨,還需要設(shè)置圖片保存位置稚叹,不然scrapy仍然會禁用圖片管道。

ITEM_PIPELINES = {
    'scrapy.pipelines.images.ImagesPipeline': 1,
}
IMAGES_STORE = 'images'

然后運行爬蟲,就可以看到圖片已經(jīng)成功保存到本地了扒袖。

scrapy crawl meizitu
下載好的圖片

重寫圖片管道

從上面的圖中我們可以看到文件名是一堆亂碼字符塞茅,因為默認的圖片管道會將圖片地址做SHA1哈希之后作為文件名。如果我們希望自定義文件名季率,就需要自己繼承圖片管道并重寫file_path方法野瘦。

先將默認的file_path方法貼出來。

    def file_path(self, request, response=None, info=None):

        # check if called from image_key or file_key with url as first argument
        if not isinstance(request, Request):
            url = request
        else:
            url = request.url

        image_guid = hashlib.sha1(to_bytes(url)).hexdigest()  # change to request.url after deprecation
        return 'full/%s.jpg' % (image_guid)

下面是我們的自定義圖片管道飒泻,這里獲取圖片URL的最后一部分作為圖片文件名鞭光,例如對于/123.JPG,就獲取123.jpg作為文件名泞遗。

import scrapy.pipelines.images
from scrapy.http import Request


class RawFilenameImagePipeline(scrapy.pipelines.images.ImagesPipeline):
    def file_path(self, request, response=None, info=None):
        if not isinstance(request, Request):
            url = request
        else:
            url = request.url
        beg = url.rfind('/') + 1
        end = url.rfind('.')
        if end == -1:
            return f'full/{url[beg:]}.jpg'
        else:
            return f'full/{url[beg:end]}.jpg'

如果文件名生成規(guī)則更加復(fù)雜惰许,可以參考znns項目中的pipeline編寫。他這里要根據(jù)路徑生成多級文件夾保存圖片刹孔,所以他的圖片Item需要額外幾個屬性設(shè)置圖片分類等啡省。這時候就需要重寫get_media_requests方法娜睛,從image_urls獲取圖片地址請求的時候用Request的meta屬性將對應(yīng)的圖片Item也傳進去髓霞,這樣在生成文件名的時候就可以讀取meta屬性來確定圖片的分類等信息了。

class ZnnsPipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        for image_url in item['image_urls']:
            yield Request(image_url, meta={'item': item}, headers=headers)
            # 這里把item傳過去畦戒,因為后面需要用item里面的書名和章節(jié)作為文件名

    def item_completed(self, results, item, info):
        image_paths = [x['path'] for ok, x in results if ok]
        if not image_paths:
            raise DropItem("Item contains no images")
        return item

    def file_path(self, request, response=None, info=None):
        item = request.meta['item']
        image_guid = request.url.split('/')[-1]
        filename = u'full/{0[name]}/{0[albumname]}/{1}'.format(item, image_guid)
        return filename

最后要說一點方库,如果不需要使用圖片管道的幾個功能,完全可以改為使用文件管道障斋。因為圖片管道會嘗試將所有圖片都轉(zhuǎn)換成JPG格式的纵潦,你看源代碼的話也會發(fā)現(xiàn)圖片管道中文件名類型直接寫死為JPG的。所以如果想要保存原始類型的圖片垃环,就應(yīng)該使用文件管道邀层。

爬取mm131網(wǎng)站

mm131是另一個圖片網(wǎng)站,為什么我要說這個網(wǎng)站呢遂庄?因為這個網(wǎng)站使用了防盜鏈技術(shù)寥院。對于妹子圖網(wǎng)站來說,由于它沒有防盜鏈功能涛目,所以我們從HTML中獲取的圖片地址就是實際的圖片地址秸谢。但是對于有反盜鏈的網(wǎng)站來說,當你順著圖片URL去下載圖片的時候霹肝,會被重定向到一個無關(guān)的圖片估蹄。因為這個原因,另外瀏覽器有緩存機制導(dǎo)致我直接訪問圖片地址的時候會先返回緩存的圖片沫换,導(dǎo)致我浪費好幾個小時臭蚁。最后我刷新瀏覽器的時候才發(fā)現(xiàn)原來被重定向了。

對于這種情況,需要我們研究怎樣才能訪問到圖片垮兑。使用Scrapy框架時 普通反爬蟲機制的應(yīng)對策略這篇文章列舉了一些常見的策略炭晒。我們要做的就是根據(jù)這些策略進行嘗試。現(xiàn)在我用的是火狐瀏覽器甥角,它的F12工具很好用网严,其中有一個編輯和重發(fā)功能可以方便的幫助我們定位問題。

編輯和重發(fā)
添加Refer
成功獲得圖片

在上面幾張圖中嗤无,我們可以看到直接嘗試訪問圖片會得到302震束,然后被重定向到一個騰訊logo上。但是在添加了Referer之后当犯,成功獲得了圖片垢村。所以問題就是Referer了。這里簡單介紹一下Referer嚎卫,它其實是Referrer的誤拼寫嘉栓。當我們從一個頁面點擊進入另一個頁面時,后者的Referer就是前者拓诸。所以有些網(wǎng)站就利用Referer做判斷侵佃,如果檢測是由另一個網(wǎng)頁進來的,那么正常訪問奠支,如果直接訪問圖片等資源沒有Referer馋辈,就判斷為爬蟲,拒絕請求倍谜。這種情況下的解決辦法也很簡單迈螟,既然網(wǎng)站要Referer,我們手動加上不就行了嗎尔崔。

首先答毫,對于圖片Item,新增一個referer字段季春,用于保存該圖片的Referer洗搂。

class ImageItem(scrapy.Item):
    image_urls = scrapy.Field()
    images = scrapy.Field()
    referer = scrapy.Field()

然后在爬蟲里面,抓取圖片實際地址的時候鹤盒,同時設(shè)置當前網(wǎng)頁作為Referer蚕脏。

import scrapy
from scrapy_sample.items import ImageItem


class Mm131Spider(scrapy.Spider):
    name = 'mm131'

    start_urls = ['http://www.mm131.com/xinggan/3473.html',
                  'http://www.mm131.com/xinggan/2746.html',
                  'http://www.mm131.com/xinggan/3331.html']

    def parse(self, response):
        total_page = int(response.css('span.page-ch::text').extract_first()[1:-1])
        current_page = int(response.css('span.page_now::text').extract_first())
        item = ImageItem()
        item['image_urls'] = response.css('div.content-pic img::attr(src)').extract()
        item['referer'] = response.url
        yield item
        if response.url.rfind('_') == -1:
            head, sep, tail = response.url.rpartition('.')
        else:
            head, sep, tail = response.url.rpartition('_')
        if current_page < total_page:
            yield scrapy.Request(head + f'_{current_page+1}.html')

最后還需要重寫圖片管道的get_media_requests方法。我們先來看看圖片管道基類中是怎么寫的侦锯。self.images_urls_field在這幾行前面設(shè)置的驼鞭,scrapy會嘗試先從配置文件中讀取自定義的圖片URL屬性,獲取不到就使用默認的尺碰。然后在用圖片URL屬性從item中獲取url挣棕,然后傳遞給Request構(gòu)造函數(shù)組裝為一個Request列表译隘,后續(xù)下載器就會用這些請求來下載圖片。

    def get_media_requests(self, item, info):
        return [Request(x) for x in item.get(self.images_urls_field, [])]

恰好我們要做的事情很簡單洛心,就是遍歷一遍這個Request列表固耘,在每個Request上加上Referer請求頭就行了。所以實際上代碼超級簡單词身。我們調(diào)用基類的實現(xiàn)厅目,也就是上面這個,然后遍歷一邊再返回即可法严。

class RefererImagePipeline(ImagesPipeline):
    def get_media_requests(self, item, info):
        requests = super().get_media_requests(item, info)
        for req in requests:
            req.headers.appendlist("referer", item['referer'])
        return requests

最后啟用這個管道损敷。

ITEM_PIPELINES = {
    # 'scrapy.pipelines.images.ImagesPipeline': 1,
    'scrapy_sample.pipelines.RefererImagePipeline': 2
}

運行一下爬蟲,這次可以看到深啤,成功下載到了一堆圖片拗馒。

scrapy crawl mm131

當然你也可以關(guān)掉這個管道,然后運行看看溯街,會發(fā)現(xiàn)終端里一堆重定向錯誤诱桂,無法下載圖片。

下載好的圖片

這僅僅是一個例子呈昔,實際上很多網(wǎng)站可能綜合使用多種技術(shù)來檢測爬蟲挥等,這樣我們的爬蟲也需要多種辦法結(jié)合來反爬蟲。這個網(wǎng)站恰好只使用了Referer韩肝,所以我們只用Referer就能解決触菜。

備份CSDN上所有文章

最后一個例子就來爬取CSDN上所有文章九榔,其實在我的scrapy練習(xí)中很早就有一個簡單的例子哀峻,不過那個是在未登錄的情況下獲取所有文章的名字和鏈接。這里我要做的是登錄CSDN賬號哲泊,然后把所有文章爬下來保存成文件剩蟀,也就是演示一下如何用scrapy模擬登錄過程。

為什么要選擇CSDN呢切威?其實也很簡單育特,因為現(xiàn)在POST明文用戶名和密碼還不需要驗證碼就能登錄的網(wǎng)站真的不多了啊先朦!當然用CSDN的同學(xué)也不用怕缰冤,雖然CSDN傳遞的是明文密碼,但是由于使用了HTTPS喳魏,所以安全性還是可以的棉浸。

翻了翻以前寫的文章,發(fā)現(xiàn)我確實寫過模擬CSDN登錄的文章Python登錄并獲取CSDN博客所有文章列表刺彩,不過運行了一下我發(fā)現(xiàn)CSDN頁面經(jīng)過改版迷郑,有些地方變了枝恋,所以還是需要重新研究一下。需要注意HTTPS傳輸是不會出現(xiàn)在瀏覽器F12工具中的嗡害,只有HTTP傳輸才能在工具中捕獲焚碌。所以這時候需要用Fiddler來研究。

不過實際上我又研究了半天霸妹,發(fā)現(xiàn)其實CSDN登錄過程沒變化十电,我只要把原來寫的一個多余的驗證函數(shù)刪了馬上又可以正常運行了……這里是我原來的CSDN模擬登錄代碼,用BeautifulSoup4和requests寫的叹螟。

又耗費了幾個小時終于把這個爬蟲寫完了摆出,其實編碼過程真的沒費多少時間。主要是由于我對Python語言還是屬于速成的首妖,很多細節(jié)沒掌握偎漫。比方說scrapy如何用回調(diào)方法來分別解析不同頁面、回調(diào)方法如何傳遞數(shù)據(jù)有缆、寫文件的時候沒有檢查目錄是否存在象踊、文件應(yīng)該用什么模式寫入、如何以UTF-8編碼寫文件棚壁、目錄分隔符如何處理等等杯矩,其實都是一些小問題,不過一個一個解決真的廢了我不少事情袖外。

首先史隆,照例定義一個Item,因為我只準備簡單下載文章曼验,所以只需要標題和內(nèi)容兩個屬性即可泌射,標題會作為文件名來使用。

class CsdnBlogItem(scrapy.Item):
    title = scrapy.Field()
    content = scrapy.Field()

然后是爬蟲本體鬓照,這是我目前寫過的最復(fù)雜的一個爬蟲熔酷,確實費了不少時間。這個爬蟲跨越了多個頁面豺裆,還要針對不同頁面解析不同的數(shù)據(jù)拒秘。不過雖然看著復(fù)雜,其實倒是也很簡單臭猜。首先是初始方法躺酒,從命令行獲取CSDN登錄用戶名和密碼,然后存起來備用蔑歌。由于需要用戶登錄羹应,所以parse方法的作用就從解析頁面變成了用戶登錄。具體登錄過程在我原來那篇文章中詳細解釋過了丐膝。這里就是簡單的利用FormRequest.from_response方法將用戶名量愧、密碼以及頁面中的隱藏表單域一起提交钾菊。需要注意的就是callback參數(shù),它表示頁面返回的請求會有另一個方法來處理偎肃。

然后是redirect_to_articles方法煞烫,本來瀏覽器登錄成功的話,會返回一個重定向頁面累颂,瀏覽器會執(zhí)行其中的JS代碼重定向到CSDN頁面滞详。不過我們這是爬蟲,完全沒有執(zhí)行JS代碼的功能紊馏。實際上我們也完全不用在意這個重定向過程料饥,既然登陸成功,有了Cookie朱监,我們想訪問什么頁面都可以岸啡。所以這里同樣直接生成一個新請求訪問文章頁面,然后用callback參數(shù)指定get_all_articles作為回調(diào)赫编。

從get_all_articles方法開始巡蘸,我們就開始解析頁面了。這個方法首先查詢總共有多少頁擂送,而且由于csdn服務(wù)器是REST形式的悦荒,所以我們可以直接將文章頁面基地址和文章頁數(shù)拼起來生成所有的頁面。在這些頁面中嘹吨,每一頁上都有一些文章鏈接搬味,我們點進去就能訪問實際文章了。生成所有頁面的鏈接之后蟀拷,我們同樣設(shè)置回調(diào)碰纬,將這些頁面交給parse_article_links方法處理。

import scrapy
from scrapy import FormRequest
from scrapy import Request
from scrapy_sample.items import CsdnBlogItem


class CsdnBlogBackupSpider(scrapy.Spider):
    name = 'csdn_backup'
    start_urls = ['https://passport.csdn.net/account/login']
    base_url = 'http://write.blog.csdn.net/postlist/'
    get_article_url = 'http://write.blog.csdn.net/mdeditor/getArticle?id='

    def __init__(self, name=None, username=None, password=None, **kwargs):
        super(CsdnBlogBackupSpider, self).__init__(name=name, **kwargs)
        if username is None or password is None:
            raise Exception('沒有用戶名和密碼')
        self.username = username
        self.password = password

    def parse(self, response):
        lt = response.css('form#fm1 input[name="lt"]::attr(value)').extract_first()
        execution = response.css('form#fm1 input[name="execution"]::attr(value)').extract_first()
        eventid = response.css('form#fm1 input[name="_eventId"]::attr(value)').extract_first()
        return FormRequest.from_response(
            response,
            formdata={
                'username': self.username,
                'password': self.password,
                'lt': lt,
                'execution': execution,
                '_eventId': eventid
            },
            callback=self.redirect_to_articles
        )

    def redirect_to_articles(self, response):
        return Request(CsdnBlogBackupSpider.base_url, callback=self.get_all_articles)

    def get_all_articles(self, response):
        import re
        text = response.css('div.page_nav span::text').extract_first()
        total_page = int(re.findall(r'共(\d+)頁', text)[0])
        for i in range(1, total_page + 1):
            yield Request(CsdnBlogBackupSpider.base_url + f'0/0/enabled/{i}', callback=self.parse_article_links)

    def parse_article_links(self, response):
        article_links = response.xpath('//table[@id="lstBox"]/tr[position()>1]/td[1]/a[1]/@href').extract()
        last_index_of = lambda x: x.rfind('/')
        article_ids = [link[last_index_of(link) + 1:] for link in article_links]
        for id in article_ids:
            yield Request(CsdnBlogBackupSpider.get_article_url + id, callback=self.parse_article_content)

    def parse_article_content(self, response):
        import json
        obj = json.loads(response.body, encoding='UTF8')
        yield CsdnBlogItem(title=obj['data']['title'], content=obj['data']['markdowncontent'])

在parse_article_links方法中匹厘,我們獲取每一頁上的所有文章嘀趟,將文章ID抽出來,然后和這個地址'http://write.blog.csdn.net/mdeditor/getArticle?id='拼起來愈诚。這是我編輯CSDN文章的時候從瀏覽器中抓出來的一個地址,它會返回一個JSON字符串牛隅,包含文章標題炕柔、內(nèi)容、Markdown文本等各種信息媒佣。同樣地匕累,我們用parse_article_content回調(diào)函數(shù)來處理這個新請求。

下面就是最后一步了默伍,在parse_article_content方法中做的事情很簡單欢嘿,將JSON字符串轉(zhuǎn)換成Python對象衰琐,然后把我們需要的屬性拿出來。需要交給管道處理的Item對象炼蹦,也是在這最后一步生成羡宙。當然除了用這么多回調(diào)函數(shù)來處理,我們還可以在一個函數(shù)中手動生成請求并處理響應(yīng)掐隐。

這種通過多個回調(diào)函數(shù)來處理請求的方式狗热,在編寫復(fù)雜的爬蟲中是很常見的。例如我們要爬一個美女圖片網(wǎng)站虑省,這個網(wǎng)站中每個美女都有好幾個圖集匿刮,每個圖集有好幾頁,每頁好幾張圖探颈。如果我們希望按照分類和圖集來生成目錄并保存熟丸,那么不僅需要多個回調(diào)函數(shù)來爬取,還需要將圖集伪节、分類等信息跨越多個回調(diào)函數(shù)傳遞給最終生成Item的函數(shù)虑啤。這時候需要利用Request構(gòu)造函數(shù)中的meta屬性,這里是一個例子架馋,具體代碼大家自己看就行了狞山。

最后就是文章保存管道了。這里沒什么技術(shù)難點叉寂,不過讓我這個以前沒弄過這玩意的人來寫萍启,確實費了不少功夫。首先檢測目錄是否存在屏鳍,如果不存在則創(chuàng)建之勘纯。假如目錄不存在的話,open函數(shù)就會失敗钓瞭。然后就是用UTF8編碼保存文章驳遵。

class CsdnBlogBackupPipeline(object):
    def process_item(self, item, spider):
        dirname = 'blogs'
        import os
        import codecs
        if not os.path.exists(dirname):
            os.mkdir(dirname)
        with codecs.open(f'{dirname}{os.sep}{item["title"]}.md', 'w', encoding='utf-8') as f:
            f.write(item['content'])
            f.close()
        return item

最后別忘了在配置文件中啟用管道。

ITEM_PIPELINES = {
    # 'scrapy.pipelines.images.ImagesPipeline': 1,
    'scrapy_sample.pipelines.CsdnBlogBackupPipeline': 2
}

然后運行一下爬蟲山涡,注意這個爬蟲需要接受額外的用戶名和密碼參數(shù)堤结,我們使用-a參數(shù)來指定。

scrapy crawl csdn_backup -a username="用戶名" -a password="密碼"
備份文章成功

這里說一下鸭丛,我現(xiàn)在改為使用簡書來編寫文章竞穷,一來是由于簡書的體驗確實相比來說非常好,在編輯器中可以直接粘貼并自動上傳剪貼板中的圖片鳞溉;二來因為簡書圖片沒有外鏈限制瘾带,所以Markdown文本可以直接復(fù)制到其他網(wǎng)站中,同時維護多個博客非常容易熟菲,如果有同時關(guān)注我CSDN和簡書的同學(xué)也會發(fā)現(xiàn)看政,很多文章我的提交時間基本只差了十幾秒朴恳,這就是復(fù)制粘貼所用的時間。包括剛剛爬下來的文章允蚣,只要在Markdown編輯器中打開于颖,圖片都可以正常訪問。

以上就是我備份CSDN上文章的一個簡單例子厉萝,說它簡單因為真的沒干什么事情恍飘,單純的把文章內(nèi)容爬下來而已,其中的圖片存儲仍然依賴于簡書和其他網(wǎng)站來保存谴垫。有興趣的同學(xué)可以嘗試做更完善的備份功能章母,將每篇文章按目錄保存,文章中的圖片按照各自的目錄下載到本地翩剪,并將Markdown文本中對應(yīng)圖片的地址由服務(wù)器替換為本地路徑乳怎。把這些功能全做完,就是一個真正的文章備份工具了前弯。由于水平所限蚪缀,我就不做了。

這篇文章到這里也該結(jié)束了恕出,雖然只有4個例子询枚,但是我嘗試涵蓋爬蟲的所有應(yīng)用場景、爬取圖片浙巫、爬取文本金蜀、保存到數(shù)據(jù)庫和文件、自定義管道等等的畴。希望這篇文章對大家能有所幫助渊抄!這些代碼全在我的Github上,歡迎關(guān)注丧裁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末护桦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子煎娇,更是在濱河造成了極大的恐慌二庵,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逊桦,死亡現(xiàn)場離奇詭異眨猎,居然都是意外死亡,警方通過查閱死者的電腦和手機强经,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寺渗,“玉大人匿情,你說我怎么就攤上這事兰迫。” “怎么了炬称?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵汁果,是天一觀的道長。 經(jīng)常有香客問我玲躯,道長据德,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任跷车,我火速辦了婚禮棘利,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朽缴。我一直安慰自己善玫,他們只是感情好,可當我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布密强。 她就那樣靜靜地躺著茅郎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪或渤。 梳的紋絲不亂的頭發(fā)上系冗,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天,我揣著相機與錄音薪鹦,去河邊找鬼掌敬。 笑死,一個胖子當著我的面吹牛距芬,可吹牛的內(nèi)容都是我干的涝开。 我是一名探鬼主播,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼框仔,長吁一口氣:“原來是場噩夢啊……” “哼舀武!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起离斩,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤银舱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后跛梗,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寻馏,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年核偿,在試婚紗的時候發(fā)現(xiàn)自己被綠了诚欠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖轰绵,靈堂內(nèi)的尸體忽然破棺而出粉寞,到底是詐尸還是另有隱情,我是刑警寧澤左腔,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布唧垦,位于F島的核電站,受9級特大地震影響液样,放射性物質(zhì)發(fā)生泄漏振亮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一鞭莽、第九天 我趴在偏房一處隱蔽的房頂上張望坊秸。 院中可真熱鬧,春花似錦撮抓、人聲如沸妇斤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工咬像, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留算撮,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓县昂,卻偏偏與公主長得像肮柜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倒彰,可洞房花燭夜當晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,312評論 25 707
  • 序言第1章 Scrapy介紹第2章 理解HTML和XPath第3章 爬蟲基礎(chǔ)第4章 從Scrapy到移動應(yīng)用第5章...
    SeanCheney閱讀 15,097評論 13 61
  • 當周圍的環(huán)境安靜下來的時候待讳,你的心也會隨著安靜芒澜,可以短暫的棲息一下,其實靜下來思考事情是一件很不容易的事情创淡,如今的...
    不桉份嘚小情緒閱讀 601評論 0 0
  • 我以為痴晦,深刻的喜歡定然是具體的痴突,細節(jié)化的印蔬。比如,我喜歡你的才華椭员;我喜歡你目光中隱隱的溫柔;我喜歡你懶洋洋地伏...
    予舍之遷閱讀 188評論 0 0
  • 我的超能力 使我的生活變成了一場災(zāi)難 我走到哪 哪就會混亂 今晨買菜 路過早市 只是因為多看了一眼 嘣~ 垃圾桶爆...
    鐵甕城主閱讀 170評論 8 1