Item Pipeline介紹
Item對象是一個簡單的容器,用於收集抓取到的數據,其提供了類似於字典(dictionary-like)的API,並具有用於聲明可用字段的簡單語法。
Scrapy的Item Pipeline(項目管道)是用於處理數據的組件。
當Spider將收集到的數據封裝爲Item後,將會被傳遞到Item Pipeline(項目管道)組件中等待進一步處理。Scrapy猶如一個爬蟲流水線,Item Pipeline是流水線的最後一道工序,但它是可選的,默認關閉,使用時需要將它激活。如果需要,可以定義多個Item Pipeline組件,數據會依次訪問每個組件,執行相應的數據處理功能。
Item Pipeline的典型應用:
·清理數據。
·驗證數據的有效性。
·查重並丟棄。
·將數據按照自定義的格式存儲到文件中。
·將數據保存到數據庫中。
編寫自己的Item Pipeline組件其實很簡單,它只是一個實現了幾個簡單方法的Python類。當建立一個項目後,這個類就已經自動創建了。打開項目qidian_hot下的pipelines.py,發現自動生成了如下代碼:
class LianjiaPipeline(object):
def process_item(self, item, spider):
return item
LianjiaPipeline是自動生成的Item Pipeline類,它無須繼承特定的基類,只需要實現某些特定的方法,如process_item()、open_spider()和close_spider()。注意,方法名不可改變。process_item()方法是Item Pipeline類的核心方法,必須要實現,用於處理Spider爬取到的每一條數據(Item)。它有兩個參數:item:待處理的Item對象。spider:爬取此數據的Spider對象。方法的返回值是處理後的Item對象,返回的數據會傳遞給下一級的Item Pipeline(如果有)繼續處理。
爬取長沙鏈家二手房
https://cs.lianjia.com/ershoufang/
我們爬取二手房的標題、地點、價格以及其他一些房屋基本信息。點擊房屋標題進入詳情頁面,可以查看詳細信息。
我們還要爬取詳情頁面房屋配備電梯情況以及產權年限。
編寫爬蟲文件
爬蟲文件是我們用Scrapy爬蟲主要要編寫的文件,其作用主要有兩個:
- 向網頁發起請求
- 向其他組件發送響應/數據
我們可以利用命令行scrapy genspider 爬蟲名稱 網頁域名
自動生成爬蟲文件,也可以手動創建爬蟲文件。
這裏在spiders子目錄文件夾下手動創建一個名爲lianjia_spider.py的爬蟲文件
導入需要用到的庫文件,命名爬蟲名稱
import scrapy
from scrapy import Request
from lianjia.items import LianjiaItem
class HomespiderSpider(scrapy.Spider):
name = 'home'
重寫start_requests()方法
def start_requests(self):
url = 'https://cs.lianjia.com/ershoufang/'
yield Request(url,callback=self.parse)
Request方法的callback參數用於指定爬蟲響應的解析方法,默認爲parse方法。
解析頁面,使用XPath提取數據
打開開發者工具,可以看到與房屋有關的信息都在< div class="info clear" >
下面,包括標題、地址、基本信息、價格等。我們首先定位到<div class="info clear">
這個節點,然後再在這個節點下提取各種信息。
def parse(self, response):
roomList = response.xpath('//ul/li/div[@class="info clear"]')
首先提取標題和地點,使用xpath helper這個插件可以很方便編寫xpath表達式。
輸入xpath表達式,右邊會顯示提取到的信息,同時網頁中對應的信息會高亮,可以驗證xpath是否正確。所以我們提取到標題的完整xpath就是://ul/li/div[@class="info clear"]/div[@class="title"]/a
,但是我們之前已經定位到<div class="info clear">
這個節點,只需要在這個節點之下的xpath表達式就可以提取標題了。同理,提取位置和其他基本信息。
for oneList in roomList:
title = oneList.xpath('./div[@class="title"]/a/text()').extract_first()
place = oneList.xpath('./div[@class="flood"]//a[1]/text()').extract_first()+'- '+oneList.xpath('./div[@class="flood"]//a[2]/text()').extract_first()
houseInfo = oneList.xpath('./div[@class="address"]/div/text()').extract_first()
由於房屋的基本信息包含多項以“ | ”分隔的信息:1室1廳 | 39.98平米 | 北 | 精裝 | 中樓層(共28層) | 2008年建 | 板樓
。我們需要提取其中的戶型、面積、樓層、朝向並分別保存:
houseInfoList = houseInfo.split("|")
type = houseInfoList[0]
area = houseInfoList[1]
direction = houseInfoList[2]
floor = houseInfoList[4]
提取單價和總價:
unitPrice = oneList.xpath('./div[@class="priceInfo"]/div[@class="unitPrice"]/span/text()').extract_first()
totalPrice = oneList.xpath('./div[@class="priceInfo"]/div[@class="totalPrice"]/span/text()').extract_first()+oneList.xpath('./div[@class="priceInfo"]/div[@class="totalPrice"]/text()').extract_first()
至此,基本信息都提取完了,接下來需要保存數據,提取詳情頁面的數據。
使用Item保存數據
打開items.py 建立item成員變量,也就是我們需要提取的信息
import scrapy
class LianjiaItem(scrapy.Item):
# -*- coding: utf-8 -*-
title = scrapy.Field()
place = scrapy.Field()
type = scrapy.Field()
area = scrapy.Field()
direction = scrapy.Field()
floor = scrapy.Field()
unitPrice = scrapy.Field()
totalPrice = scrapy.Field()
elevator = scrapy.Field()
propertyYears = scrapy.Field()
回到爬蟲文件,初始化item類,並且將提取的信息保存到item中:
item = LianjiaItem()
item["title"] = title
item["place"] = place
item["type"] = type
item["area"] = area
item["direction"] = direction
item["floor"] = floor
item["unitPrice"] = unitPrice
item["totalPrice"] = totalPrice
獲取詳情頁面鏈接並解析
可以看到詳情面的鏈接是我們之前提取的標題所在的a標籤的href屬性
# 獲取詳情鏈接
clearUrl = oneList.xpath('./div[@class="title"]/a/@href').extract_first()
# print(clearUrl)
yield Request(clearUrl,meta={"item":item},callback=self.clearUrlParse)
獲取詳情頁面連接後,像爬蟲發起爬取請求,注意這裏的meta參數和callback參數。
meta用於傳遞用戶參數,我們之前提取的信息保存在item中,這裏將item一併傳遞出去
callback用於指定解析函數,這裏不能採用默認的parse,需要編寫新的解析函數。
# 詳情鏈接解析函數
def clearUrlParse(self,response):
elevator = response.xpath('//div[@class="base"]//ul/li[last()-1]/text()').extract_first()
propertyYears = response.xpath('//div[@class="base"]//ul/li[last()]/text()').extract_first()
item = response.meta["item"]
item["elevator"] = elevator
item["propertyYears"] = propertyYears
yield item
獲取下一頁鏈接
獲取下一頁有兩種方法,
- 直接提取下一頁的相對鏈接,直至爲None,表明到了最後一頁。
- 提取總頁數,修改下一頁的鏈接 例如第二頁的鏈接
https://cs.lianjia.com/ershoufang/pg2/
pg後面的數字代表當前頁,修改該數字即可。
方法一:
next = response.xpath('//div[@class="page-box house-lst-page-box"]/a[last()]/@href').extract_first()
nextUrl = 'https://cs.lianjia.com'+next
if nextUrl:
yield Request(nextUrl, callback=self.parse)
方法二:
def __init__(self):
self.currentPage = 1
self.totalPage = 1
self.currentPage += 1
nextUrl = 'https://cs.lianjia.com/ershoufang/pg%d'%self.currentPage
self.totalPage = response.xpath('//div[@class="leftContent"]//div[@class="page-box house-lst-page-box"]/a[last()-1]/text()').extract_first()
print(self.totalPage)
if self.currentPage < self.totalPage:
yield Request(nextUrl,callback=self.parse)
至此,爬蟲文件已經編寫完成了,設置一下代理和robots協議就可以直接運行爬蟲導出數據了
但是我們希望爬取的信息按照指定的順序排列保存,當前的順序是隨機的。此外單價一欄下的信息是“單價12106元/平米”,正常來講應該去掉單價二字。所以接下來使用Item Pipeline進行數據二次處理。
使用Item Pipeline處理並保存數據
打開pipelines.py,默認生成了一個LianjiaPipeline類,其中有一個process方法,我們就是在這個方法中處理數據的。
處理數據
import re
class LianjiaPipeline(object):
def process_item(self, item, spider):
item["unitPrice"]=re.sub("單價",'',item["unitPrice"])
return item
用到正則表達式導入re庫,去掉單價信息中的“單價”二字。
按一定格式保存數據
class csvPipeline(object):
def __init__(self):
self.index = 0
self.file = None
def open_spider(self,spider):
self.file = open("home.csv", "a", encoding="utf-8")
def process_item(self,item,spider):
if self.index == 0:
columnName = "title,place,type,area,direction,floor,unitPrice,totalPrice,elevator,propertyYears\n"
self.file.write(columnName)
self.index = 1
homeStr = item["title"]+","+item["place"]+","+item["type"]+","+item["area"]+","+item["direction"]+","+ \
item["floor"]+","+item["unitPrice"]+","+item["totalPrice"]+","+item["elevator"]+","+item["propertyYears"]+"\n"
self.file.write(homeStr)
return item
def close_spider(self,spider):
self.file.close()
再定義一個csvPipeline類,其中除了process方法,還有open_spider方法和close_spider方法用於打開和關閉爬蟲,再次highlight函數名不能隨意更改
配置Item Pipeline
默認是'lianjia.pipelines.LianjiaPipeline': 300,
這裏再將csvPipeline加入,後面的數字400代表優先級,數字越小優先級越低,代表Pipeline處理item中數據的順序。
運行爬蟲
使用命令行運行爬蟲比較麻煩,可以編寫一個start.py文件運行爬蟲
from scrapy import cmdline
cmdline.execute("scrapy crawl home".split())
可以看到提取的信息按照我們設定的順序排列,並且將單價信息進行了處理。
完整的爬蟲文件
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#@Time : 2020/2/21 18:07
#@Author: bz
#@File : lianjia_spider.py
import scrapy
from scrapy import Request
from lianjia.items import LianjiaItem
class HomespiderSpider(scrapy.Spider):
name = 'home'
def __init__(self):
self.currentPage = 1
self.totalPage = 1
def start_requests(self):
url = 'https://cs.lianjia.com/ershoufang/'
yield Request(url,callback=self.parse)
def parse(self, response):
roomList = response.xpath('//ul/li/div[@class="info clear"]')
for oneList in roomList:
title = oneList.xpath('./div[@class="title"]/a/text()').extract_first()
place = oneList.xpath('./div[@class="flood"]//a[1]/text()').extract_first()+'- '+oneList.xpath('./div[@class="flood"]//a[2]/text()').extract_first()
houseInfo = oneList.xpath('./div[@class="address"]/div/text()').extract_first()
houseInfoList = houseInfo.split("|")
type = houseInfoList[0]
area = houseInfoList[1]
direction = houseInfoList[2]
floor = houseInfoList[4]
unitPrice = oneList.xpath('./div[@class="priceInfo"]/div[@class="unitPrice"]/span/text()').extract_first()
totalPrice = oneList.xpath('./div[@class="priceInfo"]/div[@class="totalPrice"]/span/text()').extract_first()+oneList.xpath('./div[@class="priceInfo"]/div[@class="totalPrice"]/text()').extract_first()
item = LianjiaItem()
item["title"] = title
item["place"] = place
item["type"] = type
item["area"] = area
item["direction"] = direction
item["floor"] = floor
item["unitPrice"] = unitPrice
item["totalPrice"] = totalPrice
# 獲取詳情鏈接
clearUrl = oneList.xpath('./div[@class="title"]/a/@href').extract_first()
# print(clearUrl)
yield Request(clearUrl,meta={"item":item},callback=self.clearUrlParse)
# 獲取下一頁鏈接
self.currentPage += 1
nextUrl = 'https://cs.lianjia.com/ershoufang/pg%d'%self.currentPage
self.totalPage = response.xpath('//div[@class="leftContent"]//div[@class="page-box house-lst-page-box"]/a[last()-1]/text()').extract_first()
if self.currentPage < self.totalPage:
yield Request(nextUrl,callback=self.parse)
# 詳情鏈接解析函數
def clearUrlParse(self,response):
elevator = response.xpath('//div[@class="base"]//ul/li[last()-1]/text()').extract_first()
propertyYears = response.xpath('//div[@class="base"]//ul/li[last()]/text()').extract_first()
item = response.meta["item"]
item["elevator"] = elevator
item["propertyYears"] = propertyYears
yield item