您好,登錄后才能下訂單哦!
講師博客:https://www.cnblogs.com/wupeiqi/p/6229292.html
中文資料(有示例參考):http://www.scrapyd.cn/doc/
Scrapy是一個為了爬取網站數據,提取結構性數據而編寫的應用框架。使用之前有一個類似django的創建項目以及目錄結構的過程。
使用pip安裝(windows會有問題):
pip3 install scrapy
裝不上主要是因為依賴的模塊Twisted安裝不上,所以得先安裝Twisted,并且不能用pip直接下載安裝。先去下載Twisted的whl安裝文件:https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
然后使用pip本地安裝:
pip install E:\Downloads\Twisted-18.9.0-cp36-cp36m-win_amd64.whl
pip install -i https://mirrors.aliyun.com/pypi/simple/ scrapy
pip install -i https://mirrors.aliyun.com/pypi/simple/ pywin32
Scrapy主要包括了以下組件:
工作流程:
綠線是數據流向,引擎是整個程序的入口。首先從初始 URL 開始(這步大概是引擎把初始URL加到調度器),Scheduler 會將其交給 Downloader 進行下載,下載之后會交給 Spider 進行分析,Spider 分析出來的結果有兩種:一種是需要進一步抓取的鏈接,例如“下一頁”的鏈接,這些東西會被傳回 Scheduler ;另一種是需要保存的數據,它們則被送到 Item Pipeline 那里,那是對數據進行后期處理(詳細分析、過濾、存儲等)的地方。
另外,引擎和其他3個組件直接有通道。在數據流動的通道里還可以安裝各種中間件,進行必要的處理。
啟動項目
打開終端進入想要存儲 Scrapy 項目的目錄,然后運行 scrapy startproject (project name)。創建一個項目:
> scrapy startproject PeppaScrapy
執行完成后,會生成如下的文件結構:
ProjectName/
├── ProjectName
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ └── __init__.py
└── scrapy.cfg
文件說明
關于配置文件,需要的時候可以先去下面的地址查,版本不是最新的,不過是中文。
https://www.jianshu.com/p/df9c0d1e9087
創建爬蟲應用
先切換到項目目錄,在執行grnspider命令 scrapy genspider [-t template] (name) (domain) 。比如:
> cd PeppaScrapy
> scrapy genspider spider_lab lab.scrapyd.cn
效果就是在spiders目錄下,創建了一個spider_lab.py的文件。這里沒有用-t參數指定模板,就是用默認模板創建的。其實不用命令也行了,自己建空文件,然后自己寫也是一樣的。
可以使用-l參數,查看有哪些模板:
> scrapy genspider -l
Available templates:
basic
crawl
csvfeed
xmlfeed
然后再用-d參數,加上上面查到的模板名,查看模板的內容:
> scrapy genspider -d basic
# -*- coding: utf-8 -*-
import scrapy
class $classname(scrapy.Spider):
name = '$name'
allowed_domains = ['$domain']
start_urls = ['http://$domain/']
def parse(self, response):
pass
把之前的創建的應用的文件修改一下,簡單完善一下parse方法:
import scrapy
class SpiderLabSpider(scrapy.Spider):
name = 'spider_lab'
allowed_domains = ['lab.scrapyd.cn']
start_urls = ['http://lab.scrapyd.cn/']
def parse(self, response):
print(response.url)
print(response.body.decode())
查看應用列表:
> scrapy list
spider_lab
運行單獨爬蟲應用,這里加上了--nolog參數,避免打印日志的干擾:
> scrapy crawl spider_lab --nolog
每次都去命令行打一遍命令也很麻煩,也是可以直接寫python代碼,執行python來啟動的。把下面的代碼加到引用文件的最后:
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
其實就是提供了在python里調用命令行執行命令的方法。之后,還可以寫一個main.py放到項目根目錄下,寫上啟動整個項目的命令。
有可能會遇到編碼問題,不過我的windows沒問題,如果遇到了,試一下下面的方法:
import io
import sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
Robots協議就是每個網站對于來到的爬蟲所提出的要求。并非強制要求遵守的協議,只是一種建議。
默認scrapy遵守robot協議。我在爬 http://dig.chouti.com/ 的時候遇到了這個問題。把 --nolog 參數去掉,查看錯誤日志,有如下的信息:
[scrapy.core.engine] DEBUG: Crawled (200) <GET http://dig.chouti.com/robots.txt> (referer: None)
[scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt: <GET http://dig.chouti.com/>
先去下載robots.txt文件,然后根據文件的建議,就禁止繼續爬取了。可以直接瀏覽器輸入連接查看文件內容:
User-agent: *
Allow: /link/
Disallow: /?
Disallow: /*?
Disallow: /user
Disallow: /link/*/comments
Disallow: /admin/login
# Sitemap files
Sitemap: https://dig.chouti.com/sitemap.xml
你要守規矩的的話,就只能爬 https://dig.chouti.com/link/xxxxxxxx
這樣的url,一個帖子一個帖子爬下來。
如果可以選擇不遵守協議,那么就在爬的時候把這個設置設為False。全局的設置在settings.py文件里:
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
也可以只對一個應用修改設置:
import scrapy
class SpiderLabSpider(scrapy.Spider):
name = 'chouti'
allowed_domains = ['chouti.com']
start_urls = ['http://dig.chouti.com/']
custom_settings = {'ROBOTSTXT_OBEY': False}
def parse(self, response):
print(response.url)
print(response.encoding)
print(response.text)
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
上面踩坑的過程中,一度以為是請求頭有問題,已定義請求頭的方法也是設置settings.py文件,里面有一個剩下的默認配置:
# 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',
#}
默認都注釋掉了,你可以在這里為全局加上自定義的請求頭,當然也可以只為單獨的應用配置:
import scrapy
class SpiderLabSpider(scrapy.Spider):
name = 'test'
allowed_domains = ['chouti.cn']
start_urls = ['http://dig.chouti.com/']
# 這個網站會屏蔽User-Agent里包含python的請求
custom_settings = {'ROBOTSTXT_OBEY': False,
'DEFAULT_REQUEST_HEADERS': {'User-Agent': 'python'},
}
def parse(self, response):
print(response.request.headers) # 這個是請求頭
print(response.headers) # 這個是響應頭
if __name__ == '__main__':
from scrapy import cmdline
log_level = ''
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
使用xpaht選擇器可以提取數據,另外還有CSS選擇器也可以用。
XPath 是一門在 XML 文檔中查找信息的語言。XPath 可用來在 XML 文檔中對元素和屬性進行遍歷。對 XPath 的理解是很多高級 XML 應用的基礎。
解析頁面內容會用到Selector這個類,下面只貼出parse回調函數里的代碼:
from scrapy.selector import Selector
def parse(self, response):
title1 = response.xpath('//title')
print('title1', title1)
title2 = Selector(response).xpath('//title')
print('title2', title2)
上面的兩種用法是一樣的,通過response對象也可以直接調用xpath方法。這里說明了xpath方法是Selector這個類提供的。另外用方法二還有一個好處,就是因為之后需要調用Selector類里的方法,這樣顯示的聲明Selector類之后,編輯器可以找到類似的方法,給出各種提示。直接用response調用,就沒有這種便利了。
另外還有一個XmlXPathSelector類,作用和Selector類差不多,可能是就版本使用的類。
常用的表達式:
提取屬性
提取屬性的話,也是先定位到標簽的范圍,然后最后@屬性名稱,拿到所有對應的屬性。另外@*可以拿到所有的屬性。要當某個標簽下的屬性,就在標簽名之后/@就好了:
Selector(response).xpath('//@href') # 提取所有的href屬性
Selector(response).xpath('//ol[@class="page-navigator"]//@href') # ol.page-navigator下的所有的href屬性
Selector(response).xpath('//head/meta/@*').extract() # head>meta 標簽了所有的屬性
Selector(response).xpath('//*[@id="body"]/div/@class') # id為body的標簽的下一級標簽里的class屬性
查找標簽,限定屬性
使用這樣的表達式:標簽[@屬性名='屬性值'] ,另外還能用not(),注意要用小括號把取反的內容包起來:
Selector(response).xpath('//div[@id="body"]//span[@class="text"]') # 只要 span.text 的span標簽
Selector(response).xpath('//div[@id="body"]//span[not(@class="text")]') # 沒有text這個class的span標簽
Selector(response).xpath('//meta[@name]') # 有name屬性的meta
Selector(response).xpath('//meta[not(@name)]') # 沒有name屬性meta
提取值
xpath方法返回的是個對象,這個對象還可以無限次的再調用xpath方法。拿到最終的對象之后,我們需要獲取值,這里有 extract() 和 extract_first() 這兩個方法。因為查找的結果可能是多個值,extract方法返回列表,而extract_first方法直接返回值,但是是列表是第一個元素的值。
提取文字
表達式:/text() 可以把文字提取出來:
def parse(self, response):
tags = Selector(response).xpath('//ul[@class="tags-list"]//a/text()').extract()
print(tags) # 這樣打印效果不是很好
for tag in tags:
print(tag.strip())
還有個方法,可以提取整段文字拼到一起。表達式:string() :
Selector(response).xpath('string(//ul[@class="tags-list"]//a)').extract() # 這樣沒拿全
Selector(response).xpath('string(//ul[@class="tags-list"])').extract() # 這樣才拿全了
上面第一次沒拿全,某個a標簽下的文字就是一段。string()表達式看來值接收一個值,如果傳的是個列表,可能就只操作第一個元素。
在我們商品詳情、小說內容的時候可能會比較好用。
匹配class的問題
xpath中沒有提供對class的原生查找方法。因為class里是可以包含多個值的。比如下面的這個標簽:
<div class="test main">Test</div>
下面的表達式是無法匹配到的:
response.xpath('//div[@class="test"]')
要匹配到,你得寫死:
response.xpath('//div[@class="test main"]')
但是這樣顯然是不能接受的,如果還有其他test但是沒出main的標簽就匹配不上了。
contains 函數 (XPath),檢查第一個參數字符串是否包含第二個參數字符串。用這個函數就能做好了
response.xpath('//div[contains(@class, "test")]')
這樣又有新問題了,如果有別的class名字比如:test1、mytest,這種也都會被上面的方法匹配上。
concat 函數 (XPath),返回參數的串聯。就是字符串拼接,contains的兩個參數的兩邊都加上空格,就能解決上面的問題。之所以要引入concat函數時因為,后面的字符串可以手動在兩邊加上空格,但是@class是變量,這個也不能用加號,就要用這個函數做拼接:
response.xpath('//div[contains(concat(" ", @class, " "), " test ")]')
normalize-space 函數 (XPath),返回去掉了前導、尾隨和重復的空白的參數字符串。上面已經沒問題了。不過還不夠完美。在拼接@class之前,先把兩邊可能會出現的其他空白字符給去掉,可能會有某些操作需要改變一下class,但是又不要對這個class有任何實際的影響。總之這個是最終的解決方案:
response.xpath('//div[contains(concat(" ", normalize-space(@class), " "), " test ")]')
這里已經引出了好幾個函數了,還有更多別的函數,需要的時候再查吧。
正則匹配
xpath也是可以用正則匹配的,用法很簡單 re:test(x, y)
。第一個參數用@屬性比較多,否則就是正則匹配標簽了,就和純的正則匹配似乎沒什么差別了。
Selector(response=response).xpath('//a[re:test(@id, "i\d+")]')
https://www.cnblogs.com/tina-cherish/p/7127812.html
xpath很強大,但是不支持原生的class,不過上面已經給了比較嚴謹的解決方案了。
css有部分功能無法實現。比如不能向上找,只能匹配當前層級,要通過判斷子元素來確定當前元素是否匹配就不行。這種情況使用xpath的話,中括號里可以在嵌套中括號的。
不過css感覺更直觀,也已經沒什么學習成本了。
登錄抽屜并點贊。邊一步一步實現,邊補充用到的知識點。
import scrapy
from scrapy.selector import Selector
class SpiderLabSpider(scrapy.Spider):
name = 'chouti'
allowed_domains = ['chouti.com']
start_urls = ['http://dig.chouti.com/']
custom_settings = {'ROBOTSTXT_OBEY': False}
def parse(self, response):
items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
for item in items:
news = item.xpath(
'./div[@class="news-content"]'
'//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
'/text()'
).extract()[-1]
print(news.strip())
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
這里爬取的只是首頁的內容
現在要獲取所有分頁的url,然后繼續爬取。下面就是在parse回調函數后面增加了一點代碼是做好了。不過現在的代碼還不完善,會無休止的爬取下去,先不要運行,之后還要再改:
import urllib.parse
def parse(self, response):
items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
for item in items:
news = item.xpath(
'./div[@class="news-content"]'
'//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
'/text()'
).extract()[-1]
print(news.strip())
# 不找下一頁,而是找全部的頁,這樣會有去重的問題,就是要這個效果
pages = Selector(response).xpath('//div[@id="dig_lcpage"]//a/@href').extract()
print(pages)
url_parse = urllib.parse.urlparse(response.url)
for page in pages:
url = "%s://%s%s" % (url_parse.scheme, url_parse.hostname, page)
yield scrapy.Request(url=url)
這里做的事情就是當從前也分析了分頁的信息,把分頁信息生成新的url,然后再給調度器繼續爬取。
這里用的 scrapy.Request()
,實際上是應該要通過 from scrapy.http import Request
導入再用的。不過這里并不需要導入,并且只能能在scrapy下調用。因為在 scrapy/__init__.py 里有導入這個模塊了。并且這里已經不是系統第一次調用這個類了,程序啟動的時候,其實就是跑了下面的代碼把 start_urls 的地址開始爬取網頁了:
for url in self.start_urls:
yield Request(url, dont_filter=True)
這段代碼就是在當前類的父類 scrapy.Spider 里的 start_requests 方法里面。
爬取深度,允許抓取任何網站的最大深度。如果為零,則不施加限制。
這個是可以在配置文件里設置的。默認的配置里沒有寫這條,并且默認值是0,就是爬取深度沒有限制。所以就會永不停止的爬取下去。實際上不會無休止,似乎默認就有去重的功能,爬過的頁面不會重復爬取。所以不設置爬取深度,就能把所有的頁面都爬下來了
這里要講的是爬取深度的設置,所以和其他設置一樣,可以全局的在settings.py里設置。也可以現在類的公用屬性 custom_settings 這個字典里:
custom_settings = {
'ROBOTSTXT_OBEY': False,
'DEPTH_LIMIT': 1,
}
這個深度可以在返回的response參數里找到,在meta這個字典里:response.meta['depth']
默認有下面2條配置:
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
DUPEFILTER_DEBUG = False
去重的功能默認就是在 'scrapy.dupefilters.RFPDupeFilter' 這個類里做的。這個類有個父類 BaseDupeFilter 幫我們定義好了接口,我們可以寫一個自己的類自定義去重規則,繼承 BaseDupeFilter 實現里面的方法:
from scrapy.dupefilters import BaseDupeFilter
class MyFilter(BaseDupeFilter):
def __init__(self):
# 去重可以用上集合
# 在request_seen方法里判斷這個set,操作這個set
self.visited_url = set()
@classmethod
def from_settings(cls, settings):
"""初始化時調用的方法
返回一個實例,作用就是可以調用配置的信息生成實例
實例化時使用:obj = MyFilter.from_settings()
所以不要這樣實例化:obj = MyFilter()
什么都不寫,上面兩重方法生成的實例是一樣的
"""
return cls()
def request_seen(self, request):
"""過濾規則
檢測當前請求是否需要過濾(去重)
返回True表示需要過濾,返回False表示不用過濾
"""
return False
def open(self): # can return deferred
"""開始爬蟲時,調用一次
比如要記錄到文件的,在這里檢查和重建記錄文件
"""
pass
def close(self, reason): # can return a deferred
"""結束爬蟲時,調用一次
這里可以把之前的記錄文件close掉
"""
pass
def log(self, request, spider): # log that a request has been filtered
"""日志消息的記錄或打印可以寫在這里"""
pass
現在知道了,默認就是有去重規則的。所以上面爬取所有頁面的代碼并并不會無休止的執行下去,而是可以把所有頁面都爬完的。
程序啟動后,首先會調用父類 scrapy.Spider 里的 start_requests 方法。我們也可以不設置 start_urls 屬性,然后自己重構 start_requests 方法。啟動的效果是一樣的:
# start_urls = ['http://lab.scrapyd.cn/']
def start_requests(self):
urls = ['http://lab.scrapyd.cn/']
for url in urls:
yield scrapy.Request(url=url, dont_filter=True)
另外就是這個 scrapy.Request 類,回調函數 parse 方法最后也是調用這個方法類。這里還有一個重要的參數 callback 。默認不設置時 callback=parse
,所以可以手動設置callback參數,使用別的回調函數。或者準備多個回調函數,每次調度的時候設置不同額callback。比如第一次用默認的,之后在 parse 方法里再調用的時候,設置 callback=func
使用另外的回調函數。
默認就是開啟Cookie的,所以其實我們并不需要操作什么。
配置的 COOKIES_ENABLED 選項一旦關閉,則不會有Cookie了,別處再怎么設置也沒用。
可以用meta參數,為請求單獨設置cookie:
yield scrapy.Request(url, self.login, meta={'cookiejar': True})
不過如果要為請求單獨設置的話,就得為每個請求都顯示的聲明。否則不寫,就是認為是不要cookie。meta可以有如下設置:
meta={'cookiejar': True} # 使用Cookie
meta={'cookiejar': False} # 不使用Cookie,也就寫在第一個請求里。之后的請求不設置就是不使用Cookie
meta={'cookiejar': response.meta['cookiejar']} # 使用上一次的cookie,上一次必須是True或者這個,否則會有問題
手動設置cookie值
Request 實例化的時候有 cookies 參數,直接傳字典進去就可以了。
獲取cookie的值
并沒有cookie這個專門的屬性。本質上cookie就是headers里的一個鍵值對,用下面的方法去headers里獲取:
response.request.headers.getlist('Cookie') # 請求的Cookie
response.headers.getlist('Set-Cookie') # 響應的Cookie
最后就是綜合應用了。登錄需要Cookies的操作。不過其實什么都不做就可以了,默認方法就能把Cookies操作好。
然后就是從打開頁面、完成登錄、到最后點贊,需要發多次的請求,然后每次請求返回后所需要做的操作也是不一樣的,這里就需要準備多個回調函數,并且再發起請求的時候指定回調函數。代碼如下:
import scrapy
from scrapy.selector import Selector
from utils.base64p import b64decode_str # 自己寫的從文件讀密碼的方法,不是重點
class SpiderLabSpider(scrapy.Spider):
name = 'chouti_favor'
custom_settings = {
'ROBOTSTXT_OBEY': False,
}
def start_requests(self):
url = 'http://dig.chouti.com/'
yield scrapy.Request(url, self.login)
def login(self, response):
# 避免把密碼公開出來,去文件里拿,并且做了轉碼,這不是這里的重點
with open('../../utils/password') as f:
auth = f.read()
auth = auth.split('\n')
post_dict = {
'phone': '86%s' % auth[0], # 從請求正文里發現,會在手機號前加上86
'password': b64decode_str(auth[1]), # 直接填明文的用戶名和密碼也行的
}
yield scrapy.FormRequest(
url='http://dig.chouti.com/login',
formdata=post_dict,
callback=self.check_login,
)
def check_login(self, response):
print(response.request.headers.getlist('Cookie'))
print(response.headers.getlist('Set-Cookie'))
print(response.text)
yield scrapy.Request(
url='http://dig.chouti.com/',
dont_filter=True, # 這頁之前爬過了,如果不關掉過濾,就不會再爬了
)
def parse(self, response):
items = Selector(response).xpath('//*[@id="content-list"]/div[@class="item"]')
do_favor = True
for item in items:
news = item.xpath(
'./div[@class="news-content"]'
'//a[contains(concat(" ", normalize-space(@class), " "), " show-content ")]'
'/text()'
).extract()[-1]
print(news.strip())
# 點贊,做個判斷,只贊第一條
if do_favor:
do_favor = False
linkid = item.xpath('./div[@class="news-content"]/div[@share-linkid]/@share-linkid').extract_first()
yield scrapy.Request(
url='https://dig.chouti.com/link/vote?linksId=%s' % linkid,
method='POST',
callback=self.favor,
)
def favor(self, response):
print("點贊", response.text)
if __name__ == '__main__':
from scrapy import cmdline
log_level = '--nolog'
name = SpiderLabSpider.name
cmdline.execute(('scrapy crawl %s %s' % (name, log_level)).split())
注意:首頁的地址 http://dig.chouti.com 一共訪問了兩次。第二次如果不把 dont_filter 設為True,關閉過濾,就不會再去爬了。當然也可以第一次爬完之后,就保存在變量里,等登錄后再從這個返回開始之后的處理。
上面的POST請求,用到了 FormRequest 這個類。這個類繼承的是 Request 。里面主要就是把字典拼接成請求體,設置一下請求頭的 Content-Type ,默認再幫我們把 method 設為 POST 。也是可以繼續用 Request 的,就是把上面的3個步驟自己做了。主要是請求體,大概是按下面這樣拼接一下傳給body參數:
body='phone=86151xxxxxxxx&password=123456&oneMonth=1',
之前只是簡單的處理,所以在parse方法中直接處理。對于想要獲取更多的數據處理,則可以利用Scrapy的items將數據格式化,然后統一交由pipelines來處理。
回顧一下 Scrapy 組件和工作流程,項目管道(Pipeline) 組件負責這個工作。
先要編輯一下 items.py 里的類,默認會幫我們生成一個類,并有簡單的注釋。必須要處理2個數據 title 和 href ,則改寫 items.py 如下:
import scrapy
class PeppascrapyItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
href = scrapy.Field()
然后去修改之前的 parse 方法,導入上面的類,把要處理的數據傳遞進去生成實例,然后 yield :
from PeppaScrapy.items import PeppascrapyItem
class SpiderLabSpider(scrapy.Spider):
name = 'spider_lab'
allowed_domains = ['lab.scrapyd.cn']
start_urls = ['http://lab.scrapyd.cn/']
def parse(self, response):
print(response.url)
items = Selector(response).xpath(
'//div[@id="body"]//div[@id="main"]/div[@class="quote post"]')
for item in items:
title = item.xpath('./span[@class="text"]/text()').extract_first()
href = item.xpath('./span/a/@href').extract_first()
yield PeppascrapyItem(title=title, href=href)
上面這段代碼,只需要注意最后3行。把要保存的數據用items.py里的類實例化后,yield返回。
回顧下流程,之前yield返回給 scrapy.Request ,就是把數據返回給調度器繼續繼續爬取
這里yield返回給 scrapy.Item ,就是 Item Pipeline 里的 Item 進入數據的處理。
在 Item 里只是把數據傳遞出來,數據的處理則在 Pipeline 里。
如果有多處數據要返回,則可以自定義多個 scrapy.Item 類,來做數據的格式化處理。
還有一個 pipelines.py 文件,默認里面只有一個 return ,但是傳入2個參數 item 和 spider,先打印看看:
class PeppascrapyPipeline(object):
def process_item(self, item, spider):
print(item)
print(spider)
return item
只是編寫處理方法還不夠,這個方法需要注冊。在settings.py文件里,默認寫好了注冊的方法,只需要把注釋去掉。ITEM_PIPELINES 的 key 就是要注冊的方法,而 value 則是優先級。理論上字典沒有順序,優先級小的方法先執行:
ITEM_PIPELINES = {
'PeppaScrapy.pipelines.PeppascrapyPipeline': 300,
}
最后返回的item是個字典,我們報錯的變量名是key,值就是value。而spider則是這個爬蟲 scrapy.Spider 對象。
執行多個操作
這里一個類就是執行一個操作,如果對返回的數據要有多次操作,也可以多定義幾個類,然后注冊上即可。
每次操作的item,就是上一次操作最后 return item 傳遞下來的。第一次操作的item則是從 scrapy.Item 傳過來的。所以也可以對item進行處理,然后之后的操作就是在上一次操作對item的修改之上進行的。所以也可以想return什么就return什么,就是給下一個操作處理的數據。
綁定特定的爬蟲
Pipline并沒有和特定的爬蟲進行綁定,也就是所有的爬蟲都會依次執行所有的Pipline。對于特定爬蟲要做得特定的操作,可以在process_item方法里通過參數spider的spider.name進行判斷。
接著講上面的執行多個操作。如果在某個地方要終止之后所有的操作,則可以用 DropItem 。用法如下:
from scrapy.exceptions import DropItem
class PeppascrapyPipeline(object):
def process_item(self, item, spider):
print(item)
raise DropItem()
這樣對這組數據的操作就終止了。一般應該把這句放在某個條件的分支里。
Pipeline 這個類里,還可以定義更多方法。除了上面的處理方法,還有另外3個方法,其中一個是類方法。所有的方法名都不能修改,具體如下:
class PeppascrapyPipeline(object):
def __init__(self, value):
self.value = value
def process_item(self, item, spider):
"""操作并進行持久化"""
print(item)
# 表示將item丟棄,不會被后續pipeline處理
raise DropItem()
# print(spider)
# return item 給后續的pipeline繼續處理
# return item
@classmethod
def from_crawler(cls, crawler):
"""初始化時候,用于創建pipeline對象"""
val = crawler.settings.get('BOT_NAME')
# getint 方法可以直接獲取 int 參數
# val = crawler.settings.getint('DEPTH_LIMIT')
return cls(val)
def open_spider(self,spider):
"""爬蟲開始執行時,調用"""
print('START')
def close_spider(self,spider):
"""爬蟲關閉時,被調用"""
print('OVER')
類方法 from_crawler 是用于創建pipeline對象的。主要是接收了crawler參數,可以獲取到settings里的參數然后傳給構造方法。比如這里獲取了settings.py里的值傳給了對象。
另外2個方法 open_spider 和 close_spider ,是在爬蟲開始和關閉時執行的。即使爬蟲有多次返回,處理方法要調用多次,但是這2個方法都只會調用一次。這2個方法是在爬蟲 scrapy.Spider 開始和關閉的時候各執行一次的。而不是第一次返回數據處理和最后一次數據處理完畢。
打開文件的操作
以寫入文件為例,寫入一段數據需要3步:打開文件,寫入,關閉文件。如果把這3不都寫在 process_item 方法里,則會有多次的打開和關閉操作。正確的做法是,打開文件在 open_spider 方法里執行,寫入還是在 process_item 方法里每次返回都可以寫入,最后在 close_spider 方法里關閉文件。
默認有一個 middlewares.py 文件,里面默認創建了2個類,分別是爬蟲中間件和下載中間件
class PeppascrapySpiderMiddleware(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the spider middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_spider_input(self, response, spider):
"""下載完成,執行,然后交給parse處理"""
# Called for each response that goes through the spider
# middleware and into the spider.
# Should return None or raise an exception.
return None
def process_spider_output(self, response, result, spider):
"""spider處理完成,返回時調用
返回Request或者Item(字典也行,Item本身也是個字典)
Request就是給調度器繼續處理
Item就是給項目管道保存
"""
# Called with the results returned from the Spider, after
# it has processed the response.
# Must return an iterable of Request, dict or Item objects.
for i in result:
yield i
def process_spider_exception(self, response, exception, spider):
"""異常調用"""
# Called when a spider or process_spider_input() method
# (from other spider middleware) raises an exception.
# Should return either None or an iterable of Response, dict
# or Item objects.
pass
def process_start_requests(self, start_requests, spider):
"""爬蟲啟動時調用"""
# Called with the start requests of the spider, and works
# similarly to the process_spider_output() method, except
# that it doesn’t have a response associated.
# Must return only requests (not items).
for r in start_requests:
yield r
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
爬蟲中間件這里要注意下 process_spider_output() 返回的內容之后是要交給調度器繼續爬取的,或者是交給項目管道做保存操作。所以返回的可以是 Request 或者是 Item 。
class PeppascrapyDownloaderMiddleware(object):
# Not all methods need to be defined. If a method is not defined,
# scrapy acts as if the downloader middleware does not modify the
# passed objects.
@classmethod
def from_crawler(cls, crawler):
# This method is used by Scrapy to create your spiders.
s = cls()
crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
return s
def process_request(self, request, spider):
"""請求需要被下載時,經過所有下載器中間件的process_request調用"""
# Called for each request that goes through the downloader
# middleware.
# Must either:
# - return None: continue processing this request
# - or return a Response object
# - or return a Request object
# - or raise IgnoreRequest: process_exception() methods of
# installed downloader middleware will be called
return None
def process_response(self, request, response, spider):
"""spider處理完成,返回時調用"""
# Called with the response returned from the downloader.
# Must either;
# - return a Response object
# - return a Request object
# - or raise IgnoreRequest
return response
def process_exception(self, request, exception, spider):
"""異常處理
當下載處理器(download handler)
或 process_request() (下載中間件)拋出異常時執行
"""
# Called when a download handler or a process_request()
# (from other downloader middleware) raises an exception.
# Must either:
# - return None: continue processing this exception
# - return a Response object: stops process_exception() chain
# - return a Request object: stops process_exception() chain
pass
def spider_opened(self, spider):
spider.logger.info('Spider opened: %s' % spider.name)
process_request方法
對不同的返回值,回有不同的效果:
一般返回None,繼續后面的中間件或者下載。這里可以修改一下請求頭信息。比如,在請求頭里添加代理的設置,然后再讓后續的操作來執行。
返回Response,下載器就是要去下載生成Response。這里直接返回Response就相當于已經下載完成了。所以之后不再是執行下載了,而是返回給中間件里的process_response方法,執行下載完成后的操作。比如,可以不用默認的下載器來下載。到這里自己用Request模塊寫段代碼去下載,然后創建一個scrap.http.Eesponse對象,把內容填進去返回。
返回Request,調度器就是生成一個個的Request,然后調度執行。如果這里返回了Request,就會停止這次的執行,把Request放回調度器,等待下一次被調度執行。在process_response方法里返回Request也是一樣的效果,只是這里是在下載前要重新調度,那個是在下載后。
自定制命令
一、在spiders同級創建任意目錄,如:commands
二、在目錄里創建 crawlall.py 文件,名字任意取,這個文件名將來就是執行這段代碼的命令
下面是一個啟動spiders里所有爬蟲的代碼:
from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings
class Command(ScrapyCommand):
requires_project = True
def syntax(self):
return '[options]'
def short_desc(self):
return 'Runs all of the spiders'
def run(self, args, opts):
spider_list = self.crawler_process.spiders.list()
for name in spider_list:
self.crawler_process.crawl(name, **opts.__dict__)
self.crawler_process.start()
三、在 settings.py 中添加配置 COMMANDS_MODULE = '項目名稱.目錄名稱' ,比如:
COMMANDS_MODULE = "PeppaScrapy.commands"
四、執行命令: scrapy crawlall
利用信號在指定位置注冊制定操作。
自定義的型號要寫在寫一類,然后在settings里注冊。默認的配置文件里是有EXTENSIONS的,注釋掉了,這里就放開注釋然后改一下:
# Enable or disable extensions
# See https://doc.scrapy.org/en/latest/topics/extensions.html
EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
'PeppaScrapy.extensions.MyExtension': 100
}
根據上面的操作,就是創建 extensions.py 文件,然后寫一個 MyExtension 的類:
# PeppaScrapy/extensions.py 文件
from scrapy import signals
class MyExtension(object):
def __init__(self, value):
self.value = value
@classmethod
def from_crawler(cls, crawler):
val = crawler.settings.get('BOT_NAME')
ext = cls(val)
# 注冊你的方法和信息
crawler.signals.connect(ext.spider_start, signal=signals.spider_opened)
crawler.signals.connect(ext.spider_stop, signal=signals.spider_closed)
return ext
# 寫你要執行的方法
def spider_start(self, spider):
print('open')
def spider_stop(self, spider):
print('close')
所有的信號
上面的例子里用到了 spider_opened 和 spider_closed 這2個信號。
在 scrapy/signals.py 里可以查到所有的信號:
engine_started = object()
engine_stopped = object()
spider_opened = object()
spider_idle = object()
spider_closed = object()
spider_error = object()
request_scheduled = object()
request_dropped = object()
response_received = object()
response_downloaded = object()
item_scraped = object()
item_dropped = object()
# -*- coding: utf-8 -*-
# Scrapy settings for step8_king project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# http://doc.scrapy.org/en/latest/topics/settings.html
# http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
# http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
# 1. 爬蟲名稱
BOT_NAME = 'step8_king'
# 2. 爬蟲應用路徑
SPIDER_MODULES = ['step8_king.spiders']
NEWSPIDER_MODULE = 'step8_king.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
# 3. 客戶端 user-agent請求頭
# USER_AGENT = 'step8_king (+http://www.yourdomain.com)'
# Obey robots.txt rules
# 4. 禁止爬蟲配置
# ROBOTSTXT_OBEY = False
# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 5. 并發請求數
# CONCURRENT_REQUESTS = 4
# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# 6. 延遲下載秒數
# DOWNLOAD_DELAY = 2
# The download delay setting will honor only one of:
# 7. 單域名訪問并發數,并且延遲下次秒數也應用在每個域名
# CONCURRENT_REQUESTS_PER_DOMAIN = 2
# 單IP訪問并發數,如果有值則忽略:CONCURRENT_REQUESTS_PER_DOMAIN,并且延遲下次秒數也應用在每個IP
# CONCURRENT_REQUESTS_PER_IP = 3
# Disable cookies (enabled by default)
# 8. 是否支持cookie,cookiejar進行操作cookie
# COOKIES_ENABLED = True
# COOKIES_DEBUG = True
# Disable Telnet Console (enabled by default)
# 9. Telnet用于查看當前爬蟲的信息,操作爬蟲等...
# 使用telnet ip port ,然后通過命令操作
# TELNETCONSOLE_ENABLED = True
# TELNETCONSOLE_HOST = '127.0.0.1'
# TELNETCONSOLE_PORT = [6023,]
# 10. 默認請求頭
# 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',
# }
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
# 11. 定義pipeline處理請求
# ITEM_PIPELINES = {
# 'step8_king.pipelines.JsonPipeline': 700,
# 'step8_king.pipelines.FilePipeline': 500,
# }
# 12. 自定義擴展,基于信號進行調用
# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
# EXTENSIONS = {
# # 'step8_king.extensions.MyExtension': 500,
# }
# 13. 爬蟲允許的最大深度,可以通過meta查看當前深度;0表示無深度
# DEPTH_LIMIT = 3
# 14. 爬取時,0表示深度優先Lifo(默認);1表示廣度優先FiFo
# 后進先出,深度優先
# DEPTH_PRIORITY = 0
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleLifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.LifoMemoryQueue'
# 先進先出,廣度優先
# DEPTH_PRIORITY = 1
# SCHEDULER_DISK_QUEUE = 'scrapy.squeue.PickleFifoDiskQueue'
# SCHEDULER_MEMORY_QUEUE = 'scrapy.squeue.FifoMemoryQueue'
# 15. 調度器隊列
# SCHEDULER = 'scrapy.core.scheduler.Scheduler'
# from scrapy.core.scheduler import Scheduler
# 16. 訪問URL去重
# DUPEFILTER_CLASS = 'step8_king.duplication.RepeatUrl'
# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
"""
17. 自動限速算法
from scrapy.contrib.throttle import AutoThrottle
自動限速設置
1. 獲取最小延遲 DOWNLOAD_DELAY
2. 獲取最大延遲 AUTOTHROTTLE_MAX_DELAY
3. 設置初始下載延遲 AUTOTHROTTLE_START_DELAY
4. 當請求下載完成后,獲取其"連接"時間 latency,即:請求連接到接受到響應頭之間的時間
5. 用于計算的... AUTOTHROTTLE_TARGET_CONCURRENCY
target_delay = latency / self.target_concurrency
new_delay = (slot.delay + target_delay) / 2.0 # 表示上一次的延遲時間
new_delay = max(target_delay, new_delay)
new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
slot.delay = new_delay
"""
# 開始自動限速
# AUTOTHROTTLE_ENABLED = True
# The initial download delay
# 初始下載延遲
# AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
# 最大下載延遲
# AUTOTHROTTLE_MAX_DELAY = 10
# The average number of requests Scrapy should be sending in parallel to each remote server
# 平均每秒并發數
# AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# 是否顯示
# AUTOTHROTTLE_DEBUG = True
# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
"""
18. 啟用緩存
目的用于將已經發送的請求或相應緩存下來,以便以后使用
from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
from scrapy.extensions.httpcache import DummyPolicy
from scrapy.extensions.httpcache import FilesystemCacheStorage
"""
# 是否啟用緩存策略
# HTTPCACHE_ENABLED = True
# 緩存策略:所有請求均緩存,下次在請求直接訪問原來的緩存即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 緩存策略:根據Http響應頭:Cache-Control、Last-Modified 等進行緩存的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"
# 緩存超時時間
# HTTPCACHE_EXPIRATION_SECS = 0
# 緩存保存路徑
# HTTPCACHE_DIR = 'httpcache'
# 緩存忽略的Http狀態碼
# HTTPCACHE_IGNORE_HTTP_CODES = []
# 緩存存儲的插件
# HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
"""
19. 代理,需要在環境變量中設置
from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware
方式一:使用默認
os.environ
{
http_proxy:http://root:woshiniba@192.168.11.11:9999/
https_proxy:http://192.168.11.11:9999/
}
方式二:使用自定義下載中間件
def to_bytes(text, encoding=None, errors='strict'):
if isinstance(text, bytes):
return text
if not isinstance(text, six.string_types):
raise TypeError('to_bytes must receive a unicode, str or bytes '
'object, got %s' % type(text).__name__)
if encoding is None:
encoding = 'utf-8'
return text.encode(encoding, errors)
class ProxyMiddleware(object):
def process_request(self, request, spider):
PROXIES = [
{'ip_port': '111.11.228.75:80', 'user_pass': ''},
{'ip_port': '120.198.243.22:80', 'user_pass': ''},
{'ip_port': '111.8.60.9:8123', 'user_pass': ''},
{'ip_port': '101.71.27.120:80', 'user_pass': ''},
{'ip_port': '122.96.59.104:80', 'user_pass': ''},
{'ip_port': '122.224.249.122:8088', 'user_pass': ''},
]
proxy = random.choice(PROXIES)
if proxy['user_pass'] is not None:
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
encoded_user_pass = base64.encodestring(to_bytes(proxy['user_pass']))
request.headers['Proxy-Authorization'] = to_bytes('Basic ' + encoded_user_pass)
print "**************ProxyMiddleware have pass************" + proxy['ip_port']
else:
print "**************ProxyMiddleware no pass************" + proxy['ip_port']
request.meta['proxy'] = to_bytes("http://%s" % proxy['ip_port'])
DOWNLOADER_MIDDLEWARES = {
'step8_king.middlewares.ProxyMiddleware': 500,
}
"""
"""
20. Https訪問
Https訪問時有兩種情況:
1. 要爬取網站使用的可信任證書(默認支持)
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"
2. 要爬取網站使用的自定義證書
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "step8_king.https.MySSLFactory"
# https.py
from scrapy.core.downloader.contextfactory import ScrapyClientContextFactory
from twisted.internet.ssl import (optionsForClientTLS, CertificateOptions, PrivateCertificate)
class MySSLFactory(ScrapyClientContextFactory):
def getCertificateOptions(self):
from OpenSSL import crypto
v1 = crypto.load_privatekey(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.key.unsecure', mode='r').read())
v2 = crypto.load_certificate(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.pem', mode='r').read())
return CertificateOptions(
privateKey=v1, # pKey對象
certificate=v2, # X509對象
verify=False,
method=getattr(self, 'method', getattr(self, '_ssl_method', None))
)
其他:
相關類
scrapy.core.downloader.handlers.http.HttpDownloadHandler
scrapy.core.downloader.webclient.ScrapyHTTPClientFactory
scrapy.core.downloader.contextfactory.ScrapyClientContextFactory
相關配置
DOWNLOADER_HTTPCLIENTFACTORY
DOWNLOADER_CLIENTCONTEXTFACTORY
"""
"""
21. 爬蟲中間件
class SpiderMiddleware(object):
def process_spider_input(self,response, spider):
'''
下載完成,執行,然后交給parse處理
:param response:
:param spider:
:return:
'''
pass
def process_spider_output(self,response, result, spider):
'''
spider處理完成,返回時調用
:param response:
:param result:
:param spider:
:return: 必須返回包含 Request 或 Item 對象的可迭代對象(iterable)
'''
return result
def process_spider_exception(self,response, exception, spider):
'''
異常調用
:param response:
:param exception:
:param spider:
:return: None,繼續交給后續中間件處理異常;含 Response 或 Item 的可迭代對象(iterable),交給調度器或pipeline
'''
return None
def process_start_requests(self,start_requests, spider):
'''
爬蟲啟動時調用
:param start_requests:
:param spider:
:return: 包含 Request 對象的可迭代對象
'''
return start_requests
內置爬蟲中間件:
'scrapy.contrib.spidermiddleware.httperror.HttpErrorMiddleware': 50,
'scrapy.contrib.spidermiddleware.offsite.OffsiteMiddleware': 500,
'scrapy.contrib.spidermiddleware.referer.RefererMiddleware': 700,
'scrapy.contrib.spidermiddleware.urllength.UrlLengthMiddleware': 800,
'scrapy.contrib.spidermiddleware.depth.DepthMiddleware': 900,
"""
# from scrapy.contrib.spidermiddleware.referer import RefererMiddleware
# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
SPIDER_MIDDLEWARES = {
# 'step8_king.middlewares.SpiderMiddleware': 543,
}
"""
22. 下載中間件
class DownMiddleware1(object):
def process_request(self, request, spider):
'''
請求需要被下載時,經過所有下載器中間件的process_request調用
:param request:
:param spider:
:return:
None,繼續后續中間件去下載;
Response對象,停止process_request的執行,開始執行process_response
Request對象,停止中間件的執行,將Request重新調度器
raise IgnoreRequest異常,停止process_request的執行,開始執行process_exception
'''
pass
def process_response(self, request, response, spider):
'''
spider處理完成,返回時調用
:param response:
:param result:
:param spider:
:return:
Response 對象:轉交給其他中間件process_response
Request 對象:停止中間件,request會被重新調度下載
raise IgnoreRequest 異常:調用Request.errback
'''
print('response1')
return response
def process_exception(self, request, exception, spider):
'''
當下載處理器(download handler)或 process_request() (下載中間件)拋出異常
:param response:
:param exception:
:param spider:
:return:
None:繼續交給后續中間件處理異常;
Response對象:停止后續process_exception方法
Request對象:停止中間件,request將會被重新調用下載
'''
return None
默認下載中間件
{
'scrapy.contrib.downloadermiddleware.robotstxt.RobotsTxtMiddleware': 100,
'scrapy.contrib.downloadermiddleware.httpauth.HttpAuthMiddleware': 300,
'scrapy.contrib.downloadermiddleware.downloadtimeout.DownloadTimeoutMiddleware': 350,
'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': 400,
'scrapy.contrib.downloadermiddleware.retry.RetryMiddleware': 500,
'scrapy.contrib.downloadermiddleware.defaultheaders.DefaultHeadersMiddleware': 550,
'scrapy.contrib.downloadermiddleware.redirect.MetaRefreshMiddleware': 580,
'scrapy.contrib.downloadermiddleware.httpcompression.HttpCompressionMiddleware': 590,
'scrapy.contrib.downloadermiddleware.redirect.RedirectMiddleware': 600,
'scrapy.contrib.downloadermiddleware.cookies.CookiesMiddleware': 700,
'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 750,
'scrapy.contrib.downloadermiddleware.chunked.ChunkedTransferMiddleware': 830,
'scrapy.contrib.downloadermiddleware.stats.DownloaderStats': 850,
'scrapy.contrib.downloadermiddleware.httpcache.HttpCacheMiddleware': 900,
}
"""
# from scrapy.contrib.downloadermiddleware.httpauth import HttpAuthMiddleware
# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
# DOWNLOADER_MIDDLEWARES = {
# 'step8_king.middlewares.DownMiddleware1': 100,
# 'step8_king.middlewares.DownMiddleware2': 500,
# }
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。