1. 程式人生 > >用 Scrapy 抓取某家的樓盤資訊

用 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