scrapy爬蟲實(shí)例分享
前一篇文章介紹了很多關(guān)于scrapy的進(jìn)階知識(shí),不過(guò)說(shuō)歸說(shuō),只有在實(shí)際應(yīng)用中才能真正用到這些知識(shí)。所以這篇文章就來(lái)嘗試?yán)胹crapy爬取各種網(wǎng)站的數(shù)據(jù)。
爬取百思不得姐
首先一步一步來(lái),我們先從爬最簡(jiǎn)單的文本開始。這里爬取的就是百思不得姐的的段子,都是文本。
首先打開段子頁(yè)面,用F12工具查看元素。然后用下面的命令打開scrapyshell。
scrapy shell http://www.budejie.com/text/
稍加分析即可得到我們要獲取的數(shù)據(jù),在介紹scrapy的第一篇文章中我就寫過(guò)一次了。這次就給上次那個(gè)爬蟲加上一個(gè)翻頁(yè)功能。
要獲取的是用戶名和對(duì)應(yīng)的段子,所以在items.py中新建一個(gè)類。
class BudejieItem(scrapy.Item): username = scrapy.Field() content = scrapy.Field()
爬蟲本體就這樣寫,唯一需要注意的就是段子可能分為好幾行,這里我們要統(tǒng)一合并成一個(gè)大字符串。選擇器的extract()方法默認(rèn)會(huì)返回一個(gè)列表,哪怕數(shù)據(jù)只有一個(gè)也是這樣。所以如果數(shù)據(jù)是單個(gè)的,使用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等格式的文件。要做的只需要在運(yùn)行scrapy的時(shí)候用-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í)候爬出來(lái)的數(shù)據(jù)并不想放到文件中,而是存在數(shù)據(jù)庫(kù)中。這時(shí)候就需要編寫管道來(lái)處理數(shù)據(jù)了。一般情況下,爬蟲只管爬取數(shù)據(jù),數(shù)據(jù)是否重復(fù)是否有效都不是爬蟲要關(guān)心的事情。清洗數(shù)據(jù)、驗(yàn)證數(shù)據(jù)、保存數(shù)據(jù)這些活,都應(yīng)該交給管道來(lái)處理。當(dāng)然爬個(gè)段子的話,肯定是用不到清洗數(shù)據(jù)這些步驟的。這里用的是pymongo,所以首先需要安裝它。
pip install pymongo
代碼其實(shí)很簡(jiǎn)單,用scrapy官方文檔的例子稍微改一下就行了。由于MongoDB的特性,所以這部分代碼幾乎是無(wú)縫遷移的,如果希望保存其他數(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
這個(gè)管道需要從配置文件中讀取數(shù)據(jù)庫(kù)信息,所以還需要在settings.py中增加以下幾行。別忘了在ITEM_PIPELINES中吧我們的管道加進(jìn)去。
MONGO_URI = 'mongodb://localhost:27017/' MONGO_DATABASE = 'budejie' ITEM_PIPELINES = { 'scrapy.pipelines.images.ImagesPipeline': 1, 'scrapy_sample.pipelines.BudejieMongoPipeline': 2 }
最后運(yùn)行一下爬蟲,應(yīng)該就可以看到MongoDB中保存好的數(shù)據(jù)了。這里我用的MongoDB客戶端是Studio 3T,我個(gè)人覺得比較好用的一個(gè)客戶端。
scrapy crawl budejie
保存到SQL數(shù)據(jù)庫(kù)
原來(lái)我基本都是用MySQL數(shù)據(jù)庫(kù),不過(guò)重裝系統(tǒng)之后,我選擇了另一個(gè)非常流行的開源數(shù)據(jù)庫(kù)PostgreSQL。這里就將數(shù)據(jù)保存到PostgreSQL中。不過(guò)說(shuō)起來(lái),SQL數(shù)據(jù)庫(kù)確實(shí)更加麻煩一些。MongoDB基本上毫無(wú)配置可言,一個(gè)數(shù)據(jù)庫(kù)、數(shù)據(jù)集合不需要定義就能直接用,如果沒(méi)有就自動(dòng)創(chuàng)建。而SQL的表需要我們手動(dòng)創(chuàng)建才行。
首先需要安裝PostgreSQL的Python驅(qū)動(dòng)程序。
pip install Psycopg2
然后建立一個(gè)數(shù)據(jù)庫(kù)test和數(shù)據(jù)表joke。在PostgreSQL中自增主鍵使用SERIAL來(lái)設(shè)置。
CREATE TABLE joke ( id SERIAL PRIMARY KEY, author VARCHAR(128), content TEXT );
管道基本上一樣,只不過(guò)將插入數(shù)據(jù)換成了SQL形式的。由于默認(rèn)情況下需要手動(dòng)調(diào)用commit()函數(shù)才能提交數(shù)據(jù),于是我索性打開了自動(dòng)提交。
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 }
再次運(yùn)行爬蟲,就可以看到數(shù)據(jù)成功的放到PostgreSQL數(shù)據(jù)庫(kù)中了。
以上就是抽取文本數(shù)據(jù)的例子了。雖然我只是簡(jiǎn)單的爬了百思不得姐,不過(guò)這些方法可以應(yīng)用到其他方面,爬取更多更有用的數(shù)據(jù)。這就需要大家探索了。
爬美女圖片
爬妹子圖網(wǎng)站
說(shuō)完了抽取文本,下面來(lái)看看如何下載圖片。這里以妹子圖為例說(shuō)明一下。
首先定義一個(gè)圖片Item。scrapy要求圖片Item必須有image_urls和images兩個(gè)屬性。另外需要注意這兩個(gè)屬性類型都必須是列表,我就因?yàn)闆](méi)有將image_urls設(shè)置為列表而卡了好幾個(gè)小時(shí)。
class ImageItem(scrapy.Item): image_urls = scrapy.Field() images = scrapy.Field()
然后照例對(duì)網(wǎng)站用F12和scrapy shell這兩樣工具進(jìn)行測(cè)試,找出爬取圖片的方式。這里我只是簡(jiǎn)單的爬取一個(gè)頁(yè)面的上的圖片,不過(guò)只要熟悉了scrapy可以很快的修改成跨越多頁(yè)爬取圖片。再次提醒,爬蟲中生成Item的時(shí)候切記image_urls屬性是一個(gè)列表,就算只有一個(gè)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仍然會(huì)禁用圖片管道。
ITEM_PIPELINES = { 'scrapy.pipelines.images.ImagesPipeline': 1, } IMAGES_STORE = 'images'
然后運(yùn)行爬蟲,就可以看到圖片已經(jīng)成功保存到本地了。
scrapy crawl meizitu
重寫圖片管道
從上面的圖中我們可以看到文件名是一堆亂碼字符,因?yàn)槟J(rèn)的圖片管道會(huì)將圖片地址做SHA1哈希之后作為文件名。如果我們希望自定義文件名,就需要自己繼承圖片管道并重寫file_path方法。
先將默認(rèn)的file_path方法貼出來(lái)。
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的最后一部分作為圖片文件名,例如對(duì)于/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項(xiàng)目中的pipeline編寫。他這里要根據(jù)路徑生成多級(jí)文件夾保存圖片,所以他的圖片Item需要額外幾個(gè)屬性設(shè)置圖片分類等。這時(shí)候就需要重寫get_media_requests方法,從image_urls獲取圖片地址請(qǐng)求的時(shí)候用Request的meta屬性將對(duì)應(yīng)的圖片Item也傳進(jìn)去,這樣在生成文件名的時(shí)候就可以讀取meta屬性來(lái)確定圖片的分類等信息了。
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傳過(guò)去,因?yàn)楹竺嫘枰胕tem里面的書名和章節(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
最后要說(shuō)一點(diǎn),如果不需要使用圖片管道的幾個(gè)功能,完全可以改為使用文件管道。因?yàn)閳D片管道會(huì)嘗試將所有圖片都轉(zhuǎn)換成JPG格式的,你看源代碼的話也會(huì)發(fā)現(xiàn)圖片管道中文件名類型直接寫死為JPG的。所以如果想要保存原始類型的圖片,就應(yīng)該使用文件管道。
爬取mm131網(wǎng)站
mm131是另一個(gè)圖片網(wǎng)站,為什么我要說(shuō)這個(gè)網(wǎng)站呢?因?yàn)檫@個(gè)網(wǎng)站使用了防盜鏈技術(shù)。對(duì)于妹子圖網(wǎng)站來(lái)說(shuō),由于它沒(méi)有防盜鏈功能,所以我們從HTML中獲取的圖片地址就是實(shí)際的圖片地址。但是對(duì)于有反盜鏈的網(wǎng)站來(lái)說(shuō),當(dāng)你順著圖片URL去下載圖片的時(shí)候,會(huì)被重定向到一個(gè)無(wú)關(guān)的圖片。因?yàn)檫@個(gè)原因,另外瀏覽器有緩存機(jī)制導(dǎo)致我直接訪問(wèn)圖片地址的時(shí)候會(huì)先返回緩存的圖片,導(dǎo)致我浪費(fèi)好幾個(gè)小時(shí)。最后我刷新瀏覽器的時(shí)候才發(fā)現(xiàn)原來(lái)被重定向了。
對(duì)于這種情況,需要我們研究怎樣才能訪問(wèn)到圖片。使用Scrapy框架時(shí) 普通反爬蟲機(jī)制的應(yīng)對(duì)策略這篇文章列舉了一些常見的策略。我們要做的就是根據(jù)這些策略進(jìn)行嘗試?,F(xiàn)在我用的是火狐瀏覽器,它的F12工具很好用,其中有一個(gè)編輯和重發(fā)功能可以方便的幫助我們定位問(wèn)題。
在上面幾張圖中,我們可以看到直接嘗試訪問(wèn)圖片會(huì)得到302,然后被重定向到一個(gè)騰訊logo上。但是在添加了Referer之后,成功獲得了圖片。所以問(wèn)題就是Referer了。這里簡(jiǎn)單介紹一下Referer,它其實(shí)是Referrer的誤拼寫。當(dāng)我們從一個(gè)頁(yè)面點(diǎn)擊進(jìn)入另一個(gè)頁(yè)面時(shí),后者的Referer就是前者。所以有些網(wǎng)站就利用Referer做判斷,如果檢測(cè)是由另一個(gè)網(wǎng)頁(yè)進(jìn)來(lái)的,那么正常訪問(wèn),如果直接訪問(wèn)圖片等資源沒(méi)有Referer,就判斷為爬蟲,拒絕請(qǐng)求。這種情況下的解決辦法也很簡(jiǎn)單,既然網(wǎng)站要Referer,我們手動(dòng)加上不就行了嗎。
首先,對(duì)于圖片Item,新增一個(gè)referer字段,用于保存該圖片的Referer。
class ImageItem(scrapy.Item): image_urls = scrapy.Field() images = scrapy.Field() referer = scrapy.Field()
然后在爬蟲里面,抓取圖片實(shí)際地址的時(shí)候,同時(shí)設(shè)置當(dāng)前網(wǎng)頁(yè)作為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方法。我們先來(lái)看看圖片管道基類中是怎么寫的。self.images_urls_field在這幾行前面設(shè)置的,scrapy會(huì)嘗試先從配置文件中讀取自定義的圖片URL屬性,獲取不到就使用默認(rèn)的。然后在用圖片URL屬性從item中獲取url,然后傳遞給Request構(gòu)造函數(shù)組裝為一個(gè)Request列表,后續(xù)下載器就會(huì)用這些請(qǐng)求來(lái)下載圖片。
def get_media_requests(self, item, info): return [Request(x) for x in item.get(self.images_urls_field, [])]
恰好我們要做的事情很簡(jiǎn)單,就是遍歷一遍這個(gè)Request列表,在每個(gè)Request上加上Referer請(qǐng)求頭就行了。所以實(shí)際上代碼超級(jí)簡(jiǎn)單。我們調(diào)用基類的實(shí)現(xiàn),也就是上面這個(gè),然后遍歷一邊再返回即可。
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
最后啟用這個(gè)管道。
ITEM_PIPELINES = { # 'scrapy.pipelines.images.ImagesPipeline': 1, 'scrapy_sample.pipelines.RefererImagePipeline': 2 }
運(yùn)行一下爬蟲,這次可以看到,成功下載到了一堆圖片。
scrapy crawl mm131
當(dāng)然你也可以關(guān)掉這個(gè)管道,然后運(yùn)行看看,會(huì)發(fā)現(xiàn)終端里一堆重定向錯(cuò)誤,無(wú)法下載圖片。
這僅僅是一個(gè)例子,實(shí)際上很多網(wǎng)站可能綜合使用多種技術(shù)來(lái)檢測(cè)爬蟲,這樣我們的爬蟲也需要多種辦法結(jié)合來(lái)反爬蟲。這個(gè)網(wǎng)站恰好只使用了Referer,所以我們只用Referer就能解決。
備份CSDN上所有文章
最后一個(gè)例子就來(lái)爬取CSDN上所有文章,其實(shí)在我的scrapy練習(xí)中很早就有一個(gè)簡(jiǎn)單的例子,不過(guò)那個(gè)是在未登錄的情況下獲取所有文章的名字和鏈接。這里我要做的是登錄CSDN賬號(hào),然后把所有文章爬下來(lái)保存成文件,也就是演示一下如何用scrapy模擬登錄過(guò)程。
為什么要選擇CSDN呢?其實(shí)也很簡(jiǎn)單,因?yàn)楝F(xiàn)在POST明文用戶名和密碼還不需要驗(yàn)證碼就能登錄的網(wǎng)站真的不多了??!當(dāng)然用CSDN的同學(xué)也不用怕,雖然CSDN傳遞的是明文密碼,但是由于使用了HTTPS,所以安全性還是可以的。
翻了翻以前寫的文章,發(fā)現(xiàn)我確實(shí)寫過(guò)模擬CSDN登錄的文章Python登錄并獲取CSDN博客所有文章列表,不過(guò)運(yùn)行了一下我發(fā)現(xiàn)CSDN頁(yè)面經(jīng)過(guò)改版,有些地方變了,所以還是需要重新研究一下。需要注意HTTPS傳輸是不會(huì)出現(xiàn)在瀏覽器F12工具中的,只有HTTP傳輸才能在工具中捕獲。所以這時(shí)候需要用Fiddler來(lái)研究。
不過(guò)實(shí)際上我又研究了半天,發(fā)現(xiàn)其實(shí)CSDN登錄過(guò)程沒(méi)變化,我只要把原來(lái)寫的一個(gè)多余的驗(yàn)證函數(shù)刪了馬上又可以正常運(yùn)行了……這里是我原來(lái)的CSDN模擬登錄代碼,用BeautifulSoup4和requests寫的。
又耗費(fèi)了幾個(gè)小時(shí)終于把這個(gè)爬蟲寫完了,其實(shí)編碼過(guò)程真的沒(méi)費(fèi)多少時(shí)間。主要是由于我對(duì)Python語(yǔ)言還是屬于速成的,很多細(xì)節(jié)沒(méi)掌握。比方說(shuō)scrapy如何用回調(diào)方法來(lái)分別解析不同頁(yè)面、回調(diào)方法如何傳遞數(shù)據(jù)、寫文件的時(shí)候沒(méi)有檢查目錄是否存在、文件應(yīng)該用什么模式寫入、如何以UTF-8編碼寫文件、目錄分隔符如何處理等等,其實(shí)都是一些小問(wèn)題,不過(guò)一個(gè)一個(gè)解決真的廢了我不少事情。
首先,照例定義一個(gè)Item,因?yàn)槲抑粶?zhǔn)備簡(jiǎn)單下載文章,所以只需要標(biāo)題和內(nèi)容兩個(gè)屬性即可,標(biāo)題會(huì)作為文件名來(lái)使用。
class CsdnBlogItem(scrapy.Item): title = scrapy.Field() content = scrapy.Field()
然后是爬蟲本體,這是我目前寫過(guò)的最復(fù)雜的一個(gè)爬蟲,確實(shí)費(fèi)了不少時(shí)間。這個(gè)爬蟲跨越了多個(gè)頁(yè)面,還要針對(duì)不同頁(yè)面解析不同的數(shù)據(jù)。不過(guò)雖然看著復(fù)雜,其實(shí)倒是也很簡(jiǎn)單。首先是初始方法,從命令行獲取CSDN登錄用戶名和密碼,然后存起來(lái)備用。由于需要用戶登錄,所以parse方法的作用就從解析頁(yè)面變成了用戶登錄。具體登錄過(guò)程在我原來(lái)那篇文章中詳細(xì)解釋過(guò)了。這里就是簡(jiǎn)單的利用FormRequest.from_response方法將用戶名、密碼以及頁(yè)面中的隱藏表單域一起提交。需要注意的就是callback參數(shù),它表示頁(yè)面返回的請(qǐng)求會(huì)有另一個(gè)方法來(lái)處理。
然后是redirect_to_articles方法,本來(lái)瀏覽器登錄成功的話,會(huì)返回一個(gè)重定向頁(yè)面,瀏覽器會(huì)執(zhí)行其中的JS代碼重定向到CSDN頁(yè)面。不過(guò)我們這是爬蟲,完全沒(méi)有執(zhí)行JS代碼的功能。實(shí)際上我們也完全不用在意這個(gè)重定向過(guò)程,既然登陸成功,有了Cookie,我們想訪問(wèn)什么頁(yè)面都可以。所以這里同樣直接生成一個(gè)新請(qǐng)求訪問(wèn)文章頁(yè)面,然后用callback參數(shù)指定get_all_articles作為回調(diào)。
從get_all_articles方法開始,我們就開始解析頁(yè)面了。這個(gè)方法首先查詢總共有多少頁(yè),而且由于csdn服務(wù)器是REST形式的,所以我們可以直接將文章頁(yè)面基地址和文章頁(yè)數(shù)拼起來(lái)生成所有的頁(yè)面。在這些頁(yè)面中,每一頁(yè)上都有一些文章鏈接,我們點(diǎn)進(jìn)去就能訪問(wèn)實(shí)際文章了。生成所有頁(yè)面的鏈接之后,我們同樣設(shè)置回調(diào),將這些頁(yè)面交給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('沒(méi)有用戶名和密碼') 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+)頁(yè)', 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方法中,我們獲取每一頁(yè)上的所有文章,將文章ID抽出來(lái),然后和這個(gè)地址'http://write.blog.csdn.net/mdeditor/getArticle?id='拼起來(lái)。這是我編輯CSDN文章的時(shí)候從瀏覽器中抓出來(lái)的一個(gè)地址,它會(huì)返回一個(gè)JSON字符串,包含文章標(biāo)題、內(nèi)容、Markdown文本等各種信息。同樣地,我們用parse_article_content回調(diào)函數(shù)來(lái)處理這個(gè)新請(qǐng)求。
下面就是最后一步了,在parse_article_content方法中做的事情很簡(jiǎn)單,將JSON字符串轉(zhuǎn)換成Python對(duì)象,然后把我們需要的屬性拿出來(lái)。需要交給管道處理的Item對(duì)象,也是在這最后一步生成。當(dāng)然除了用這么多回調(diào)函數(shù)來(lái)處理,我們還可以在一個(gè)函數(shù)中手動(dòng)生成請(qǐng)求并處理響應(yīng)。
這種通過(guò)多個(gè)回調(diào)函數(shù)來(lái)處理請(qǐng)求的方式,在編寫復(fù)雜的爬蟲中是很常見的。例如我們要爬一個(gè)美女圖片網(wǎng)站,這個(gè)網(wǎng)站中每個(gè)美女都有好幾個(gè)圖集,每個(gè)圖集有好幾頁(yè),每頁(yè)好幾張圖。如果我們希望按照分類和圖集來(lái)生成目錄并保存,那么不僅需要多個(gè)回調(diào)函數(shù)來(lái)爬取,還需要將圖集、分類等信息跨越多個(gè)回調(diào)函數(shù)傳遞給最終生成Item的函數(shù)。這時(shí)候需要利用Request構(gòu)造函數(shù)中的meta屬性,這里是一個(gè)例子,具體代碼大家自己看就行了。
最后就是文章保存管道了。這里沒(méi)什么技術(shù)難點(diǎn),不過(guò)讓我這個(gè)以前沒(méi)弄過(guò)這玩意的人來(lái)寫,確實(shí)費(fèi)了不少功夫。首先檢測(cè)目錄是否存在,如果不存在則創(chuàng)建之。假如目錄不存在的話,open函數(shù)就會(huì)失敗。然后就是用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 }
然后運(yùn)行一下爬蟲,注意這個(gè)爬蟲需要接受額外的用戶名和密碼參數(shù),我們使用-a參數(shù)來(lái)指定。
scrapy crawl csdn_backup -a username="用戶名" -a password="密碼"
這里說(shuō)一下,我現(xiàn)在改為使用簡(jiǎn)書來(lái)編寫文章,一來(lái)是由于簡(jiǎn)書的體驗(yàn)確實(shí)相比來(lái)說(shuō)非常好,在編輯器中可以直接粘貼并自動(dòng)上傳剪貼板中的圖片;二來(lái)因?yàn)楹?jiǎn)書圖片沒(méi)有外鏈限制,所以Markdown文本可以直接復(fù)制到其他網(wǎng)站中,同時(shí)維護(hù)多個(gè)博客非常容易,如果有同時(shí)關(guān)注我CSDN和簡(jiǎn)書的同學(xué)也會(huì)發(fā)現(xiàn),很多文章我的提交時(shí)間基本只差了十幾秒,這就是復(fù)制粘貼所用的時(shí)間。包括剛剛爬下來(lái)的文章,只要在Markdown編輯器中打開,圖片都可以正常訪問(wèn)。
以上就是我備份CSDN上文章的一個(gè)簡(jiǎn)單例子,說(shuō)它簡(jiǎn)單因?yàn)檎娴臎](méi)干什么事情,單純的把文章內(nèi)容爬下來(lái)而已,其中的圖片存儲(chǔ)仍然依賴于簡(jiǎn)書和其他網(wǎng)站來(lái)保存。有興趣的同學(xué)可以嘗試做更完善的備份功能,將每篇文章按目錄保存,文章中的圖片按照各自的目錄下載到本地,并將Markdown文本中對(duì)應(yīng)圖片的地址由服務(wù)器替換為本地路徑。把這些功能全做完,就是一個(gè)真正的文章備份工具了。由于水平所限,我就不做了。
總結(jié)
這篇文章到這里也該結(jié)束了,雖然只有4個(gè)例子,但是我嘗試涵蓋爬蟲的所有應(yīng)用場(chǎng)景、爬取圖片、爬取文本、保存到數(shù)據(jù)庫(kù)和文件、自定義管道等等。
以上就是本文關(guān)于scrapy爬蟲實(shí)例分享的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!
相關(guān)文章
python實(shí)現(xiàn)自動(dòng)發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了python實(shí)現(xiàn)自動(dòng)發(fā)送郵件功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06python創(chuàng)建字典(dict)的幾種方法小結(jié)(含代碼示例)
字典(Dictionary)是Python中一種非常靈活的數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)鍵值對(duì)(key-value pairs),在Python中創(chuàng)建字典有多種方法,每種方法都有其特定的使用場(chǎng)景和優(yōu)勢(shì),本文將詳細(xì)介紹Python中創(chuàng)建字典的幾種常見方法,需要的朋友可以參考下2024-09-09Python+OpenCV圖像處理——實(shí)現(xiàn)輪廓發(fā)現(xiàn)
這篇文章主要介紹了Python+OpenCV實(shí)現(xiàn)輪廓發(fā)現(xiàn),幫助大家更好的利用python處理圖片,感興趣的朋友可以了解下2020-10-10pytorch中retain_graph==True的作用說(shuō)明
這篇文章主要介紹了pytorch中retain_graph==True的作用說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02python實(shí)現(xiàn)求兩個(gè)字符串的最長(zhǎng)公共子串方法
今天小編就為大家分享一篇python實(shí)現(xiàn)求兩個(gè)字符串的最長(zhǎng)公共子串方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07Python中unittest模塊做UT(單元測(cè)試)使用實(shí)例
這篇文章主要介紹了Python中unittest模塊做UT(單元測(cè)試)使用實(shí)例,本文直接給出待測(cè)試的類、測(cè)試類和測(cè)試結(jié)果以及測(cè)試總結(jié),需要的朋友可以參考下2015-06-06python用裝飾器自動(dòng)注冊(cè)Tornado路由詳解
這篇文章主要給大家介紹了python用裝飾器自動(dòng)注冊(cè)Tornado路由,文中給出了三個(gè)版本的解決方法,有需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-02-02TensorFlow Session使用的兩種方法小結(jié)
今天小編就為大家分享一篇TensorFlow Session使用的兩種方法小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07