用 Scrapy 抓取某家的樓盤資訊
最近想爬點東西,又不想造輪子,就用上了scrapy,順便記錄下自己踩過的坑和都做了些什麼。
使用的軟體版本:
ipython 5.1.x
scrapy 1.4
準備階段(在動手寫之前,一定要先觀察好標籤位置!):
這裡使用Firefox的外掛firebug對進行頁面標籤確定:
該頁面有好幾個樓盤資訊,所以在看到上面的<li>標籤後,應該再找一下它的父節點<ul>:
這些就是想要抓的新樓盤列表,id也說明了該ul列表的作用。在子節點<li>中繼續尋找到自己想要的資訊,找完差不多就可以開始爬蟲的編寫了。
編寫階段:
其資料夾內容:scrapy startproject house(專案名字,我這裡用了house)
house
├── house
│ ├── __init__.py
│ ├── items.py
│ ├── middlewares.py
│ ├── pipelines.py
│ ├── __pycache__
│ ├── settings.py
│ └── spiders
│ ├── __init__.py
│ └── __pycache__
└── scrapy.cfg
在items.py 檔案裡新增 item(儲存爬取到的資料的容器;其使用方法和python字典類似)
上面我加了城市,樓盤名字,位置,房間,面積,均價和其他,具體可以看自己需求定義。import scrapy class HouseItem(scrapy.Item): city = scrapy.Field() title = scrapy.Field() region = scrapy.Field() room = scrapy.Field() area = scrapy.Field() average = scrapy.Field() other = scrapy.Field()
在 house
/spiders/
目錄下新增house
_spider.py ,並新增以下內容:
import scrapy # 從上一層目錄匯入items.py的HouseItem類 from house.items import HouseItem class HouseSpider(scrapy.Spider): # 爬蟲名,不能衝突 name = 'house' # 請求開始 def start_requests(self): urls = [ 'http://bj.fang.lianjia.com/loupan/' ] # 對urls列表進行迭代請求 for url in urls: yield scrapy.Request(url=url, callback=self.parse) # 對響應的資料進行有選擇性的抓取 def parse(self, response): for house in response.xpath('//ul[contains(@id, "house-lst")]/li'): item = HouseItem() item['title'] = house.xpath('.//h2').xpath('.//a[contains(@data-el, "xinfang")]/text()').extract() item['region'] = house.xpath('.//span[contains(@class, "region")]/text()').extract() item['room'] = house.xpath('.//div[contains(@class, "area")]/text()').re('\S*\w') item['area'] = house.xpath('.//div[contains(@class, "area")]/span/text()').extract() item['other'] = list(set(house.xpath('.//div[contains(@class, "other")]').xpath('.//span/text()').extract() + house.xpath('.//div[contains(@class, "type")]').xpath('.//span/text()').extract())) item['average'] = house.xpath('.//div[contains(@class, "average")]').re('.*\s*(\w*)\s.*>(.\d*).*\s*(\w.*)') yield item
PS:正則寫得有點醜,以後再修改,先用著。
PPS:scrapy shell 網址,用來除錯還不錯,不過建議先裝ipython,能補全關鍵字。
留給自己:這裡的選擇器一開始用得不對,卡了很久,總是多了些空元素[ ],後來重新觀察web頁面元素,才發現自己寫得不對,改成上面這樣才好了,所以準備階段很重要!
做到這裡,就可以執行爬蟲了:
scrapy crawl house(上面定義的爬蟲名)
不過 這樣沒有儲存下資料,可以使用-o輸出json格式資料
scrapy crawl house -o data.json
當然還有其他格式的輸出,可以看官網:https://docs.scrapy.org/en/latest/topics/feed-exports.html只有第一頁明顯不夠用,如何抓下一頁呢?
同樣,先進行下一頁標籤的獲取:
本來很簡單的,直接用 response.xpath("//a[contains(., '下一頁')]//@href").extract_first() 應該提取到這個標籤的href,不過不行,一番折騰也沒發現有其他直接獲取到該下一頁標籤的方法,沒辦法只能用它的父節點:
<div class="page-box house-lst-page-box" comp-module="page" data-xftrack="10139" page-url="/loupan/pg{page}/" page-data="{"totalPage":26,"curPage":1}">
這個父節點的page-data屬性中包括了總頁數和當前頁,所以在當頁基礎上加1就可以到達下一頁:
next_page = '/loupan/pg' + str(int(page[1]) + 1)
而且還要用總頁數和當前頁比較,來確定是最後一頁,所以加上這些的house_spider.py 程式碼如下:
import scrapy
# 從上一層目錄匯入items.py的HouseItem類
from house.items import HouseItem
class HouseSpider(scrapy.Spider):
# 爬蟲名,不能衝突
name = 'house'
# 請求開始
def start_requests(self):
urls = [
'http://bj.fang.lianjia.com/loupan/'
]
# 對urls列表進行迭代請求
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
# 對響應的資料進行有選擇性的抓取
def parse(self, response):
for house in response.xpath('//ul[contains(@id, "house-lst")]/li'):
item = HouseItem()
item['title'] = house.xpath('.//h2').xpath('.//a[contains(@data-el, "xinfang")]/text()').extract()
item['region'] = house.xpath('.//span[contains(@class, "region")]/text()').extract()
item['room'] = house.xpath('.//div[contains(@class, "area")]/text()').re('\S*\w')
item['area'] = house.xpath('.//div[contains(@class, "area")]/span/text()').extract()
item['other'] = list(set(house.xpath('.//div[contains(@class, "other")]').xpath('.//span/text()').extract() + house.xpath('.//div[contains(@class, "type")]').xpath('.//span/text()').extract()))
item['average'] = house.xpath('.//div[contains(@class, "average")]').re('.*\s*(\w*)\s.*>(.\d*).*\s*(\w.*)')
yield item
# 對屬性中的總頁數和當前頁進行提取
page = response.xpath('//div[contains(@class, "page-box")]/@page-data').re('totalPage":(\w*).*:(\w*)')
# 最後一頁的頁碼和總頁數一致
if page[0] != page[1]:
next_page = '/loupan/pg' + str(int(page[1]) + 1)
yield scrapy.Request(response.urljoin(next_page))
再執行一下爬蟲,已經能抓到很多頁的資料了。不過還是有些問題,有些樓盤資訊出現缺失,這是怎麼回事呢?
{"title": ["中駿西山天璟"], "room": ["3居/2居"], "area": ["建面 102~155m²"], "other": ["五證齊全", "在售", "低密度", "住宅"], "region": ["門頭溝-龍泉鎮城子大街東側"], "average": ["均價", "67000", "元/平"]}
{"title": ["炫立方"], "room": [], "area": [], "other": ["五證齊全", "在售", "商鋪"], "region": ["順義-南法信順平路與南焦路交匯處向南50米路東"], "average": ["均價", "43000", "元/平"]}
找到對應的樓盤看了下缺失資訊對應的標籤,發現這些標籤原本就沒資料:
<div class="area"><span></span></div>
就是和爬蟲本身並沒有關係,所以後續要在pipelines.py 中新增過濾函式,對這些缺失資訊的樓盤進行刪除。
在資料比較少的情況,可以用json檔案儲存。但是資料多了json已經不夠用了,這時候需要將它們儲存到資料庫中。
這裡就用NoSQL非關係型的MongoDB來儲存。
首先在settings.py 中新增:
MONGODB_HOST = 'localhost'
MONGODB_PORT = 27017
MONGODB_DB = 'house'
MONGODB_COLLECTION = 'lianjia'
然後去掉ITEM_PIPELINES 那行的註釋,並新增在pipelines.py 中自定義的類MongoDBPipeline:
ITEM_PIPELINES = {
'house.pipelines.HousePipeline': 200,
'house.pipelines.MongoDBPipeline': 300,
}
在pipelines.py 中新增類MongoDBPipeline:
class MongoDBPipeline(object):
def open_spider(self, spider):
# 連線mongodb資料庫
self.client = pymongo.MongoClient(host=settings['MONGODB_HOST'], port=settings['MONGODB_PORT'])
# 設定資料庫
self.db = self.client[settings['MONGODB_DB']]
# 設定文件
self.collection = self.db[settings['MONGODB_COLLECTION']]
def process_item(self, item, spider):
# 向文件中插入一條資料
self.collection.insert_one(dict(item))
return item
def close_spider(self, spider):
# 資料庫連線關閉
self.client.close()
這章就完成了對一個地區的新樓盤資訊的抓取,以及儲存。如果想抓其他地區的,只要在urls列表中新增其他地區的程式碼即可。
程式碼(與本文有些差別):Github