何爲框架,就至關於一個封裝了不少功能的結構體,它幫咱們把主要的結構給搭建好了,咱們只需往骨架裏添加內容就行。scrapy框架是一個爲了爬取網站數據,提取數據的框架,咱們熟知爬蟲總共有四大部分,請求、響應、解析、存儲,scrapy框架都已經搭建好了。scrapy是基於twisted框架開發而來,twisted是一個流行的事件驅動的python網絡框架,scrapy使用了一種非阻塞(又名異步)的代碼實現併發的,Scrapy之因此能實現異步,得益於twisted框架。twisted有事件隊列,哪個事件有活動,就會執行!Scrapy它集成高性能異步下載,隊列,分佈式,解析,持久化等。css
1.五大核心組件html
引擎(Scrapy)python
框架核心,用來處理整個系統的數據流的流動, 觸發事務(判斷是何種數據流,而後再調用相應的方法)。也就是負責Spider、ItemPipeline、Downloader、Scheduler中間的通信,信號、數據傳遞等,因此被稱爲框架的核心。git
調度器(Scheduler)github
用來接受引擎發過來的請求,並按照必定的方式進行整理排列,放到隊列中,當引擎須要時,交還給引擎。能夠想像成一個URL(抓取網頁的網址或者說是連接)的優先隊列, 由它來決定下一個要抓取的網址是什麼, 同時去除重複的網址。web
下載器(Downloader)ajax
負責下載引擎發送的全部Requests請求,並將其獲取到的Responses交還給Scrapy Engine(引擎),由引擎交給Spider來處理。Scrapy下載器是創建在twisted這個高效的異步模型上的。shell
爬蟲(Spiders)數據庫
用戶根據本身的需求,編寫程序,用於從特定的網頁中提取本身須要的信息,即所謂的實體(Item)。用戶也能夠從中提取出連接,讓Scrapy繼續抓取下一個頁面。跟進的URL提交給引擎,再次進入Scheduler(調度器)。express
項目管道(Pipeline)
負責處理爬蟲提取出來的item,主要的功能是持久化實體、驗證明體的有效性、清除不須要的信息。
2.工做流程
Scrapy中的數據流由引擎控制,其過程以下:
(1)用戶編寫爬蟲主程序將須要下載的頁面請求requests遞交給引擎,引擎將請求轉發給調度器;
(2)調度實現了優先級、去重等策略,調度從隊列中取出一個請求,交給引擎轉發給下載器(引擎和下載器中間有中間件,做用是對請求加工如:對requests添加代理、ua、cookie,response進行過濾等);
(3)下載器下載頁面,將生成的響應經過下載器中間件發送到引擎;
(4) 爬蟲主程序進行解析,這個時候解析函數將產生兩類數據,一種是items、一種是連接(URL),其中requests按上面步驟交給調度器;items交給數據管道(數據管道實現數據的最終處理);
官方文檔
英文版:https://docs.scrapy.org/en/latest/
http://doc.scrapy.org/en/master/
中文版:https://scrapy-chs.readthedocs.io/zh_CN/latest/intro/overview.html
https://www.osgeo.cn/scrapy/topics/architecture.html
1. 安裝
Linux:pip3 install scrapy
Windows:
a. pip3 install wheel
b. 下載twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
c. shift右擊進入下載目錄,執行 pip3 install typed_ast-1.4.0-cp36-cp36m-win32.whl
d. pip3 install pywin32
e. pip3 install scrapy
2.scrapy基本命令行
(1)建立一個新的項目 scrapy startproject ProjectName (2)生成爬蟲 scrapy genspider +SpiderName+website (3)運行(crawl) # -o output scrapy crawl +SpiderName scrapy crawl SpiderName -o file.json scrapy crawl SpiderName-o file.csv (4)check檢查錯誤 scrapy check (5)list返回項目全部spider名稱 scrapy list (6)view 存儲、打開網頁 scrapy view https://www.baidu.com (7)scrapy shell,進入終端 scrapy shell https://www.baidu.com (8)scrapy runspider scrapy runspider zufang_spider.py
以麥田租房信息爬取爲例,網站http://bj.maitian.cn/zfall/PG1
1.建立項目
scrapy startproject houseinfo
生成項目結構:
scrapy.cfg 項目的主配置信息。(真正爬蟲相關的配置信息在settings.py文件中)
items.py 設置數據存儲模板,用於結構化數據,如:Django的Model
pipelines 數據持久化處理
settings.py 配置文件
spiders 爬蟲目錄
2.建立爬蟲應用程序
cd houseinfo
scrapy genspider maitian maitian.com
而後就能夠在spiders目錄下看到咱們的爬蟲主程序
3.編寫爬蟲文件
步驟2執行完畢後,會在項目的spiders中生成一個應用名的py爬蟲文件,文件源碼以下:
1 # -*- coding: utf-8 -*- 2 import scrapy 3 4 5 class MaitianSpider(scrapy.Spider): 6 name = 'maitian' # 應用名稱 7 allowed_domains = ['maitian.com'] #通常註釋掉,容許爬取的域名(若是遇到非該域名的url則爬取不到數據) 8 start_urls = ['http://maitian.com/'] #起始爬取的url列表,該列表中存在的url,都會被parse進行請求的發送 9 10 #解析函數 11 def parse(self, response): 12 pass
咱們能夠在此基礎上,根據需求進行編寫
1 # -*- coding: utf-8 -*- 2 import scrapy 3 4 class MaitianSpider(scrapy.Spider): 5 name = 'maitian' 6 start_urls = ['http://bj.maitian.cn/zfall/PG100'] 7 8 9 #解析函數 10 def parse(self, response): 11 12 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 13 results = [] 14 for li in li_list: 15 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip() 16 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip() 17 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡','') # 將面積的單位去掉 18 area = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[0] # 以空格分隔 19 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 20 21 dict = { 22 "標題":title, 23 "月租金":price, 24 "面積":square, 25 "區域":area, 26 "地址":adress 27 } 28 results.append(dict) 29 30 print(title,price,square,area,adress) 31 return results
須知:
二者等同,都是將列表中的內容提取出來
title = li.xpath('./div[2]/h1/a/text()').extract_first().strip()
title = li.xpath('./div[2]/h1/a/text()')[0].extract().strip()
4. 設置修改settings.py配置文件相關配置:
1 #假裝請求載體身份 2 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' 3 4 #能夠忽略或者不遵照robots協議 5 ROBOTSTXT_OBEY = False
5.執行爬蟲程序:scrapy crawl maitain
爬取全站數據,也就是所有頁碼數據。本例中,總共100頁,觀察頁面之間的共性,構造通用url
方式一:經過佔位符,構造通用url
1 import scrapy 2 3 class MaitianSpider(scrapy.Spider): 4 name = 'maitian' 5 start_urls = ['http://bj.maitian.cn/zfall/PG{}'.format(page) for page in range(1,4)] #注意寫法 6 7 8 #解析函數 9 def parse(self, response): 10 11 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 12 results = [] 13 for li in li_list: 14 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip() 15 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip() 16 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡','') 17 # 也能夠經過正則匹配提取出來 18 area = li.xpath('./div[2]/p[2]/span/text()[2]')..re(r'昌平|朝陽|東城|大興|豐臺|海淀|石景山|順義|通州|西城')[0] 19 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 20 21 dict = { 22 "標題":title, 23 "月租金":price, 24 "面積":square, 25 "區域":area, 26 "地址":adress 27 } 28 results.append(dict) 29 30 return results
若是碰到一個表達式不能包含全部狀況的項目,解決方式是先分別寫表達式,最後經過列表相加,將全部url合併成一個url列表,例如
start_urls = ['http://www.guokr.com/ask/hottest/?page={}'.format(n) for n in range(1, 8)] + [ 'http://www.guokr.com/ask/highlight/?page={}'.format(m) for m in range(1, 101)]
方式二:經過重寫start_requests方法,獲取全部的起始url。(不用寫start_urls
)
1 import scrapy 2 3 class MaitianSpider(scrapy.Spider): 4 name = 'maitian' 5 6 def start_requests(self): 7 pages=[] 8 for page in range(90,100): 9 url='http://bj.maitian.cn/zfall/PG{}'.format(page) 10 page=scrapy.Request(url) 11 pages.append(page) 12 return pages 13 14 #解析函數 15 def parse(self, response): 16 17 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 18 19 results = [] 20 for li in li_list: 21 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(), 22 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(), 23 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''), 24 area = li.xpath('./div[2]/p[2]/span/text()[2]').re(r'昌平|朝陽|東城|大興|豐臺|海淀|石景山|順義|通州|西城')[0], 25 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 26 27 dict = { 28 "標題":title, 29 "月租金":price, 30 "面積":square, 31 "區域":area, 32 "地址":adress 33 } 34 results.append(dict) 35 36 return results
只要是數據持久化存儲,parse方法必須有返回值(也就是return後的內容)
1. 基於終端指令的持久化存儲
執行輸出指定格式進行存儲:將爬取到的數據寫入不一樣格式的文件中進行存儲,windows終端不能使用txt格式
以麥田爲例,spider中的代碼不變,將返回值寫到qiubai.csv中。本地沒有,就會本身建立一個。本地有就會追加
scrapy crawl maitian -o maitian.csv
就會在項目目錄下看到,生成的文件
查看文件內容
2.基於管道的持久化存儲
scrapy框架中已經爲咱們專門集成好了高效、便捷的持久化操做功能,咱們直接使用便可。要想使用scrapy的持久化操做功能,咱們首先來認識以下兩個文件:
持久化流程:
① 爬蟲文件爬取到數據解析後,須要將數據封裝到items對象中。
② 使用yield關鍵字將items對象提交給pipelines管道,進行持久化操做。
③ 在管道文件中的process_item方法中接收爬蟲文件提交過來的item對象,而後編寫持久化存儲的代碼,將item對象中存儲的數據進行持久化存儲(在管道的process_item方法中執行io操做,進行持久化存儲)
④ settings.py配置文件中開啓管道
2.1保存到本地的持久化存儲
爬蟲文件:maitian.py
1 import scrapy 2 from houseinfo.items import HouseinfoItem # 將item導入 3 4 class MaitianSpider(scrapy.Spider): 5 name = 'maitian' 6 start_urls = ['http://bj.maitian.cn/zfall/PG100'] 7 8 #解析函數 9 def parse(self, response): 10 11 li_list = response.xpath('//div[@class="list_wrap"]/ul/li') 12 13 for li in li_list: 14 item = HouseinfoItem( 15 title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(), 16 price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(), 17 square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''), 18 area = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[0], 19 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] 20 ) 21 22 yield item # 提交給管道,而後管道定義存儲方式
items文件:items.py
1 import scrapy 2 3 class HouseinfoItem(scrapy.Item): 4 title = scrapy.Field() #存儲標題,裏面能夠存儲任意類型的數據 5 price = scrapy.Field() 6 square = scrapy.Field() 7 area = scrapy.Field() 8 adress = scrapy.Field()
管道文件:pipelines.py
1 class HouseinfoPipeline(object): 2 def __init__(self): 3 self.file = None 4 5 #開始爬蟲時,執行一次 6 def open_spider(self,spider): 7 self.file = open('maitian.csv','a',encoding='utf-8') # 選用了追加模式 8 self.file.write(",".join(["標題","月租金","面積","區域","地址","\n"])) 9 print("開始爬蟲") 10 11 # 由於該方法會被執行調用屢次,因此文件的開啓和關閉操做寫在了另外兩個只會各自執行一次的方法中。 12 def process_item(self, item, spider): 13 content = [item["title"], item["price"], item["square"], item["area"], item["adress"], "\n"] 14 self.file.write(",".join(content)) 15 return item 16 17 # 結束爬蟲時,執行一次 18 def close_spider(self,spider): 19 self.file.close() 20 print("結束爬蟲")
配置文件:settings.py
1 #假裝請求載體身份 2 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' 3 4 #能夠忽略或者不遵照robots協議 5 ROBOTSTXT_OBEY = False 6 7 #開啓管道 8 ITEM_PIPELINES = { 9 'houseinfo.pipelines.HouseinfoPipeline': 300, #數值300表示爲優先級,值越小優先級越高 10 }
要求:
1.爬取豆瓣top 250電影名字、演員列表、評分和簡介
2.設置隨機UserAgent和Proxy
3.爬取到的數據保存到MongoDB數據庫
items.py
# -*- coding: utf-8 -*- import scrapy class DoubanItem(scrapy.Item): # define the fields for your item here like: # 標題 title = scrapy.Field() # 信息 bd = scrapy.Field() # 評分 star = scrapy.Field() # 簡介 quote = scrapy.Field()
doubanmovie.py
# -*- coding: utf-8 -*- import scrapy from douban.items import DoubanItem class DoubamovieSpider(scrapy.Spider): name = "doubanmovie" allowed_domains = ["movie.douban.com"] offset = 0 url = "https://movie.douban.com/top250?start=" start_urls = ( url+str(offset), ) def parse(self, response): item = DoubanItem() movies = response.xpath("//div[@class='info']") for each in movies: # 標題 item['title'] = each.xpath(".//span[@class='title'][1]/text()").extract()[0] # 信息 item['bd'] = each.xpath(".//div[@class='bd']/p/text()").extract()[0] # 評分 item['star'] = each.xpath(".//div[@class='star']/span[@class='rating_num']/text()").extract()[0] # 簡介 quote = each.xpath(".//p[@class='quote']/span/text()").extract() if len(quote) != 0: item['quote'] = quote[0] yield item if self.offset < 225: self.offset += 25 yield scrapy.Request(self.url + str(self.offset), callback = self.parse)
pipelines.py
# -*- coding: utf-8 -*- import pymongo from scrapy.conf import settings class DoubanPipeline(object): def __init__(self): host = settings["MONGODB_HOST"] port = settings["MONGODB_PORT"] dbname = settings["MONGODB_DBNAME"] sheetname= settings["MONGODB_SHEETNAME"] # 建立MONGODB數據庫連接 client = pymongo.MongoClient(host = host, port = port) # 指定數據庫 mydb = client[dbname] # 存放數據的數據庫表名 self.sheet = mydb[sheetname] def process_item(self, item, spider): data = dict(item) self.sheet.insert(data) return item
settings.py
DOWNLOAD_DELAY = 2.5 COOKIES_ENABLED = False DOWNLOADER_MIDDLEWARES = { 'douban.middlewares.RandomUserAgent': 100, 'douban.middlewares.RandomProxy': 200, } USER_AGENTS = [ 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0)', 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)', 'Opera/9.27 (Windows NT 5.2; U; zh-cn)', 'Opera/8.0 (Macintosh; PPC Mac OS X; U; en)', 'Mozilla/5.0 (Macintosh; PPC Mac OS X; U; en) Opera 8.0', 'Mozilla/5.0 (Linux; U; Android 4.0.3; zh-cn; M032 Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30', 'Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13' ] PROXIES = [ {"ip_port" :"121.42.140.113:16816", "user_passwd" : "****"}, #{"ip_prot" :"121.42.140.113:16816", "user_passwd" : ""} #{"ip_prot" :"121.42.140.113:16816", "user_passwd" : ""} #{"ip_prot" :"121.42.140.113:16816", "user_passwd" : ""} ] ITEM_PIPELINES = { 'douban.pipelines.DoubanPipeline': 300, } # MONGODB 主機名 MONGODB_HOST = "127.0.0.1" # MONGODB 端口號 MONGODB_PORT = 27017 # 數據庫名稱 MONGODB_DBNAME = "Douban" # 存放數據的表名稱 MONGODB_SHEETNAME = "doubanmovies"
middlewares.py
#!/usr/bin/env python # -*- coding:utf-8 -*- import random import base64 from settings import USER_AGENTS from settings import PROXIES # 隨機的User-Agent class RandomUserAgent(object): def process_request(self, request, spider): useragent = random.choice(USER_AGENTS) #print useragent request.headers.setdefault("User-Agent", useragent) class RandomProxy(object): def process_request(self, request, spider): proxy = random.choice(PROXIES) if proxy['user_passwd'] is None: # 沒有代理帳戶驗證的代理使用方式 request.meta['proxy'] = "http://" + proxy['ip_port'] else: # 對帳戶密碼進行base64編碼轉換 base64_userpasswd = base64.b64encode(proxy['user_passwd']) # 對應到代理服務器的信令格式裏 request.headers['Proxy-Authorization'] = 'Basic ' + base64_userpasswd request.meta['proxy'] = "http://" + proxy['ip_port']
爬取多級頁面,會遇到2個問題:
問題1:如何對下一層級頁面發送請求?
答:在每個解析函數的末尾,經過Request方法對下一層級的頁面手動發起請求
# 先提取二級頁面url,再對二級頁面發送請求。多級頁面以此類推 def parse(self, response): next_url = response.xpath('//div[2]/h2/a/@href').extract()[0] # 提取二級頁面url yield scrapy.Request(url=next_url, callback=self.next_parse) # 對二級頁面發送請求,注意要用yield,回調函數不帶括號
問題2:解析的數據不在同一張頁面中,最終如何將數據傳遞
答:涉及到請求傳參,能夠在對下一層級頁面發送請求的時候,經過meta參數進行數據傳遞,meta字典就會傳遞給回調函數的response參數。下一級的解析函數經過response獲取item(先經過 response.meta返回接收到的meta字典,再得到item字典)
# 經過meta參數進行Request的數據傳遞,meta字典就會傳遞給回調函數的response參數 def parse(self, response): item = Item() # 實例化item對象 Item["field1"] = response.xpath('expression1').extract()[0] # 列表中只有一個元素 Item["field2"] = response.xpath('expression2').extract() # 列表 next_url = response.xpath('expression3').extract()[0] # 提取二級頁面url # meta參數:請求傳參.經過meta參數進行Request的數據傳遞,meta字典就會傳遞給回調函數的response參數 yield scrapy.Request(url=next_url, callback=self.next_parse,meta={'item':item}) # 對二級頁面發送請求 def next_parse(self,response): # 經過response獲取item. 先經過 response.meta返回接收到的meta字典,再得到item字典 item = response.meta['item'] item['field'] = response.xpath('expression').extract_first() yield item #提交給管道
案例1:麥田,對全部頁碼發送請求。不推薦將每個頁碼對應的url存放到爬蟲文件的起始url列表(start_urls)中。這裏咱們使用Request方法手動發起請求。
# -*- coding: utf-8 -*- import scrapy from houseinfo.items import HouseinfoItem # 將item導入 class MaitianSpider(scrapy.Spider): name = 'maitian' start_urls = ['http://bj.maitian.cn/zfall/PG1'] #爬取多頁 page = 1 url = 'http://bj.maitian.cn/zfall/PG%d' #解析函數 def parse(self, response): li_list = response.xpath('//div[@class="list_wrap"]/ul/li') for li in li_list: item = HouseinfoItem( title = li.xpath('./div[2]/h1/a/text()').extract_first().strip(), price = li.xpath('./div[2]/div/ol/strong/span/text()').extract_first().strip(), square = li.xpath('./div[2]/p[1]/span[1]/text()').extract_first().replace('㎡',''), area = li.xpath('./div[2]/p[2]/span/text()[2]').re(r'昌平|朝陽|東城|大興|豐臺|海淀|石景山|順義|通州|西城')[0], # 也能夠經過正則匹配提取出來 adress = li.xpath('./div[2]/p[2]/span/text()[2]').extract_first().strip().split('\xa0')[2] ) ['http://bj.maitian.cn/zfall/PG{}'.format(page) for page in range(1, 4)] yield item # 提交給管道,而後管道定義存儲方式 if self.page < 4: self.page += 1 new_url = format(self.url%self.page) # 這裏的%是拼接的意思 yield scrapy.Request(url=new_url,callback=self.parse) # 手動發起一個請求,注意必定要寫yield
案例2:這個案例比較好的一點是,parse函數,既有對下一頁的回調,又有對詳情頁的回調
import scrapy class QuotesSpider(scrapy.Spider): name = 'quotes_2_3' start_urls = [ 'http://quotes.toscrape.com', ] allowed_domains = [ 'toscrape.com', ] def parse(self,response): for quote in response.css('div.quote'): yield{ 'quote': quote.css('span.text::text').extract_first(), 'author': quote.css('small.author::text').extract_first(), 'tags': quote.css('div.tags a.tag::text').extract(), } author_page = response.css('small.author+a::attr(href)').extract_first() authro_full_url = response.urljoin(author_page) yield scrapy.Request(authro_full_url, callback=self.parse_author) # 對詳情頁發送請求,回調詳情頁的解析函數 next_page = response.css('li.next a::attr("href")').extract_first() # 經過css選擇器定位到下一頁 if next_page is not None: next_full_url = response.urljoin(next_page) yield scrapy.Request(next_full_url, callback=self.parse) # 對下一頁發送請求,回調本身的解析函數 def parse_author(self,response): yield{ 'author': response.css('.author-title::text').extract_first(), 'author_born_date': response.css('.author-born-date::text').extract_first(), 'author_born_location': response.css('.author-born-location::text').extract_first(), 'authro_description': response.css('.author-born-location::text').extract_first(),
案例3:爬取www.id97.com電影網,將一級頁面中的電影名稱,類型,評分,二級頁面中的上映時間,導演,片長進行爬取。(多級頁面+傳參)
# -*- coding: utf-8 -*- import scrapy from moviePro.items import MovieproItem class MovieSpider(scrapy.Spider): name = 'movie' allowed_domains = ['www.id97.com'] start_urls = ['http://www.id97.com/'] def parse(self, response): div_list = response.xpath('//div[@class="col-xs-1-5 movie-item"]') for div in div_list: item = MovieproItem() item['name'] = div.xpath('.//h1/a/text()').extract_first() item['score'] = div.xpath('.//h1/em/text()').extract_first() item['kind'] = div.xpath('.//div[@class="otherinfo"]').xpath('string(.)').extract_first() item['detail_url'] = div.xpath('./div/a/@href').extract_first() #meta參數:請求傳參.經過meta參數進行Request的數據傳遞,meta字典就會傳遞給回調函數的response參數 yield scrapy.Request(url=item['detail_url'],callback=self.parse_detail,meta={'item':item}) def parse_detail(self,response): #經過response獲取item. 先經過 response.meta返回接收到的meta字典,再得到item字典 item = response.meta['item'] item['actor'] = response.xpath('//div[@class="row"]//table/tr[1]/a/text()').extract_first() item['time'] = response.xpath('//div[@class="row"]//table/tr[7]/td[2]/text()').extract_first() item['long'] = response.xpath('//div[@class="row"]//table/tr[8]/td[2]/text()').extract_first() yield item #提交item到管道
案例4:稍複雜,可參考連接進行理解:https://github.com/makcyun/web_scraping_with_python/tree/master/,https://www.cnblogs.com/sanduzxcvbnm/p/10277414.html
1 #!/user/bin/env python 2 3 """ 4 爬取豌豆莢網站全部分類下的所有 app 5 數據爬取包括兩個部分: 6 一:數據指標 7 1 爬取首頁 8 2 爬取第2頁開始的 ajax 頁 9 二:圖標 10 使用class方法下載首頁和 ajax 頁 11 分頁循環兩種爬取思路, 12 指定頁數進行for 循環,和不指定頁數一直往下爬直到爬不到內容爲止 13 1 for 循環 14 """ 15 16 import scrapy 17 from wandoujia.items import WandoujiaItem 18 19 import requests 20 from pyquery import PyQuery as pq 21 import re 22 import csv 23 import pandas as pd 24 import numpy as np 25 import time 26 import pymongo 27 import json 28 import os 29 from urllib.parse import urlencode 30 import random 31 import logging 32 33 logging.basicConfig(filename='wandoujia.log',filemode='w',level=logging.DEBUG,format='%(asctime)s %(message)s',datefmt='%Y/%m/%d %I:%M:%S %p') 34 # https://juejin.im/post/5aee70105188256712786b7f 35 logging.warning("warn message") 36 logging.error("error message") 37 38 39 class WandouSpider(scrapy.Spider): 40 name = 'wandou' 41 allowed_domains = ['www.wandoujia.com'] 42 start_urls = ['http://www.wandoujia.com/'] 43 44 def __init__(self): 45 self.cate_url = 'https://www.wandoujia.com/category/app' 46 # 首頁url 47 self.url = 'https://www.wandoujia.com/category/' 48 # ajax 請求url 49 self.ajax_url = 'https://www.wandoujia.com/wdjweb/api/category/more?' 50 # 實例化分類標籤 51 self.wandou_category = Get_category() 52 53 def start_requests(self): 54 yield scrapy.Request(self.cate_url,callback=self.get_category) 55 56 def get_category(self,response): 57 # # num = 0 58 cate_content = self.wandou_category.parse_category(response) 59 for item in cate_content: 60 child_cate = item['child_cate_codes'] 61 for cate in child_cate: 62 cate_code = item['cate_code'] 63 cate_name = item['cate_name'] 64 child_cate_code = cate['child_cate_code'] 65 child_cate_name = cate['child_cate_name'] 66 67 68 # # 單類別下載 69 # cate_code = 5029 70 # child_cate_code = 837 71 # cate_name = '通信社交' 72 # child_cate_name = '收音機' 73 74 # while循環 75 page = 1 # 設置爬取起始頁數 76 print('*' * 50) 77 78 # # for 循環下一頁 79 # pages = [] 80 # for page in range(1,3): 81 # print('正在爬取:%s-%s 第 %s 頁 ' % 82 # (cate_name, child_cate_name, page)) 83 logging.debug('正在爬取:%s-%s 第 %s 頁 ' % 84 (cate_name, child_cate_name, page)) 85 86 if page == 1: 87 # 構造首頁url 88 category_url = '{}{}_{}' .format(self.url, cate_code, child_cate_code) 89 else: 90 params = { 91 'catId': cate_code, # 大類別 92 'subCatId': child_cate_code, # 小類別 93 'page': page, 94 } 95 category_url = self.ajax_url + urlencode(params) 96 97 dict = {'page':page,'cate_name':cate_name,'cate_code':cate_code,'child_cate_name':child_cate_name,'child_cate_code':child_cate_code} 98 99 yield scrapy.Request(category_url,callback=self.parse,meta=dict) 100 101 # # for 循環方法 102 # pa = yield scrapy.Request(category_url,callback=self.parse,meta=dict) 103 # pages.append(pa) 104 # return pages 105 106 def parse(self, response): 107 if len(response.body) >= 100: # 判斷該頁是否爬完,數值定爲100是由於無內容時長度是87 108 page = response.meta['page'] 109 cate_name = response.meta['cate_name'] 110 cate_code = response.meta['cate_code'] 111 child_cate_name = response.meta['child_cate_name'] 112 child_cate_code = response.meta['child_cate_code'] 113 114 if page == 1: 115 contents = response 116 else: 117 jsonresponse = json.loads(response.body_as_unicode()) 118 contents = jsonresponse['data']['content'] 119 # response 是json,json內容是html,html 爲文本不能直接使用.css 提取,要先轉換 120 contents = scrapy.Selector(text=contents, type="html") 121 122 contents = contents.css('.card') 123 for content in contents: 124 # num += 1 125 item = WandoujiaItem() 126 item['cate_name'] = cate_name 127 item['child_cate_name'] = child_cate_name 128 item['app_name'] = self.clean_name(content.css('.name::text').extract_first()) 129 item['install'] = content.css('.install-count::text').extract_first() 130 item['volume'] = content.css('.meta span:last-child::text').extract_first() 131 item['comment'] = content.css('.comment::text').extract_first().strip() 132 item['icon_url'] = self.get_icon_url(content.css('.icon-wrap a img'),page) 133 yield item 134 135 # 遞歸爬下一頁 136 page += 1 137 params = { 138 'catId': cate_code, # 大類別 139 'subCatId': child_cate_code, # 小類別 140 'page': page, 141 } 142 ajax_url = self.ajax_url + urlencode(params) 143 144 dict = {'page':page,'cate_name':cate_name,'cate_code':cate_code,'child_cate_name':child_cate_name,'child_cate_code':child_cate_code} 145 yield scrapy.Request(ajax_url,callback=self.parse,meta=dict) 146 147 148 149 # 名稱清除方法1 去除不能用於文件命名的特殊字符 150 def clean_name(self, name): 151 rule = re.compile(r"[\/\\\:\*\?\"\<\>\|]") # '/ \ : * ? " < > |') 152 name = re.sub(rule, '', name) 153 return name 154 155 def get_icon_url(self,item,page): 156 if page == 1: 157 if item.css('::attr("src")').extract_first().startswith('https'): 158 url = item.css('::attr("src")').extract_first() 159 else: 160 url = item.css('::attr("data-original")').extract_first() 161 # ajax頁url提取 162 else: 163 url = item.css('::attr("data-original")').extract_first() 164 165 # if url: # 不要在這裏添加url存在判斷,不然空url 被過濾掉 致使編號對不上 166 return url 167 168 169 # 首先獲取主分類和子分類的數值代碼 # # # # # # # # # # # # # # # # 170 class Get_category(): 171 def parse_category(self, response): 172 category = response.css('.parent-cate') 173 data = [{ 174 'cate_name': item.css('.cate-link::text').extract_first(), 175 'cate_code': self.get_category_code(item), 176 'child_cate_codes': self.get_child_category(item), 177 } for item in category] 178 return data 179 180 # 獲取全部主分類標籤數值代碼 181 def get_category_code(self, item): 182 cate_url = item.css('.cate-link::attr("href")').extract_first() 183 184 pattern = re.compile(r'.*/(\d+)') # 提取主類標籤代碼 185 cate_code = re.search(pattern, cate_url) 186 return cate_code.group(1) 187 188 # 獲取全部子分類標籤數值代碼 189 def get_child_category(self, item): 190 child_cate = item.css('.child-cate a') 191 child_cate_url = [{ 192 'child_cate_name': child.css('::text').extract_first(), 193 'child_cate_code': self.get_child_category_code(child) 194 } for child in child_cate] 195 196 return child_cate_url 197 198 # 正則提取子分類 199 def get_child_category_code(self, child): 200 child_cate_url = child.css('::attr("href")').extract_first() 201 pattern = re.compile(r'.*_(\d+)') # 提取小類標籤編號 202 child_cate_code = re.search(pattern, child_cate_url) 203 return child_cate_code.group(1) 204 205 # # 能夠選擇保存到txt 文件 206 # def write_category(self,category): 207 # with open('category.txt','a',encoding='utf_8_sig',newline='') as f: 208 # w = csv.writer(f) 209 # w.writerow(category.values())
以上4個案例都只貼出了爬蟲主程序腳本,因篇幅緣由,因此item、pipeline和settings等腳本未貼出,可參考上面案例進行編寫。
問題:在以前代碼中,咱們歷來沒有手動的對start_urls列表中存儲的起始url進行過請求的發送,可是起始url的確是進行了請求的發送,那這是如何實現的呢?
解答:實際上是由於爬蟲文件中的爬蟲類繼承到了Spider父類中的start_requests(self)這個方法,該方法就能夠對start_urls列表中的url發起請求:
def start_requests(self): for u in self.start_urls: yield scrapy.Request(url=u,callback=self.parse)
注意:該方法默認的實現,是對起始的url發起get請求,若是想發起post請求,則須要子類重寫該方法。不過,通常狀況下不用scrapy發post請求,用request模塊。
例:爬取百度翻譯
# -*- coding: utf-8 -*- import scrapy class PostSpider(scrapy.Spider): name = 'post' # allowed_domains = ['www.xxx.com'] start_urls = ['https://fanyi.baidu.com/sug'] def start_requests(self): data = { # post請求參數 'kw':'dog' } for url in self.start_urls: yield scrapy.FormRequest(url=url,formdata=data,callback=self.parse) # 發送post請求 def parse(self, response): print(response.text)
- 在使用scrapy crawl spiderFileName運行程序時,在終端裏打印輸出的就是scrapy的日誌信息。
- 日誌信息的種類:
ERROR : 通常錯誤
WARNING : 警告
INFO : 通常的信息
DEBUG : 調試信息
- 設置日誌信息指定輸出:
在settings.py配置文件中,加入
LOG_LEVEL = ‘指定日誌信息種類’便可。
LOG_FILE = 'log.txt'則表示將日誌信息寫入到指定文件中進行存儲。
其餘經常使用設置:
BOT_NAME 默認:「scrapybot」,使用startproject命令建立項目時,其被自動賦值 CONCURRENT_ITEMS 默認爲100,Item Process(即Item Pipeline)同時處理(每一個response的)item時最大值 CONCURRENT_REQUEST 默認爲16,scrapy downloader併發請求(concurrent requests)的最大值 LOG_ENABLED 默認爲True,是否啓用logging DEFAULT_REQUEST_HEADERS 默認以下:{'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en',} scrapy http request使用的默認header LOG_ENCODING 默認utt-8,logging中使用的編碼 LOG_LEVEL 默認「DEBUG」,log中最低級別,可選級別有:CRITICAL,ERROR,WARNING,DEBUG USER_AGENT 默認:「Scrapy/VERSION(....)」,爬取的默認User-Agent,除非被覆蓋 COOKIES_ENABLED=False,禁用cookies
實際開發中,一般在同一個項目裏會有多個爬蟲,多個爬蟲的時候是怎麼將他們運行起來呢?
運行單個爬蟲
import sys from scrapy.cmdline import execute if __name__ == '__main__': execute(["scrapy","crawl","maitian","--nolog"])
而後運行py文件便可運行名爲‘maitian‘的爬蟲
同時運行多個爬蟲
步驟以下:
- 在spiders同級建立任意目錄,如:commands
- 在其中建立 crawlall.py 文件 (此處文件名就是自定義的命令)
- 在settings.py 中添加配置 COMMANDS_MODULE = '項目名稱.目錄名稱'
- 在項目目錄執行命令:scrapy crawlall
crawlall.py代碼
1 from scrapy.commands import ScrapyCommand 2 from scrapy.utils.project import get_project_settings 3 4 class Command(ScrapyCommand): 5 6 requires_project = True 7 8 def syntax(self): 9 return '[options]' 10 11 def short_desc(self): 12 return 'Runs all of the spiders' 13 14 def run(self, args, opts): 15 spider_list = self.crawler_process.spiders.list() 16 for name in spider_list: 17 self.crawler_process.crawl(name, **opts.__dict__) 18 self.crawler_process.start()