一末患、將爬取到的數(shù)據(jù)存儲到磁盤文件和數(shù)據(jù)庫中各一份
Pipeline
import pymysql
class QiubaiproPipeline(object):
fp = None
def open_spider(self,spider): #只會被調(diào)用一次
print('開始爬蟲')
self.fp = open('./qiushi.txt','w',encoding='utf-8')
#該方法被調(diào)用后活玲,就可以將item(參數(shù))包含的數(shù)據(jù)值進行持久化存儲
def process_item(self, item, spider):
print('這是item的內(nèi)容:',item)
self.fp.write(item['name'])
return item #返回給了下一個即將被執(zhí)行的管道類
def close_spider(self,spider):
print('結(jié)束爬蟲')
self.fp.close()
class MysqlPileLine(object):
conn = None
cursor = None
def open_spider(self,spider):
self.conn = pymysql.Connect(host='127.0.0.1',port=3306,user='root',password='',db='spider')
def process_item(self,item,spider):
self.cursor = self.conn.cursor()
try:
self.cursor.execute('insert into qb values("%s")'%(item['name']))
self.conn.commit()
except Exception as e:
print(e)
self.conn.rollback()
def close_spider(self,spider):
self.cursor.close()
self.conn.close()
Settings
ITEM_PIPELINES = {
'qiubaiPro.pipelines.QiubaiproPipeline': 300, # 先執(zhí)行
'qiubaiPro.pipelines.MysqlPileLine': 301, # 后執(zhí)行
}
上述代碼中窒朋,字典中的兩組鍵值分別表示會執(zhí)行管道文件中對應(yīng)的兩個管道類中的process_item方法,實現(xiàn)兩種不同形式的持久化操作想暗。
二挨摸、scrapy中selenium的應(yīng)用
在通過scrapy框架進行某些網(wǎng)站數(shù)據(jù)爬取的時候,往往會碰到頁面動態(tài)數(shù)據(jù)加載的情況發(fā)生际歼,如果直接使用scrapy對其url發(fā)請求惶翻,是絕對獲取不到那部分動態(tài)加載出來的數(shù)據(jù)值。但是通過觀察我們會發(fā)現(xiàn)鹅心,通過瀏覽器進行url請求發(fā)送則會加載出對應(yīng)的動態(tài)加載出的數(shù)據(jù)吕粗。那么如果我們想要在scrapy也獲取動態(tài)加載出的數(shù)據(jù),則必須使用selenium創(chuàng)建瀏覽器對象旭愧,然后通過該瀏覽器對象進行請求發(fā)送溯泣,獲取動態(tài)加載的數(shù)據(jù)值。需要用到下載中間件中的process_response 方法榕茧。
selenium在scrapy中使用的原理分析:
當(dāng)引擎將url對應(yīng)的請求提交給下載器后垃沦,下載器進行網(wǎng)頁數(shù)據(jù)的下載,然后將下載到的頁面數(shù)據(jù)用押,封裝到response中肢簿,提交給引擎。引擎將response在轉(zhuǎn)交給Spiders。Spiders接受到的response對象中存儲的頁面數(shù)據(jù)里是沒有動態(tài)加載的新聞數(shù)據(jù)的池充。要想獲取動態(tài)加載的新聞數(shù)據(jù)桩引,則需要在下載中間件中對下載器提交給引擎的response響應(yīng)對象進行攔截,且對其內(nèi)部存儲的頁面數(shù)據(jù)進行篡改收夸,修改成攜帶了動態(tài)加載出的數(shù)據(jù)坑匠,然后將被篡改的response對象最終交給Spiders進行解析操作。
selenium在scrapy中的使用流程:
- 在spider的init方法中實例化一個瀏覽器對象(因為瀏覽器對象只需要被實例化一次)卧惜;
- 在spider的closed方法中關(guān)閉瀏覽器對象(該方法是在爬蟲結(jié)束時被調(diào)用)厘灼;
- 在下載中間件類的process_response方法中接收spider中的瀏覽器對象;
- 處理執(zhí)行相關(guān)自動化操作(發(fā)起請求,獲取頁面數(shù)據(jù))
- 實例化一個新的響應(yīng)對象(from scrapy.http import HtmlResponse),且將頁面數(shù)據(jù)存儲到該對象中
- 返回新的響應(yīng)對象
- 在配置文件中開啟下載中間件
案例分析:
- 需求:爬取網(wǎng)易新聞的國內(nèi)板塊下的新聞數(shù)據(jù)
- 分析:當(dāng)點擊國內(nèi)超鏈進入國內(nèi)對應(yīng)的頁面時咽瓷,會發(fā)現(xiàn)當(dāng)前頁面展示的新聞數(shù)據(jù)是被動態(tài)加載出來的设凹,如果直接通過程序?qū)rl進行請求,是獲取不到動態(tài)加載出的新聞數(shù)據(jù)的茅姜。則就需要我們使用selenium實例化一個瀏覽器對象闪朱,在該對象中進行url的請求,獲取動態(tài)加載的新聞數(shù)據(jù)钻洒。
代碼展示:
爬蟲文件:
class WangyiSpider(RedisSpider):
name = 'wangyi'
#allowed_domains = ['www.xxxx.com']
start_urls = ['https://news.163.com'] #如果爬取多個url奋姿,全寫到里面,依次爬取
def __init__(self):
#實例化一個瀏覽器對象(實例化一次)
self.bro = webdriver.Chrome(executable_path='/Users/bobo/Desktop/chromedriver')
#必須在整個爬蟲結(jié)束后素标,關(guān)閉瀏覽器
def closed(self,spider):
print('爬蟲結(jié)束')
self.bro.quit()
中間件文件:修改process_response中的方法
from scrapy.http import HtmlResponse
def process_response(self, request, response, spider):
#響應(yīng)對象中存儲頁面數(shù)據(jù)的篡改
if request.url in['http://news.163.com/domestic/','http://news.163.com/world/','http://news.163.com/air/','http://war.163.com/']:
spider.bro.get(url=request.url)
js = 'window.scrollTo(0,document.body.scrollHeight)'
spider.bro.execute_script(js)
time.sleep(2) #一定要給與瀏覽器一定的緩沖加載數(shù)據(jù)的時間
page_text = spider.bro.page_source #頁面數(shù)據(jù)就是包含了動態(tài)加載出來的新聞數(shù)據(jù)對應(yīng)的頁面數(shù)據(jù)
time.sleep(2)
#篡改響應(yīng)對象
return HtmlResponse(url=spider.bro.current_url,body=page_text,encoding='utf-8',request=request)
else:
return response
配置文件:
DOWNLOADER_MIDDLEWARES = {
'wangyiPro.middlewares.WangyiproDownloaderMiddleware': 543,
}
三称诗、增量式爬蟲
當(dāng)我們在瀏覽`網(wǎng)頁的時候會發(fā)現(xiàn),某些網(wǎng)站定時會在原有網(wǎng)頁數(shù)據(jù)的基礎(chǔ)上更新一批數(shù)據(jù)糯钙,例如某電影網(wǎng)站會實時更新一批最近熱門的電影粪狼。小說網(wǎng)站會根據(jù)作者創(chuàng)作的進度實時更新最新的章節(jié)數(shù)據(jù)等等。那么任岸,類似的情景再榄,當(dāng)我們在爬蟲的過程中遇到時,我們是不是需要定時更新程序以便能爬取到網(wǎng)站中最近更新的數(shù)據(jù)呢享潜?
1. 增量式爬蟲
概念:通過爬蟲程序監(jiān)測某網(wǎng)站數(shù)據(jù)更新的情況困鸥,以便可以爬取到該網(wǎng)站****更新出的新數(shù)據(jù)****。
如何進行增量式的爬取工作:
- 在發(fā)送請求之前判斷這個URL是不是之前爬取過
- 在解析內(nèi)容后判斷這部分內(nèi)容是不是之前爬取過
- 寫入存儲介質(zhì)時判斷內(nèi)容是不是已經(jīng)在介質(zhì)中存在
分析:
不難發(fā)現(xiàn)剑按,其實增量爬取的核心是去重疾就,至于去重的操作在哪個步驟起作用,只能說各有利弊艺蝴。在我看來猬腰,前兩種思路需要根據(jù)實際情況取一個(也可能都用)。
- 第一種思路適合不斷有新頁面出現(xiàn)的網(wǎng)站猜敢,比如說小說的新章節(jié)姑荷,每天的最新新聞等等盒延;
- 第二種思路則適合頁面內(nèi)容會更新的網(wǎng)站。
- 第三個思路是相當(dāng)于是最后的一道防線鼠冕。這樣做可以最大程度上達到去重的目的添寺。
去重方法
將爬取過程中產(chǎn)生的url進行存儲,存儲在redis的set中懈费。當(dāng)下次進行數(shù)據(jù)爬取時计露,首先對即將要發(fā)起的請求對應(yīng)的url在存儲的url的set中做判斷,如果存在則不進行請求憎乙,否則才進行請求票罐。
對爬取到的網(wǎng)頁內(nèi)容進行唯一標識的制定,然后將該唯一標識寨闹,存儲至redis的set中胶坠。當(dāng)下次爬取到網(wǎng)頁數(shù)據(jù)的時候君账,在進行持久化存儲之前繁堡,首先可以先判斷該數(shù)據(jù)的唯一標識在redis的set中是否存在,再決定是否進行持久化存儲乡数。
2.項目案例
- 需求:爬取4567tv網(wǎng)站中所有的電影詳情數(shù)據(jù)椭蹄。策略:對詳情頁url去重
爬蟲文件
import scrapyfrom scrapy.linkextractors
import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from redis import Redis
from incrementPro.items import IncrementproItem
class MovieSpider(CrawlSpider):
name = 'movie'
start_urls = ['http://www.4567tv.tv/frim/index7-11.html'] #首頁
rules = (
Rule(LinkExtractor(allow=r'/frim/index7-\d+\.html'), callback='parse_item', follow=True), #正則中正常使用點號要轉(zhuǎn)義
)
conn = Redis(host='127.0.0.1',port=6379) #創(chuàng)建redis鏈接對象
def parse_item(self, response):
li_list = response.xpath('//li[@class="p1 m1"]')
for li in li_list:
detail_url = 'http://www.4567tv.tv'+li.xpath('./a/@href').extract_first() #獲取詳情頁的url
# 將詳情頁的url存入redis的set中,這里的第一個參數(shù)url净赴,是給set集合起的名字
ex = self.conn.sadd('urls',detail_url)
if ex == 1:
print('該url沒有被爬取過绳矩,可以進行數(shù)據(jù)的爬取')
yield scrapy.Request(url=detail_url,callback=self.parst_detail)
else:
print('數(shù)據(jù)還沒有更新,暫無新數(shù)據(jù)可爬染脸帷翼馆!')
#解析詳情頁中的電影名稱和類型,進行持久化存儲
def parst_detail(self,response):
item = IncrementproItem()
item['name'] = response.xpath('//dt[@class="name"]/text()').extract_first()
item['kind'] = response.xpath('//div[@class="ct-c"]/dl/dt[4]//text()').extract()
item['kind'] = ''.join(item['kind'])
yield item
管道文件:
from redis import Redis
class IncrementproPipeline(object):
conn = None
def open_spider(self,spider):
self.conn = Redis(host='127.0.0.1',port=6379)
def process_item(self, item, spider):
dic = {
'name':item['name'],
'kind':item['kind']
}
print(dic)
self.conn.lpush('movieData',dic)
return item
啟動redis數(shù)據(jù)庫的服務(wù)端和客戶端
拷貝一下金度,作為正則
需求:爬取糗事百科中的段子和作者數(shù)據(jù)应媚。策略:對爬取的內(nèi)容去重。
爬蟲文件
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from incrementByDataPro.items import IncrementbydataproItem
from redis import Redisimport hashlib
class QiubaiSpider(CrawlSpider):
name = 'qiubai'
# allowed_domains = ['www.xxx.com']
start_urls = ['https://www.qiushibaike.com/text/']
rules = (
Rule(LinkExtractor(allow=r'/text/page/\d+/'), callback='parse_item', follow=True),
Rule(LinkExtractor(allow=r'/text/$'), callback='parse_item', follow=True),
)
#創(chuàng)建redis鏈接對象
conn = Redis(host='127.0.0.1',port=6379)
def parse_item(self, response):
div_list = response.xpath('//div[@id="content-left"]/div')
for div in div_list:
item = IncrementbydataproItem()
item['author'] = div.xpath('./div[1]/a[2]/h2/text() | ./div[1]/span[2]/h2/text()').extract_first()
item['content'] = div.xpath('.//div[@class="content"]/span/text()').extract_first()
#將解析到的數(shù)據(jù)值生成一個唯一的標識進行redis存儲
source = item['author']+item['content']
source_id = hashlib.sha256(source.encode()).hexdigest()
#將解析內(nèi)容的唯一表示存儲到redis的data_id中
ex = self.conn.sadd('data_id',source_id)
if ex == 1:
print('該條數(shù)據(jù)沒有爬取過猜极,可以爬取......')
yield item
else:
print('該條數(shù)據(jù)已經(jīng)爬取過了中姜,不需要再次爬取了!!!')
管道文件:
# -*- coding: utf-8 -*-
# Define your item pipelines here## Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://doc.scrapy.org/en/latest/topics/item-pipeline.html
from redis import Redis
class IncrementbydataproPipeline(object):
conn = None
def open_spider(self, spider):
self.conn = Redis(host='127.0.0.1', port=6379)
def process_item(self, item, spider):
dic = {
'author': item['author'],
'content': item['content']
}
# print(dic)
self.conn.lpush('qiubaiData', dic)
return item
因為span標簽里面有br標簽,所以用xpath獲得的是列表跟伏,用extract提取丢胚,然后用jion轉(zhuǎn)換成字符串
要排空值,產(chǎn)生的原因是xpath解析
四受扳、如何提高scrapy的爬取效率
增加并發(fā)
默認scrapy開啟的并發(fā)線程為32個携龟,可以適當(dāng)進行增加。在settings配置文件中修改**CONCURRENT_REQUESTS **= 100值為100,并發(fā)設(shè)置成了為100勘高。降低日志級別
在運行scrapy時峡蟋,會有大量日志信息的輸出浮定,為了減少CPU的使用率〔阋冢可以設(shè)置log輸出信息為INFO或者ERROR即可桦卒。在配置文件中編寫:LOG_LEVEL = ‘INFO’禁止cookie
如果不是真的需要cookie,則在scrapy爬取數(shù)據(jù)時可以進制cookie從而減少CPU的使用率匿又,提升爬取效率方灾。在配置文件中編寫:COOKIES_ENABLED = False禁止重試
對失敗的HTTP進行重新請求(重試)會減慢爬取速度,因此可以禁止重試碌更。在配置文件中編寫:RETRY_ENABLED = False減少下載超時
如果對一個非常慢的鏈接進行爬取裕偿,減少下載超時可以能讓卡住的鏈接快速被放棄,從而提升效率痛单。在配置文件中進行編寫:DOWNLOAD_TIMEOUT = 10 超時時間為10s
測試案例:www.521609.com
import scrapyfrom xiaohua.items
import XiaohuaItem
class XiahuaSpider(scrapy.Spider):
name = 'xiaohua'
allowed_domains = ['www.521609.com']
start_urls = ['http://www.521609.com/daxuemeinv/']
pageNum = 1
url = 'http://www.521609.com/daxuemeinv/list8%d.html'
def parse(self, response):
li_list = response.xpath('//div[@class="index_img list_center"]/ul/li')
for li in li_list:
school = li.xpath('./a/img/@alt').extract_first()
img_url = li.xpath('./a/img/@src').extract_first()
item = XiaohuaItem()
item['school'] = school
item['img_url'] = 'http://www.521609.com' + img_url
yield item
if self.pageNum < 10:
self.pageNum += 1
url = format(self.url % self.pageNum)
#print(url)
yield scrapy.Request(url=url,callback=self.parse)
import scrapy
class XiaohuaItem(scrapy.Item):
school=scrapy.Field()
img_url=scrapy.Field()
import json
import os
import urllib.request
class XiaohuaPipeline(object):
def __init__(self):
self.fp = None
def open_spider(self,spider):
print('開始爬蟲')
self.fp = open('./xiaohua.txt','w')
def download_img(self,item):
url = item['img_url']
fileName = item['school']+'.jpg'
if not os.path.exists('./xiaohualib'):
os.mkdir('./xiaohualib')
filepath = os.path.join('./xiaohualib',fileName)
urllib.request.urlretrieve(url,filepath)
print(fileName+"下載成功")
def process_item(self, item, spider):
obj = dict(item)
json_str = json.dumps(obj,ensure_ascii=False)
self.fp.write(json_str+'\n')
#下載圖片
self.download_img(item)
return item
def close_spider(self,spider):
print('結(jié)束爬蟲')
self.fp.close()
配置文件:
USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 100
COOKIES_ENABLED = False
LOG_LEVEL = 'ERROR'
RETRY_ENABLED = False
DOWNLOAD_TIMEOUT = 3
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16
DOWNLOAD_DELAY = 3
五睁宰、自定制命令
1.不在終端運行爬蟲方法
不在終端運行爬蟲,可以在最外層項目的下創(chuàng)建start.py文件
導(dǎo)入如下配置:直接運行就可以了
import sys
from scrapy.cmdline import execute
if __name__ == '__main__':
execute(['scrapy','crawl','baidu','--nolog'])##這里相當(dāng)于是scrapy crawl baidu --nolog
'''
放在創(chuàng)建項目下的第一層目錄下執(zhí)行
'''
2.自定制scrapy命令方法
在settings里面的配置:
COMMANDS_MODULE='scrapyproject1.commands'
首先在你要啟動的項目下面(spider的同級目錄)創(chuàng)建一個commands文件(名字可以自己修改肴楷,但是前提settings配置信息也要修改,否則找不到)京痢,然后在commands下面創(chuàng)建crawl_allspiders.py(名字可以修改,運行命令就是--scrapy 名字--,例如:scrapy crawl_spiders)
配置信息如下:
from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings
class Command(ScrapyCommand):
requires_project = True
def syntax(self):
return '[option]' # 該命令的option
def short_desc(self): # 對該命令的描述
return 'run'
def run(self, args, opts):
spider_list=self.crawler_process.spiders.list()
print(spider_list) # 拿到當(dāng)前下的所有爬蟲
print(opts)
print(args)
'''['baidu', 'baidu1', 'baidu2']'''
for name in spider_list:
self.crawler_process.crawl(name,**opts.__dict__)
self.crawler_process.start()
xpath拿到的是selector對象挥吵,可以繼續(xù)往下面找標簽
extract() 拿到的是字符串
extarct_first() 拿到當(dāng)前標簽下面的第一個文本內(nèi)容
extract() 拿到所有的標簽文本的內(nèi)容
./ 兒子(當(dāng)前標簽下面)
.// 當(dāng)前標簽下面的孫子,子子孫孫都可以
# 取當(dāng)前標簽下面的屬性和文本:
/text() 取當(dāng)前標簽的文本內(nèi)容
/@href 拿到當(dāng)前的屬性
response.xpath('//a[2]')后面的2是按索引來找到第二個
response.xpath('//a[@href][@id]')是多個條件進行篩選
//a[contains(@href,"sina")] 包含的關(guān)系重父,只有這個標簽里面有sina這個字段就可以了,也可以是其他字符
//a[start-with(@href,'sina')] 找到這個屬性是否是以sina開頭的
re:正則
//a[re:test(@id,'i\(d+)')] 更高級的用法忽匈,前面是固定的寫法房午,后面是找到id屬性,后main是匹配的規(guī)則丹允,id=i1或id=i2>>>>d+是匹配數(shù)字
//a[re:test(@id,'i\(d+)')]/text() 拿文本郭厌,或者其他
*是匹配多個的寫法,代表任意的標簽
//*[@id="newsContent23123186"]/div[1] 找第幾個,最后是索引
split('',) 切割
strip() 去除空
內(nèi)鏈就是同一網(wǎng)站域名下站內(nèi)的鏈接雕蔽,鏈接指向網(wǎng)站內(nèi)部折柠,好的內(nèi)鏈結(jié)構(gòu)是有助于網(wǎng)站收錄的。最通俗的說法是自己在自己的網(wǎng)站上添加鏈接萎羔,就是在同一網(wǎng)站域名下的各內(nèi)容頁面間的互相鏈接液走。內(nèi)鏈,其實在于用戶體驗贾陷,能從這個頁面快速的進入下個頁面就可以了缘眶。
網(wǎng)站的內(nèi)鏈和外鏈的區(qū)別,外鏈可以算作是友情鏈接髓废,就是你和別人交換相互之間的鏈接巷懈,從別人網(wǎng)站可以進入你的網(wǎng)站,內(nèi)鏈在同一網(wǎng)站域名下的內(nèi)容頁面之間的互相鏈接慌洪,是網(wǎng)站自身內(nèi)部結(jié)構(gòu)顶燕。不管是內(nèi)鏈還是外鏈凑保,都對網(wǎng)站的SEO有重要的影響,所有不管你是做什么網(wǎng)站涌攻,都要注重內(nèi)鏈和外鏈的布局欧引,這樣你的網(wǎng)站才能在搜索引擎上有排名.