1. 程式人生 > >初識Scrapy之再續火影情緣

初識Scrapy之再續火影情緣

復雜 功能 splay 註意 回調函數 正則 理解 turn bject

前言
Scrapy框架之初窺門徑
1 Scrapy簡介
2 Scrapy安裝
3 Scrapy基礎
31 創建項目
32 Shell分析
4 Scrapy程序編寫
41 Spiders程序測試
42 Items編寫
43 Settings編寫
44 Comic_spider編寫
45 Pipelines編寫
運行結果
總結
1 前言

如果有人問我,你最喜歡的動漫是什麽?我會毫不猶豫地告訴他:《火影忍者》。因為,這是唯一的一部貫穿我小學、初中、高中、大學、研究生生活的動漫。小學五年級的時候,家裏的電視安裝了機頂盒,那時候的動漫頻道還不是清一色的《天線寶寶》、《熊出沒》這樣的國產動漫。大部分都是日本動漫,《火影忍者》、《海賊王》、《浪客劍心》這樣的熱血動漫充斥著整個動漫頻道。就從那時開始,我走上了追《火影忍者》的道路。雖然,這是一個暴露年齡的事情,可是我還是想說,我也算是一個資深的火影迷了。鳴人的火之意誌、鳴人和佐助的羈絆的故事,看得我熱血沸騰。初中的時候,我還曾傻傻地學習忍術的結印手勢,以為只要學會了結印手勢就能放出忍術,現在想想,真的是無憂無慮的童年啊!可能,有朋友會問,《火影忍者》不是已經完結了嗎?《火影忍者》是完結了,但是鳴人兒子的故事才剛剛開始,《博人傳之火影忍者新時代》正在熱播中。因此,我又開始追動漫了,雖然現在不會像兒時那樣激動到上躥下跳,但是我依然喜歡看,現在感覺,繼續看火影,更多的是一種情懷吧!


今天的閑話有點多,就此打住,回歸正題。為了了解動漫的進展,看相應的漫畫是個不錯的選擇。而KuKu動漫又是免費的試看平臺,滿足我的需求。奉上URL:http://comic.kukudm.com/


可以看到,這個網站的第一個推薦動漫就是《火影忍者》。這個網站不提供下載功能,但是又很想收藏怎麽辦?那就用分布式爬蟲Scrapy搞下來吧!當然,在此之前,不得不說的一句話就是:請勿將程序用於任何商業用途,僅供交流學習。尊重著作權,請購買正版漫畫。

2 Scrapy框架之初窺門徑

2.1 Scrapy簡介

Scrapy Engine(Scrapy核心) 負責數據流在各個組件之間的流。Spiders(爬蟲)發出Requests請求,經由Scrapy Engine(Scrapy核心) 交給Scheduler(調度器),Downloader(下載器)Scheduler(調度器) 獲得Requests請求,然後根據Requests請求,從網絡下載數據。Downloader(下載器)的Responses響應再傳遞給Spiders進行分析。根據需求提取出Items,交給Item Pipeline進行下載。Spiders和Item Pipeline是需要用戶根據響應的需求進行編寫的。除此之外,還有兩個中間件,Downloaders Mddlewares和Spider Middlewares,這兩個中間件為用戶提供方面,通過插入自定義代碼擴展Scrapy的功能,例如去重等。因為中間件屬於高級應用,本次教程不涉及,因此不做過多講解。


2.2 Scrapy安裝

關於Scrapy的安裝,可以查看我之前的筆記:www.yongshiiyule.cn/ /c406495762/article/details/60156205

2.3 Scrapy基礎

安裝好Scrapy之後,我們就可以開啟我們的Scrapy之旅了。官方的詳細中文教程,請參見:www.caihongyule2017.cn o/zh_CN/0.24/intro/tutorial.html 。我這裏只講本次實戰用到的知識。

簡單流程如下:

創建一個Scrapy項目;
定義提取的Item;
編寫爬取網站的 spider 並提取 Item;
編寫 Item Pipeline 來存儲提取到的Item(即數據)。
2.3.1 創建項目

