1. Scrapy的核心組成
? ? ? ? 引擎(engine):scrapy的核心,所有模塊的銜接,數據流程梳理。
? ? ? ? 調度器(scheduler):本質可以看成一個集合和隊列,里面存放著一堆即將要發送的請求,可以看成是一個url容器,它決定了下一步要爬取哪一個url,通常我們在這里可以對url進行去重操作。
? ? ? ? 下載器(downloader):本質是一個用來發動請求的模塊,可以理解成是一個requests.get()的功能,只不過返回的是一個response對象。
? ? ? ? 爬蟲(spider):負載解析下載器返回的response對象,從中提取需要的數據。
? ? ? ? 管道(pipeline):主要負責數據的存儲和各種持久化操作。
2. 安裝步驟
? ? ? ? 這里安裝的scrapy版本為2.5.1版,在pycharm命令行內輸入pip install scrapy==2.5.1即可。
pip install scrapy==2.5.1
? ? ? ? 但是要注意OpenSSL的版本,其查看命令為
scrapy version --verbose
? ? ? ?如果OpenSSL版本不為1.1版本的話,需要對其進行降級。
pip uninstall cryptography
pip install cryptography==36.0.2
? ? ? ? 注:如果降級之后使用scrapy version --verbose出現錯誤:TypeError: deprecated() got an unexpected keyword argument 'name',可能是OpenSSL版本過低導致,這里需要根據自身情況,進行對應處理。
卸載cryptography:pip uninstall cryptography
重新安裝cryptography 36.0.2:pip install cryptography==36.0.2
卸載pyOpenSSL:pip uninstall pyOpenSSL
重新安裝pyOpenSSL 22.0.0:pip install pyOpenSSL==22.0.0
? ? ? ? 如果查看時出現錯誤:AttributeError: 'SelectReactor' object has no attribute '_handleSignals'
可能是由于Twisted版本問題,進行卸載重新安裝Twisted即可。
pip uninstall Twisted
pip install Twisted==22.10.0
3. 基礎使用
1.創建項目scrapy startproject 項目名
2.進入項目目錄cd 項目名
3.生成spiderscrapy genspider 爬蟲名字 網站的域名
4.調整spider給出start_urls以及如何解析數據
5.調整setting配置文件配置user_agent,robotstxt_obey,pipeline取消日志信息,留下報錯,需調整日志級別 LOG_LEVEL
6.允許scrapy程序scrapy crawl 爬蟲的名字
4. 案例分析
? ? ? ? 當使用 scrapy startproject csdn?之后,會出現csdn的文件夾
? ? ? ? 當輸入 scrapy genspider csdn_spider blog.csdn.net 之后,會出現
? ? ? ? ?我們這里以爬取自己csdn所發表的文章為例,在csdn_spider.py中編輯頁面元素的定位方式
import scrapyclass CsdnSpiderSpider(scrapy.Spider):name = 'csdn_spider'allowed_domains = ['blog.csdn.net']start_urls = ['http://blog.csdn.net/mozixiao__']def parse(self, response):print('===>',response)infos = response.xpath('//*[@id="navList-box"]/div[2]/div/div/div') #這里的路徑需要注意的是,最后一個div不需要加確定的值,這里是一個模糊匹配,不然infos就只有一個信息for info in infos:title = info.xpath('./article/a/div/div[1]/div[1]/h4/text()').extract_first().strip()date = info.xpath('./article/a/div/div[2]/div[1]/div[2]/text()').extract_first().strip().split()[1]view = info.xpath('./article/a/div/div[2]/div[1]/div[3]/span/text()').extract_first().strip()dianzan = info.xpath('./article/a/div/div[2]/div[1]/div[4]/span/text()').extract_first().strip()pinglun = info.xpath('./article/a/div/div[2]/div[1]/div[5]/span/text()').extract_first().strip()shouchang = info.xpath('./article/a/div/div[2]/div[1]/div[6]/span/text()').extract_first().strip()yield {'title':title,'date':date,'view':view,'dianzan':dianzan,'pinglun':pinglun,'shouchang':shouchang}# print(title,date,view,dianzan,pinglun,shouchang)
? ? ? ? 通過yield返回的數據會傳到piplines.py文件中,在pipelines.py文件中進行數據的保存。
#管道想要使用要在setting開啟
class CsdnPipeline:def process_item(self, item, spider):# print(type(item['title']),type(item['date']),type(item['view']),type(item['dianzan']),type(item['pinglun']),type(item['shouchang']))with open('data.csv',mode='a+',encoding='utf-8') as f:# line =f.write('標題:{} 更新日期:{} 瀏覽量:{} 點贊:{} 評論:{} 收藏:{} \n'.format(
item['title'],item['date'],item['view'],item['dianzan'],item['pinglun'],item['shouchang']))# f.write(f"標題:{item['title']} 更新日期:{item['date']} 瀏覽量:{item['view']} 點贊:{item['dianzan']} 評論:{item['pinglun']} 收藏:{item['shouchang']} \n")return item
5. pipelines.py改進
? ? ? ? 上面的pipelines.py文件中對于文件的open次數與爬取的信息數量有關,為了減少文件的讀取關閉操作,采用全局操作的方式。
class CsdnPipeline:def open_spider(self,spider):self.f = open('data.csv',mode='a+',encoding='utf-8')def close_spider(self,spider):self.f.close()def process_item(self, item, spider):self.f.write('標題:{} 更新日期:{} 瀏覽量:{} 點贊:{} 評論:{} 收藏:{} \n'.format(
item['title'],item['date'],item['view'],item['dianzan'],item['pinglun'],item['shouchang']))# f.write(f"標題:{item['title']} 更新日期:{item['date']} 瀏覽量:{item['view']} 點贊:{item['dianzan']} 評論:{item['pinglun']} 收藏:{item['shouchang']} \n")return item
6. 爬蟲時,當前頁面爬取信息時,需要跳轉到其他url
? ? ? ? 爬取當前頁面時,爬取到的信息是一個url信息,這是需要將其與之前的url進行拼接。
? ? ? ? 以https://desk.zol.com.cn/dongman/為主url,/bizhi/123.html為跳轉url為例。如果鏈接以 / 開頭,需要拼接的是域名,最前面的 / 是根目錄。結果為https://desk.zol.com.cn/bizhi/123.html。如果不是以 / 開頭,需要冥界的是當前目錄,同級文件夾中找到改內容。結果為https://desk.zol.com.cn/dongman/bizhi/123.html。
? ? ? ? 為了方便url的跳轉,可以使用python中urllib庫或者scrapy封裝好的函數。
class PicSpiderSpider(scrapy.Spider):name = 'pic_spider'allowed_domains = ['blog.csdn.net']start_urls = ['http://blog.csdn.net/mozixiao__']def parse(self, response):infos = response.xpath('')for info in infos:if info.endswith(''):continue#方法1from urllib.parse import urljoinchild_url = urljoin(response.url,info)#方法2child_url = response.urljoin(info)
? ? ? ? 為了更好地處理跳轉之后的鏈接(不需要用requests庫寫圖片的提取),同時為了方式新的url繼續跳轉到parse,我們可以重寫一個new_parse來處理跳轉url。
import scrapy
from scrapy import Requestclass PicSpiderSpider(scrapy.Spider):name = 'pic_spider'allowed_domains = ['blog.csdn.net']start_urls = ['http://blog.csdn.net/mozixiao__']def parse(self, response):infos = response.xpath('')for info in infos:if info.endswith(''):continue#方法1# from urllib.parse import urljoin# child_url = urljoin(response.url,info)#方法2child_url = response.urljoin(info)yield Request(child_url,callback=self.new_parse)def new_parse(self,response):img_src = response.xpath('')yield {"src":img_src}
7. pipelines.py保存對象是圖片或者文件等
from itemadapter import ItemAdapterfrom scrapy.pipelines.images import ImagesPipeline
from scrapy.pipelines.files import FilesPipeline
from scrapy import Requestclass PicPipeline(ImagesPipeline):def get_media_requests(self,item,info):srcs = item['src']for src in srcs:yield Request(src,meta={'path':src})def file_path(self,request,response=None,info=None,*,item=None):path = request.meta['path']file_name = path.split('/')[-1]return '***/***/***/{}'.format(file_name)def item_completed(self, results, item,info):return item
注:為了使圖片可以成功的保存,需要在settings.py文件中設置一個IMAGES_STORE的路徑。同時,如果在下載圖片時,出現了302的問題,需要設置MEDIA_ALLOW_REDIRECTS。
8. Scrapy爬蟲遇到分頁跳轉的時候
1.普通分頁表現為:上一頁 1,2,3,4,5,6 下一頁類型1:觀察頁面源代碼發現url直接在頁面源代碼里體現解決方案:1.訪問第一頁->提取下一個url,訪問下一頁2.直接觀察最多大少爺,然后觀察每一頁url的變化類型2:觀察頁面源代碼發現url不在頁面源代碼中體現解決方案:通過抓包找規律(可能在url上體現,也可能在參數上體現)
2.特殊分頁類型1:顯示為加載更多的圖標,點擊之后出來一推新的信息解決方案:通過抓包找規律類型2:滾動刷新,滑倒數據結束的時候會再次加載新數據這種通常的邏輯是:這一次更新時獲得的參數會附加到下一次更新的請求中
? ? ? ? 情況1:如果遇到分頁跳轉信息在url中體現,可以通過重寫start_request的方式來進行
import scrapy
from scrapy import Requestclass FenyeSpiderSpider(scrapy.Spider):name = 'fenye_spider'allowed_domains = ['blog.csdn.net']start_urls = ['http://blog.csdn.net/']def start_requests(self):num = int(input())for i in range(1,num):url = "https://***.com/page_{}.html".format(i)yield Request(url)def parse(self, response):pass
? ? ? ? 情況2:分頁跳轉信息的url體現在的頁面源代碼中
import scrapy
from scrapy import Requestclass FenyeSpiderSpider(scrapy.Spider):name = 'fenye_spider'allowed_domains = ['blog.csdn.net']start_urls = ['http://blog.csdn.net/page_1.html']def parse(self, response):infos = response.xpath('')for info in infos:if info.startswith('***'):continuechild_info = response.urljoin(info)#這里無需考慮死循環的問題,scrapy中的調度器會自動去重yield Request(child_info,callback=self.parse)
9. Scrapy面對帶有cookie的信息頁面時的登陸操作
1.常規登錄網站會在cookie中寫入登錄信息,在登陸成功之后,返回的響應頭里面會帶著set-cookie字樣,后續的請求會在請求頭中加入cookie內容可以用session來自動圍護響應頭中的set-cookie
2. ajax登陸登陸后,從瀏覽器中可能發現響應頭沒有set-cookie信息,但是在后續的請求中存在明顯的cookie信息該情況90%的概率是:cookie通過JavaScript腳本語言動態設置,seesion就不能自動維護了,需要通過程序手工去完成cookie的拼接
3. 依然是ajax請求,也沒有響應頭,也是js和2的區別是,該方式不會把登錄信息放在cookie中,而是放在storage里面。每次請求時從storage中拿出登錄信息放在請求參數中。這種方式則必須要做逆向。該方式有一個統一的解決方案,去找公共攔截器。
? ? ? ? 方法1,直接在settings.py文件中設置請求頭信息。但是由于scrapy(引擎和下載器之間的中間件)會自動管理cookie,因此設置時,也需要將COOKIES_ENABLED設置為False
? ? ? ? ? ? ? ? 方法2,重寫start_requests函數,將cookie作為參數傳入
import scrapy
from scrapy import Requestclass LoginSpiderSpider(scrapy.Spider):name = 'login_spider'allowed_domains = ['blog.csdn.net']start_urls = ['http://blog.csdn.net/']def start_requests(self):cookie_info = ""cookie_dic = {}for item in cookie_info.split(';'):item = item.strip()k,v = item.split('=',1)cookie_dic[k]=v#需要注意的是,這里的cookie要以自己的參數傳入,而不是字符串yield Request(self.start_urls[0],cookies=cookie_dic)def parse(self, response):pass
? ? ? ? 方法3,自己走一個登錄流程,登錄之后,由于scrapy(引擎和下載器之間的中間件)會自己管理cookie信息,所以直接執行start_urls即可。
import scrapy
from scrapy import Requestclass LoginSpiderSpider(scrapy.Spider):name = 'login_spider'allowed_domains = ['blog.csdn.net']start_urls = ['http://blog.csdn.net/']def start_requests(self):login_url = "https://blog.csdn.net/login"data = {'login':'123456','password':'123456'}#但是這里要注意,Request中的body需要傳入的是字符串信息,而不是字典#方法1login_info = []for k,v in data.items():login_info.append(k+"="+v)login_info = '&'.join(login_info)#方法2from urllib.parse import urlencodelogin_info = urlencode(data)yield Request(login_url,method='POST',body=login_info)def parse(self, response):pass
10. Scrapy中間件
? ? ? ? 中間件位于middlewares.py文件中,
11. Scrapy之鏈接url提取器
? ? ? ? 上面提到當爬蟲需要跳轉url時,需要使用urljoin的函數來進行url的憑借,這個操作可以使用LinkExtractor來簡化。
from urllib.request import Request
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy import Request
import reclass LinkSpiderSpider(scrapy.Spider):name = 'link_spider'allowed_domains = ['4399.com']start_urls = ['https://www.4399.com/flash_fl/5_1.htm']def parse(self, response):# print(response.text)game_le = LinkExtractor(restrict_xpaths=("//ul[@class='list affix cf']/li/a",))game_links = game_le.extract_links(response)for game_link in game_links:# print(game_link.url)yield Request(url=game_link.url,callback=self.game_name_date)if '5_1.htm' in response.url:page_le = LinkExtractor(restrict_xpaths=("//div[@class='bre m15']//div[@class='pag']/a",))else:page_le = LinkExtractor(restrict_xpaths=("//div[@class='pag']/a",))page_links = page_le.extract_links(response)for page_link in page_links:# print(page_link.url)yield Request(url=page_link.url,callback=self.parse)def game_name_date(self,response):try:name = response.xpath('//*[@id="skinbody"]/div[7]/div[1]/div[1]/div[2]/div[1]/h1/a/text()')info = response.xpath('//*[@id="skinbody"]/div[7]/div[1]/div[1]/div[2]/div[2]/text()')if not info:info = response.xpath('//*[@id="skinbody"]/div[6]/div[1]/div[1]/div[2]/div[2]/text()')# print(name,info)# print(1)name = name.extract_first()infos = info.extract()[1].strip()size = re.search(r'大小:(.*?)M',infos).group(1)date = re.search(r'日期:(\d{4}-\d{2}-\d{2})',infos).group(1)yield {'name':name,'size':size+'M','date':date}except Exception as e:print(e,info,response.url)
12. 增量式爬蟲
? ? ? ? 當爬取的數據中包含之前訪問過的數據時,需要對url進行判斷,以保證不重復爬取。增量式爬蟲不能將中間數據存儲在內存級別的存儲,只能選擇硬盤上的存儲。
import scrapy
from redis import Redis
from scrapy import Request,signalsclass ZengliangSpiderSpider(scrapy.Spider):name = 'zengliang_spider'allowed_domains = ['4399.com']start_urls = ['http://4399.com/']#觀察到middlewares中間間中的寫法,想要減少程序連接redis數據庫的次數@classmethoddef from_crawler(cls, crawler):# This method is used by Scrapy to create your spiders.s = cls()#如果遇到Crawler中找不到當前spider時,可以參考父類中的寫法,將去copy過來#s._set_crawler(crawler)crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)return sdef spider_opened(self, spider):self.red = Redis(host='',port=123,db=3,password='')def spider_closed(self,spider):self.red.save()self.red.close()def parse(self, response):hrefs = response.xpath('').extract()for href in hrefs:href = response.urljoin(href)if self.red.sismember('search_path',href):continueyield Request(url=href,callback=self.new_parse,meta={'href':href} #防止url重定向)def new_parse(self,response):href = response.meta.get('href')self.red.sadd('save_path',href)pass
13. 分布式爬蟲
? ? ? ? scrapy可以借助scrapy-redis插件來進行分布式爬蟲,但要注意兩個庫的版本問題。
? ? ? ? 與普通的scrapy不同,redis版本的在spider文件中繼承時采用redis的繼承。
from scrapy_redis.spiders import RedisSpiderclass FbSpider(RedisSpider):name = 'fb'allowed_domains = ['4399.com']redis_key = "path"def parse(self, response):pass
? ? ? ? 同時,需要在settings.py中設置redis相關的信息。
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER_PERSIST = TrueITEM_PIPELINES = {'fenbu.pipelines.FenbuPipeline': 300,'scrapy_redis.pipelines.RedisPipeline':301
}
REDIS_HOST = ''
REDIS_PORT = ''
REDIS_DB = ''
REDIS_PARAMS = {'':''
}
以上這些就是我關于scrapy爬蟲的基本學習,有疑問可以相互交流。