Scrapy是一個爲了爬取網站數據,提取結構性數據而編寫的應用框架,很是出名,很是強悍。所謂的框架就是一個已經被集成了各類功能(高性能異步下載,隊列,分佈式,解析,持久化等)的具備很強通用性的項目模板。php
環境的安裝:html
mac/linux:pip install scrapy window: - pip install wheel - 下載twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted - 進入下載目錄,執行 pip install Twisted‑17.1.0‑cp37‑cp37m‑win_amd64.whl - pip install pywin32 - pip install scrapy
新建一個工程:python
scrapy startproject ProName
ROBOTSTXT_OBEY = False
USER_AGENT = 'xxx'
LOG_LEVEL = 'ERROR'
cd ProName
:進入到工程目錄中scrapy genspider spiderName www.xxx.com
scrapy crawl spiderName
# -*- coding: utf-8 -*- import scrapy # 爬蟲類: 父類 => Spider class FirstSpider(scrapy.Spider): # 爬蟲文件的名稱: 當前爬蟲源文件的惟一標識 name = 'first' # 容許的域名, 限定爬取的域名 # allowed_domains = ['www.baidu.com'] # 起始的url列表, 列表中存放的url均可以被scrapy進行異步的網絡請求 start_urls = ['https://www.baidu.com/', 'https://www.taobao.com/'] # 用做與數據解析 # 參數: response就是響應對象 def parse(self, response): print(response)
scrapy的數據解析與持久化存儲:mysql
需求: 爬取 https://dig.chouti.com/ 的文章標題與做者linux
extract()與extract_first(): 從xpath查詢結果對象(Selector)中取得數據web
scrapy的持久化存儲正則表達式
基於終端指令進行持久化存儲redis
只能夠將parse方法的返回值存儲到本地的磁盤文件(指定形式後綴)中sql
支持: json
, jsonlines
, jl
, csv
, xml
, marshall
, pickle
chrome
scrapy crawl spiderName -o filePath
# 用做與數據解析 # 參數: response就是響應對象 def parse(self, response): div_list = response.xpath('/html/body/main/div/div/div[1]/div/div[2]/div[1]/div') all_data = [] for div in div_list: # 解析標題和做者信息 # xpath在取標籤中存儲的文本數據時必需要使用extract(),extract_first()進行字符串的單獨提取 title = div.xpath('.//div[@class="link-detail"]/a/text()')[0].extract() author = div.xpath('.//span[@class="left author-name"]/text()').extract_first() all_data.append({'title': title, 'author': author}) # 基於終端指令的持久化存儲 # scrapy crawl spiderName -o filePath return all_data
基於管道進行持久化存儲(經常使用)
編碼流程:
# settings.py # 開啓管道,註冊管道 ITEM_PIPELINES = { # 300表示優先級,數值越小優先級越高 'firstblood.pipelines.FirstbloodPipeline': 300, }
# items.py import scrapy class FirstbloodItem(scrapy.Item): # define the fields for your item here like: # name = scrapy.Field() # Field類型視爲一個萬能的數據類型 title = scrapy.Field() author = scrapy.Field()
# pipelines.py import pymysql class FirstbloodPipeline(object): conn = None cursor = None # 重寫父類方法,該方法只會在爬蟲開始時被執行一次 def open_spider(self, spider): self.conn = pymysql.Connection(host='127.0.0.1', port=3306, user='root', password='123', db='scrapy', charset='utf8') # process_item方法調用後就能夠接收爬蟲類提交來的item對象 def process_item(self, item, spider): title = item['title'] author = item['author'] sql = 'insert into chouti values (%s, %s)' self.cursor = self.conn.cursor() # 事務處理 try: self.cursor.execute(sql, [author, title]) self.conn.commit() except Exception as e: print(e) # 回滾事務 self.conn.rollback() return item # 將item傳遞給下個管道類 def close_spider(self, spider): self.cursor.close() self.conn.close()
# first.py # -*- coding: utf-8 -*- import scrapy from firstblood.items import FirstbloodItem class FirstSpider(scrapy.Spider): name = 'first' start_urls = ['https://dig.chouti.com/'] # 用做與數據解析 # 參數: response就是響應對象 def parse(self, response): div_list = response.xpath('/html/body/main/div/div/div[1]/div/div[2]/div[1]/div') for div in div_list: title = div.xpath('.//div[@class="link-detail"]/a/text()')[0].extract() author = div.xpath('.//span[@class="left author-name"]/text()').extract_first() # 實例化一個item類型的對象 item = FirstbloodItem() # 給item對象的屬性賦值 item['title'] = title item['author'] = author yield item # 將item提交給管道
案例:
爬取http://www.521609.com/daxuexiaohua/的圖片
scrapy中封裝了一個管道類(ImagesPipeline
),基於該管道類,能夠實現圖片資源的請求和永久化存儲
編碼流程:
1.在爬蟲文件中解析出圖片的地址
2.將圖片地址封裝到item中且提交給管道
import scrapy from getImg.items import GetimgItem class ImgSpider(scrapy.Spider): name = 'Img' # allowed_domains = ['www.xxx.com'] start_urls = ['http://www.521609.com/daxuemeinv/'] # 定義一個通用的url模板 url = 'http://www.521609.com/daxuemeinv/list8%d.html' pageNum = 1 def parse(self, response): li_list = response.xpath('//*[@id="content"]/div[2]/div[2]/ul/li') for li in li_list: img_src = 'http://www.521609.com' + li.xpath('./a[1]/img/@src').extract_first() item = GetimgItem() item['img_src'] = img_src yield item if self.pageNum < 5: self.pageNum += 1 new_url = self.url % self.pageNum # 手動發請求,實現深度爬取 yield scrapy.Request(new_url, callback=self.parse)
3.管道文件中自定義一個管道類(繼承ImagesPipeline
)
4.重寫三個方法:
def get_media_requests(self, item, info):
def file_path(self, request, response=None, info=None):
def item_completed(self, results, item, info):
import scrapy from scrapy.pipelines.images import ImagesPipeline class GetimgPipeline(ImagesPipeline): # 該方法是用做與請求發送的 def get_media_requests(self, item, info): # 對item中的img_src進行請求發送 # 手動進行請求發送 yield scrapy.Request(url=item['img_src']) # 用於指定文件路徑(文件夾 + 文件名稱) def file_path(self, request, response=None, info=None): # 存到settings中指定的文件夾中 return request.url.split('/')[-1] # 將當前的item提交給下個管道類 def item_completed(self, results, item, info): return item
5.在配置文件中開啓管道且加上IMAGES_STORE配置
# 自定義文件夾路徑 IMAGES_STORE = './imgLibs'
在scrapy中如何進行手動請求發送?
yield scrapy.Request(url, callback)
在scrapy中如何進行post請求的發送?
# formdata是post提交的數據 yield scrapy.FormRequest(url, callback, formdata={})
如何對起始的url進行post請求的發送?
# 重寫父類的start_requests(self)方法 def start_requests(self): for url in self.start_urls: yield scrapy.FormRequest(url, callback=self.parse, formdata={})
增長併發: 默認scrapy開啓的併發線程爲32個,能夠適當進行增長。在settings配置文件中修改CONCURRENT_REQUESTS = 100值爲100,併發設置成了爲100。 下降日誌級別: 在運行scrapy時,會有大量日誌信息的輸出,爲了減小CPU的使用率。能夠設置log輸出信息爲INFO或者ERROR便可。在配置文件中編寫:LOG_LEVEL = ‘ERROR’ 禁止cookie: 若是不是真的須要cookie,則在scrapy爬取數據時能夠禁止cookie從而減小CPU的使用率,提高爬取效率。在配置文件中編寫:COOKIES_ENABLED = False 禁止重試: 對失敗的HTTP進行從新請求(重試)會減慢爬取速度,所以能夠禁止重試。在配置文件中編寫:RETRY_ENABLED = False 減小下載超時: 若是對一個很是慢的連接進行爬取,減小下載超時能夠能讓卡住的連接快速被放棄,從而提高效率。在配置文件中進行編寫:DOWNLOAD_TIMEOUT = 1 超時時間爲10s
什麼叫深度爬取?
如何實現請求傳參?
Request(url,callback,meta={}):能夠將meta字典傳遞給callback
callback接收item:response.meta
需求:
爬取https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1.html的每一頁的電影名稱和簡介.
import scrapy from moviePro.items import MovieproItem class MovieSpider(scrapy.Spider): name = 'movie' # allowed_domains = ['www.xxx.com'] start_urls = ['https://www.4567tv.tv/index.php/vod/show/class/%E5%8A%A8%E4%BD%9C/id/1.html'] # 通用url模板 url = 'https://www.4567tv.tv/index.php/vod/show/class/動做/id/1/page/%d.html' pageNum = 2 # 解析電影名稱和詳情頁的url def parse(self, response): li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li') for li in li_list: title = li.xpath('./div/div/h4/a/text()').extract_first() detail_url = 'https://www.4567tv.tv' + li.xpath('./div/div/h4/a/@href').extract_first() item = MovieproItem() item['title'] = title # 手動請求發送, 將meta字典傳遞給指定的callback(請求傳參) yield scrapy.Request(detail_url, callback=self.parse_detailxxx, meta={'item': item}) if self.pageNum < 5: new_url = format(self.url % self.pageNum) self.pageNum += 1 yield scrapy.Request(new_url, callback=self.parse) # 解析詳情頁中的電影簡介 def parse_detailxxx(self, response): item = response.meta['item'] desc = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[2]/text()').extract_first() item['desc'] = desc yield item
Scrapy Engine(引擎): 負責Spider、ItemPipeline、Downloader、Scheduler中間的通信,信號、數據傳遞等。
Scheduler(調度器): 它負責接受引擎發送過來的Request請求,並按照必定的方式進行整理排列,入隊,當引擎須要時,交還給引擎。
Downloader(下載器):負責下載Scrapy Engine(引擎)發送的全部Requests請求,並將其獲取到的Responses交還給Scrapy Engine(引擎),由引擎交給Spider來處理,
Spider(爬蟲):它負責處理全部Responses,從中分析提取數據,獲取Item字段須要的數據,並將須要跟進的URL提交給引擎,再次進入Scheduler(調度器).
Item Pipeline(管道):它負責處理Spider中獲取到的Item,並進行進行後期處理(詳細分析、過濾、存儲等)的地方。
Downloader Middlewares(下載中間件):你能夠看成是一個能夠自定義擴展下載功能的組件。 (經常使用,能夠取到request和response)
Spider Middlewares(Spider中間件):你能夠理解爲是一個能夠自定擴展和操做引擎和Spider中間通訊的功能組件(好比進入Spider的Responses;和從Spider出去的Requests)
執行流程:
1 引擎:Hi!Spider, 你要處理哪個網站? 2 Spider:老大要我處理xxxx.com。 3 引擎:你把第一個須要處理的URL給我吧。 4 Spider:給你,第一個URL是xxxxxxx.com。 5 引擎:Hi!調度器,我這有request請求你幫我排序入隊一下。 6 調度器:好的,正在處理你等一下。 7 引擎:Hi!調度器,把你處理好的request請求給我。 8 調度器:給你,這是我處理好的request 9 引擎:Hi!下載器,你按照老大的下載中間件的設置幫我下載一下這個request請求 10 下載器:好的!給你,這是下載好的東西。(若是失敗:sorry,這個request下載失敗了。而後引擎告訴調度器,這個request下載失敗了,你記錄一下,咱們待會兒再下載) 11 引擎:Hi!Spider,這是下載好的東西,而且已經按照老大的下載中間件處理過了,你本身處理一下(注意!這兒responses默認是交給def parse()這個函數處理的) 12 Spider:(處理完畢數據以後對於須要跟進的URL),Hi!引擎,我這裏有兩個結果,這個是我須要跟進的URL,還有這個是我獲取到的Item數據。 13 引擎:Hi !管道 我這兒有個item你幫我處理一下!調度器!這是須要跟進URL你幫我處理下。而後從第四步開始循環,直到獲取完老大須要所有信息。 14 管道調度器:好的,如今就作! 注意!只有當調度器中不存在任何request了,整個程序纔會中止,(也就是說,對於下載失敗的URL,Scrapy也會從新下載。)
scrapy有哪些中間件?
下載中間件的做用:
爲何攔截請求?
request.headers['User-Agent'] = 'xxxxx'
request.meta['proxy'] = 'http://ip:port'
爲何攔截響應?
案例分析:
爬取網易新聞(國內,國際,軍事,航空,無人機)新聞數據的標題和內容,而且調用百度AI對文章內容進行標籤提取和文章分類
分析:
1.每個板塊下對應的新聞數據都是動態加載出來的
2.會對五個板塊的響應數據進行數據解析,可是板塊對應的響應對象是不包含動態加載的新聞數據,目前獲取的每個板塊對應的響應對象是不知足需求的響應對象
3.將不知足需求的5個響應對象(工程中一共會有1+5+n),修改爲知足需求的響應對象。
# 爬蟲文件中 import scrapy from NewsPro.items import NewsproItem from selenium import webdriver import time from aip import AipNlp class NewsSpider(scrapy.Spider): name = 'news' # allowed_domains = ['https://news.163.com'] start_urls = ['https://news.163.com/'] # 5個板塊的url model_urls = [] # 打開selenium browser = webdriver.Chrome(executable_path='./chromedriver.exe') time.sleep(1) # 解析出每一個板塊對應的url def parse(self, response): li_index = [3, 4, 6, 7, 8] li_list = response.xpath('//div[@class="ns_area list"]/ul/li') for index in li_index: # 解析到每一個板塊的url model_url = li_list[index].xpath('./a/@href').extract_first() self.model_urls.append(model_url) # 對板塊的url進行請求發送獲取每個板塊對應的頁面源碼數據 yield scrapy.Request(model_url, self.parse_model) # 請求每一個板塊的頁面源碼數據,解析出新聞的標題和詳情頁url def parse_model(self, response): div_list = response.xpath('//div[@class="ndi_main"]/div') for div in div_list: item = NewsproItem() new_url = div.xpath('./div/div[1]/h3/a/@href').extract_first() title = div.xpath('./div/div[1]/h3/a/text()').extract_first() if new_url and title: item['title'] = title # 對新聞的詳情頁進行請求發送,獲取詳情頁的頁面源碼數據 yield scrapy.Request(new_url, self.parse_new, meta={'item': item}) # 解析新聞詳情頁數據,並提交給管道 def parse_new(self, response): content = response.xpath('//*[@id="endText"]//text()').extract() content = ''.join(content) item = response.meta['item'] item['content'] = content yield item # 關閉selenium瀏覽器 def closed(self, spider): self.browser.quit()
from scrapy import signals import random from time import sleep from scrapy.http import HtmlResponse user_agent_list = ['xxx', 'xxx'] class NewsproDownloaderMiddleware(object): def process_request(self, request, spider): # 經過中間件進行UA假裝,儘可能每次使用不一樣的UA request.headers['User-Agent'] = random.choice(user_agent_list) return None def process_response(self, request, response, spider): # selenium對象 browser = spider.browser url = request.url # 判斷是否5個模塊的url if url in spider.model_urls: browser.get(url) sleep(1) # 下來滾軸,獲取所有的動態加載數據 browser.execute_script('window.scrollTo(0,document.body.scrollHeight)') sleep(1) browser.execute_script('window.scrollTo(0,document.body.scrollHeight)') sleep(1) browser.execute_script('window.scrollTo(0,document.body.scrollHeight)') sleep(1) page_text = browser.page_source # 封裝一個response對象,替代不符合要求的response對象 response = HtmlResponse(url=request.url, body=page_text, encoding='utf-8', request=request) return response
用途: 便捷的實現全站數據爬取
scrapy startproject proName
cd proName
scrapy genspider -t crawl spiderName www.xxx.com
核心:
連接提取器(LinkExtractor)
規則解析器(Rule)
follow=True
: 將連接提取器繼續做用到連接提取到的連接所對應的頁面源碼中,實現深度爬取案例:
爬取http://wz.sun0769.com/html/top/report.shtml中全部頁文章的標題和內容
class LxSpider(CrawlSpider): name = 'lx' # allowed_domains = ['www.xxx.com'] start_urls = ['http://wz.sun0769.com/html/top/report.shtml'] # 連接提取器 link = LinkExtractor(allow=r'page=\d+') link_1 = LinkExtractor(allow=r'page=$') link_detail = LinkExtractor(allow=r'question/\d+/\d+\.shtml') rules = ( # 實例化一個Rule(規則解析器)對象 # follow=True: 將連接提取器 繼續 做用到連接提取到的連接所對應的頁面源碼中 Rule(link_1, callback='parse_item', follow=False), Rule(link, callback='parse_item', follow=True), Rule(link_detail, callback='parse_item_detail', follow=False), ) # 數據解析,是用來解析連接提取器提取到的連接所對應的頁面 def parse_item(self, response): tr_list = response.xpath('/html/body/div[8]/table[2]//tr') for tr in tr_list: title = tr.xpath('./td[3]/a/text()').extract_first() num = tr.xpath('./td[1]/text()').extract_first() item = Item2() item['title'] = title item['num'] = num yield item # 解析詳情頁中的新聞內容 def parse_item_detail(self, response): content = ''.join(response.xpath('/html/body/div[9]/table[2]//tr[1]/td//text()').extract()) num = response.xpath('/html/body/div[9]/table[1]//tr/td[2]/span[2]/text()').extract_first().split(':')[-1] item = Item1() item['content'] = content item['num'] = num yield item
注意, 若是想將不一樣頁面的數據統一保存,使用Crawl Spider不是很方便,由於它沒法進行請求傳參.
什麼是分佈式?
須要搭建一個由n臺電腦組成的機羣,而後在每一臺電腦中執行同一組程序,讓其對同一個網絡資源進行聯合且分佈的數據爬取,大幅度提高爬取效率
實現方式:
scrapy
+ scrapy_redis
組件實現的分佈式.注意, 原生的scrapy是不能夠實現分佈式的,爲何?
scrapy-redis
組件的做用是什麼?
實現流程:
pip install scrapy-redis
scrapy startproject proName
cd proName
修改爬蟲文件
1. 導入: from scrapy_redis.spiders import RedisCrawlSpider from scrapy_redis.spiders import RedisSpider 將當前爬蟲類的父類修改成導入的類 刪除allowed_domains和start_urls 添加一個redis_key = 'xxx'屬性,表示調度器隊列的名稱 根據常規形式編寫爬蟲文件後續的代碼
修改settings.py配置文件
# 指定管道 ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 400 } # 指定調度器 # 增長了一個去重容器類的配置, 做用使用Redis的set集合來存儲請求的指紋數據, 從而實現請求去重的持久化 DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" # 使用scrapy-redis組件本身的調度器 SCHEDULER = "scrapy_redis.scheduler.Scheduler" # 配置調度器是否要持久化, 也就是當爬蟲結束了, 要不要清空Redis中請求隊列和去重指紋的set。若是是True, 就表示要持久化存儲, 就不清空數據, 不然清空數據 SCHEDULER_PERSIST = True # 指定redis數據庫 REDIS_HOST = '127.0.0.1' REDIS_PORT = 6379
修改redis的配置文件redis.windows.conf
關閉默認綁定 56行: # bind 127.0.0.1 關閉保護模式 75行: protected-mode no
啓動redis的服務端(攜帶配置文件)和客戶端
啓動分佈式的程序
cd 到spiders文件夾內 scrapy runspider xxx.py
向調度器的隊列中添加一個起始的url
隊列是存在於redis中的 redis的客戶端中: lpush sun www.xxx.com
案例:
# spiders文件中 from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from scrapy_redis.spiders import RedisCrawlSpider from fenbushi.items import FenbushiItem class FbsSpider(RedisCrawlSpider): name = 'fbs' # allowed_domains = ['www.xxx.com'] # start_urls = ['http://www.xxx.com/'] redis_key = 'sun' # 可被共享的調度器隊列的名稱,即須要添加起始url的redis中的set link = LinkExtractor(allow=r'page=\d+') rules = ( Rule(link, callback='parse_item', follow=True), ) def parse_item(self, response): tr_list = response.xpath('/html/body/div[8]/table[2]//tr') for tr in tr_list: title = tr.xpath('./td[3]/a/text()').extract_first() item = FenbushiItem() item['title'] = title yield item
概念: 監測網站數據更新
核心技術: 去重
適合使用增量式的網站:
記錄表是以怎樣的形式存在於哪?
推薦使用redis的set充當記錄表
案例:
監測https://www.4567tv.tv/index.php/vod/show/id/1/page/1.html的電影標題與簡介
import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule from redis import Redis from zlsPro.items import ZlsproItem class ZlsSpider(CrawlSpider): name = 'zls' start_urls = ['https://www.4567tv.tv/index.php/vod/show/id/1/page/1.html'] # 解析每一頁的頁碼連接 link = LinkExtractor(allow=r'1/page/\d+\.html') rules = ( Rule(link, callback='parse_item', follow=False), ) conn = Redis(host='127.0.0.1', port=6379) def parse_item(self, response): li_list = response.xpath('/html/body/div[1]/div/div/div/div[2]/ul/li') for li in li_list: detail_url = 'https://www.4567tv.tv' + li.xpath('./div/a/@href').extract_first() ex = self.conn.sadd('urls', detail_url) if ex == 1: # detail_url不存在於redis的set中 yield scrapy.Request(detail_url, self.parse_detail) else: # detail_url存在於redis的set中 print('無數據更新') def parse_detail(self, response): title = response.xpath('/html/body/div[1]/div/div/div/div[2]/h1/text()').extract_first() desc = response.xpath('/html/body/div[1]/div/div/div/div[2]/p[5]/span[3]/text()').extract_first() print(title, '是新數據') item = ZlsproItem() item['title'] = title item['desc'] = desc yield item
robots: 無論它..
UA檢測: UA假裝
圖片懶加載: 獲取相應的僞屬性對應的值
驗證碼: 打碼平臺,selenium
cookie: session對象自動記錄cookie
動態加載的數據: 使用抓包工具,獲取動態加載數據對應的url
動態變化的請求參數: 通常隱藏在頁面源碼中
IP檢測: IP代理池
js混淆: js反混淆
js加密: 找到對應的js代碼,使用PyExecJS運行js代碼