在開始爬取之前,我們必須創建一個新的Scrapy項目。 進入打算存儲代碼的目錄中,運行下列命令:

scrapy startproject cartoon
1
1
scrapy startproject是固定命令,後面的cartoon是自己想起的工程名字。這裏,我起名為cartoon(漫畫)。

該命令將會創建包含下列內容的cartoon目錄:

cartoon/
scrapy.cfg
cartoon/
__init__.py
items.py
middlewares.http://www.xyseo.net/ py
pipelines.py
settings.py
spiders/
__init__.py
這些文件分別是:

scrapy.cfg: 項目的配置文件;
cartoon/: 該項目的python模塊。之後將在此加入Spider代碼;
cartoon/items.py: 項目中的item文件;
cartoon/middlewares .py:項目中的中間件;
cartoon/pipelines.py: 項目中的pipelines文件;
cartoon/settings.py: 項目的設置文件;
cartoon/spiders/: 放置spider代碼的目錄。
2.3.2 Shell分析

在編寫程序之前,我們可以使用Scrapy內置的Scrapy shell,分析下目標網頁,為後編寫梳理思路。先分析下《火影忍者》主界面:

scrapy shell "http://comic.kukudm.com/comiclist/3/"
1
1

在Scrapy shell中,我們可以通過如下指令打印網頁的body信息:

response.body
1
1
通過返回的內容,我們可以尋找自己想要的鏈接,但是這種方法,顯然有些麻煩,因為內容太多,不好找。這裏,我們還是使用審查元素的方式進行分析:


可以看到,每個章節的鏈接和名字都存放在了dd標簽下的a標簽中。在shell中輸入如下指令提取鏈接:

response.xpath(‘//dd/www.xuancayule.com/ a[1]‘)
1
1
xpath之前講過了,如果忘記了,可翻閱我之前整理的筆記。從輸出結果可以看到,每個鏈接都已經提取出來了,但是沒有顯示a標簽裏面的內容。


想要顯示全,就需要extract()方法,轉換成字符串輸出,指令如下:

response.xpath(‘//dd/a[1]‘).extract()
1
1
從運行結果可以看出,這樣就顯示完全了。現在開始思考一個問題,如果我想保存每個章節的圖片,需要哪些東西?鏈接必不可少,當然還有每個章節的名字,我們要以文件夾的形式存儲每個章節,文件夾的命名就是章節的名字,這樣更規整。


我們使用text()獲取每個章節的名字,指令如下:

response.xpath(‘//dd/a[1]/text()‘).extract()
1
1
瞧,每個章節的名字被我們輕松的提取出來了,記住這個指令,在編寫程序的時候,需要用到。


獲取完章節名字,接下來就是獲取鏈接了,使用指令如下:

response.xpath(‘//dd/a[1][email protected]).extract()
1
1
Scrapy還是蠻好用的嘛~省去了使用Beautifulsoup這些工具的使用。當然,它的強大不僅僅於此,讓我們慢慢道來。


《火影忍者》首頁分析完了。接下來,我們分析每個章節裏的內容,看看如何獲取每個圖片的鏈接。還是使用審查元素的方式,我們可以看到,這個網頁提供的信息如下。再思考一個問題,從這個網頁我們要獲取哪些信息?第一個當然還是圖片的鏈接,第二個呢?將一個章節裏的每個圖片保存下來,我們如何命名圖片?用默認名字下載下來的圖片,順序也就亂了。仔細一點的話,不難發現,第一頁的鏈接為:http://comic.kukudm.com/comiclist/3/3/1.htm,第二頁的鏈接為:http://comic.kukudm.com/comiclist/3/3/2.htm,第三頁的鏈接為:http://comic.kukudm.com/comiclist/3/3/3.htm 依此類推,所以我們可以根據這個規律進行翻頁,而為了翻頁,首先需要獲取的就是每個章節的圖片數,也就是頁數,隨後,我們根據每頁的地址就可以為每個圖片命名:第1頁、第2頁、第3頁…,這樣命名就可以了。不會出現亂序,並且很工整,方便我們閱讀。由於有的章節圖片的鏈接不是規律的,所以只能先獲取頁面地址,再獲取圖片地址,這樣遞進爬取。


使用ctrl+c退出之前的shell,分析章節頁面,以第一章為例,使用指令如下:

scrapy shell "http://comic.kukudm.com/comiclist/3/1.htm"
1
1
套路已經想好,那就開始測試吧。通過審查元素可以知道,頁數存放在valign屬性i為top的td標簽中。獲取的內容由於有好多信息,我們再使用re()方法,通過正則表達式獲取頁數。獲取頁數代碼如下:

response.xpath(‘//td[@valign="top"]/text()‘).re(‘共(\d+)頁‘)[0]
1
1
可以看到,通過幾次測試就把頁數提取出來了。最終的這個指令頁要記住,編寫程序需要用到。


圖片頁獲取完了,下面該獲取圖片的鏈接了,通過審查元素我們會發現,圖片鏈接保存再img標簽下的src屬性中,理想狀態,使用如下指令就可以獲取圖片鏈接:

response.xpath(‘//img[@id="comipic"][email protected]).extract()
1
1
但是你會發現,返回為空。這是為什麽?通過response.body打印信息不難發現,這個鏈接是使用JS動態加載進去的。直接獲取是不行的,網頁分為靜態頁面和動態頁面,對於靜態頁面好說,對於動態頁面就復雜一些了。可以使用PhantomJS、發送JS請求、使用Selenium、運行JS腳本等方式獲取動態加載的內容。(該網站動態加載方式簡單,不涉及這些,後續教程會講解其他動態加載方法)


該網站是使用如下指令加載圖片的:

document.write("<img src=‘"+server+"comic/kuku2comic/Naruto/01/01_01.JPG‘><span style=‘display:none‘><img src=‘"+server+"comic/kuku2comic/Naruto/01/01_02.JPG‘></span>");
1
1
JS腳本放在網頁裏,沒有使用外部JS腳本,這就更好辦了,直接獲取腳本信息,不就能獲取圖片鏈接了?使用指令如下:

response.xpath(‘//scrpit/text()‘).extract()
1
1
通過運行結果可以看出,我們已經獲取到了圖片鏈接,server的值是通過運行JS外部腳本獲得的,但是這裏,我們仔細觀察server的值為http://n.1whour.com/,其他頁面也是一樣,因此也就簡化了流程。同樣,記住這個指令,編寫程序的時候會用到。


就這樣這個思路已經梳理清楚,需要的內容有章節鏈接、章節名、圖片鏈接、每張頁數。shell分析完畢,接下來開始編寫程序。

2.4 Scrapy程序編寫

2.4.1 Spiders程序測試

在cortoon/spiders目錄下創建文件comic_spider.py,編寫內容如下:

# -*- coding:UTF-8 -*-
import scrapy

class ComicSpider(scrapy.Spider):

name = "comic"
allowed_domains = [‘comic.kukudm.com‘]
start_urls = [‘http://comic.kukudm.com/comiclist/3/‘]

def parse(self, response):
link_urls = response.xpath(‘//dd/a[1][email protected]).extract()
for each_link in link_urls:
print(‘http://comic.kukudm.com‘ + each_link)
name:自己定義的內容,在運行工程的時候需要用到的標識;
allowed_domains:允許爬蟲訪問的域名,防止爬蟲跑飛。讓爬蟲只在指定域名下進行爬取,值得註意的一點是,這個域名需要放到列表裏;
start_urls:開始爬取的url,同樣這個url鏈接也需要放在列表裏;
def parse(self, response) :請求分析的回調函數,如果不定義start_requests(self),獲得的請求直接從這個函數分析;
parse函數中的內容,就是之前我們獲取鏈接的解析內容,在cmd中使用如下指令運行工程:

scrapy crawl comic
1
1
打印輸出了這個章節的鏈接:


再打印章節名字看看,代碼如下:

# -*- coding:UTF-8 -*-
import scrapy

# -*- coding:UTF-8 -*-
import scrapy

class ComicSpider(scrapy.Spider):

name = "comic"
allowed_domains = [‘comic.kukudm.com‘]
start_urls = [‘http://comic.kukudm.com/comiclist/3/‘]

def parse(self, response):
# link_urls = response.xpath(‘//dd/a[1][email protected]).extract()
dir_names = response.xpath(‘//dd/a[1]/text()‘).extract()
for each_name in dir_names:
print(each_name)

章節名字打印成功!


2.4.2 Items編寫

剛剛進行了簡單的測試,了解下Spiders的編寫。現在開始進入正題,按步驟編寫爬蟲。第一步,填寫items.py,內容如下:

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

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

import scrapy

class ComicItem(scrapy.Item):
dir_name = scrapy.Field()
link_url = scrapy.Field()
img_url = scrapy.Field()
image_paths = scrapy.Field()dir_name:文件名,也就是章節名;
link_url:每個章節的每一頁的鏈接,根據這個鏈接保存圖片名;
img_url:圖片鏈接;
image_paths:圖片保存路徑。
2.4.3 Settings編寫

填寫settings.py,內容如下:

BOT_NAME = ‘cartoon‘

SPIDER_MODULES = [‘cartoon.spiders‘]
NEWSPIDER_MODULE = ‘cartoon.spiders‘


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

# Obey robots.txt rules
ROBOTSTXT_OBEY = Flase

ITEM_PIPELINES = {
‘cartoon.pipelines.ComicImgDownloadPipeline‘: 1,
}

IMAGES_STORE = ‘J:/火影忍者‘

COOKIES_ENABLED = False

DOWNLOAD_DELAY = 0.25 # 250 ms of delay

BOT_NAME:自動生成的內容,根名字;
SPIDER_MODULES:自動生成的內容;
NEWSPIDER_MODULE:自動生成的內容;
ROBOTSTXT_OBEY:自動生成的內容,是否遵守robots.txt規則,這裏選擇不遵守;
ITEM_PIPELINES:定義item的pipeline;
IMAGES_STORE:圖片存儲的根路徑;
COOKIES_ENABLED:Cookie使能,這裏禁止Cookie;
DOWNLOAD_DELAY:下載延時,這裏使用250ms延時。
2.4.4 Comic_spider編寫

在comic_spider.py文件中,編寫代碼如下,代碼進行了詳細的註釋:

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

import re
import scrapy
from scrapy import Selector
from cartoon.items import ComicItem

class ComicSpider(scrapy.Spider):
name = ‘comic‘

def __init__(self):
#圖片鏈接server域名
self.server_img = ‘http://n.1whour.com/‘
#章節鏈接server域名
self.server_link = ‘http://comic.kukudm.com‘
self.allowed_domains = [‘comic.kukudm.com‘]
self.start_urls = [‘http://comic.kukudm.com/comiclist/3/‘]
#匹配圖片地址的正則表達式
self.pattern_img = re.compile(r‘\+"(.+)\‘><span‘)

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

#解析response,獲得章節圖片鏈接地址
def parse1(self, response):
hxs = Selector(response)
items = []
#章節鏈接地址
urls = hxs.xpath(‘//dd/a[1][email protected]).extract()
#章節名
dir_names = hxs.xpath(‘//dd/a[1]/text()‘).extract()
#保存章節鏈接和章節名
for index in range(len(urls)):
item = ComicItem()
item[‘link_url‘] = self.server_link + urls[index]
item[‘dir_name‘] = dir_names[index]
items.append(item)

#根據每個章節的鏈接,發送Request請求,並傳遞item參數
for item in items[-13:-1]:
yield scrapy.Request(url = item[‘link_url‘], meta = {‘item‘:item}, callback = self.parse2)

#解析獲得章節第一頁的頁碼數和圖片鏈接
def parse2(self, response):
#接收傳遞的item
item = response.meta[‘item‘]
#獲取章節的第一頁的鏈接
item[‘link_url‘] = response.url
hxs = Selector(response)
#獲取章節的第一頁的圖片鏈接
pre_img_url = hxs.xpath(‘//script/text()‘).extract()
#註意這裏返回的圖片地址,應該為列表,否則會報錯
img_url = [self.server_img + re.findall(self.pattern_img, pre_img_url[0])[0]]
#將獲取的章節的第一頁的圖片鏈接保存到img_url中
item[‘img_url‘] = img_url
#返回item,交給item pipeline下載圖片
yield item
#獲取章節的頁數
page_num = hxs.xpath(‘//td[@valign="top"]/text()‘).re(u‘共(\d+)頁‘)[0]
#根據頁數,整理出本章節其他頁碼的鏈接
pre_link = item[‘link_url‘][:-5]
for each_link in range(2, int(page_num) + 1):
new_link = pre_link + str(each_link) + ‘.htm‘
#根據本章節其他頁碼的鏈接發送Request請求,用於解析其他頁碼的圖片鏈接,並傳遞item
yield scrapy.Request(url = new_link, meta = {‘item‘:item}, callback = self.parse3)

#解析獲得本章節其他頁面的圖片鏈接
def parse3(self, response):
#接收傳遞的item
item = response.meta[‘item‘]
#獲取該頁面的鏈接
item[‘link_url‘] = response.url
hxs = Selector(response)
pre_img_url = hxs.xpath(‘//script/text()‘).extract()
#註意這裏返回的圖片地址,應該為列表,否則會報錯
img_url = [self.server_img + re.findall(self.pattern_img, pre_img_url[0])[0]]
#將獲取的圖片鏈接保存到img_url中
item[‘img_url‘] = img_url
#返回item,交給item pipeline下載圖片
yield item
代碼看上去可能不好理解,自己動手嘗試一下,一步一步來,最終你就會找到答案的。這部分代碼不能一步一步講解,思路已經講完,其他的就靠自己嘗試與體悟了。關於python的yield,簡單地講,yield 的作用就是把一個函數變成一個 generator,帶有 yield 的函數不再是一個普通函數,Python 解釋器會將其視為一個 generator。想要保持代碼的整潔,又要想獲得 iterable 的效果,就可以使用yield了,這部分內容,可以查看廖雪峰老師的教程。

2.4.5 Pipelines編寫

pipelines.py主要負責圖片的下載,我們根據item保存的信息,進行圖片的分類保存,代碼如下:

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

# Define your item pipelines here
#
# Don‘t forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
from cartoon import settings
from scrapy import Request
import requests
import os


class ComicImgDownloadPipeline(object):

def process_item(self, item, spider):
#如果獲取了圖片鏈接,進行如下操作
if ‘img_url‘ in item:
images = []
#文件夾名字
dir_path = ‘%s/%s‘ % (settings.IMAGES_STORE, item[‘dir_name‘])
#文件夾不存在則創建文件夾
if not os.path.exists(dir_path):
os.makedirs(dir_path)
#獲取每一個圖片鏈接
for image_url in item[‘img_url‘]:
#解析鏈接,根據鏈接為圖片命名
houzhui = image_url.split(‘/‘)[-1].split(‘.‘)[-1]
qianzhui = item[‘link_url‘].split(‘/‘)[-1].split(‘.‘)[0]
#圖片名
image_file_name = ‘第‘ + qianzhui + ‘頁.‘ + houzhui
#圖片保存路徑
file_path = ‘%s/%s‘ % (dir_path, image_file_name)
images.append(file_path)
if os.path.exists(file_path):
continue
#保存圖片
with open(file_path, ‘wb‘) as handle:
response = requests.get(url = image_url)
for block in response.iter_content(1024):
if not block:
break
handle.write(block)
#返回圖片保存路徑
item[‘image_paths‘] = images
return item
代碼依舊進行了註釋,自己動手嘗試吧!

3 運行結果

由於工程文件較多,我將我的整體代碼上傳到了我的Github,歡迎Follow、Star。URL:https://github.com/Jack-Cherish/python-spider/tree/master/cartoon

我下載了後面火影忍者博人傳的內容,也可以用代碼整個小說爬取。效果如下所示:

初識Scrapy之再續火影情緣