1. 程式人生 > 實用技巧 >分散式爬取鏈家網二手房資訊

分散式爬取鏈家網二手房資訊

任務目標:以分散式的方式爬取鏈家網上二手房資訊,包括標題、城市、行政區、總價、戶型、面積、朝向等資訊

分散式爬蟲,即在多臺電腦上同時執行同一個爬蟲任務,在分散式爬取之前,需要先完成單機爬蟲,然後部署到多臺機器上,完成分散式。

鏈家網單機爬蟲:從城市頁面開始爬取,到每個城市的不同行政區,以及每個行政區的多個頁面,每個頁面的多個二手房資訊,到最後的二手房詳情頁面。
經過相應的網頁機構分析,得到專案(專案名lianjia、爬蟲名lj)結構及最終程式碼如下:

專案結構:

items.py

import scrapy


class LianjiaItem(scrapy.Item):
    title = scrapy.Field()
    city = scrapy.Field()
    # 行政區
    region = scrapy.Field()
    total_price = scrapy.Field()
    unit_price = scrapy.Field()
    # 戶型
    house_type = scrapy.Field()
    # 朝向
    orientation = scrapy.Field()
    full_area = scrapy.Field()
    inside_area = scrapy.Field()
    # 建築型別
    building_type = scrapy.Field()

pipelines.py

import json


class LianjiaPipeline:

    def __init__(self):
        self.fp = open('houses.txt', 'w', encoding='utf-8')

    def process_item(self, item, spider):
        self.fp.write(json.dumps(dict(item), ensure_ascii=False) + '\n')
        return item

    def close_spider(self, spider):
        self.fp.close()

由於最終要實現分散式爬取,資料是儲存在redis資料庫中,這裡先用txt檔案暫存資料,測試程式碼

settings.py

BOT_NAME = 'lianjia'

SPIDER_MODULES = ['lianjia.spiders']
NEWSPIDER_MODULE = 'lianjia.spiders'

ROBOTSTXT_OBEY = False
CONCURRENT_REQUESTS = 5
DOWNLOAD_DELAY = 0.2
DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
  'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36'
}
ITEM_PIPELINES = {
   'lianjia.pipelines.LianjiaPipeline': 300,
}

lj.py

import scrapy
from ..items import LianjiaItem


class LjSpider(scrapy.Spider):
    name = 'lj'
    allowed_domains = ['lianjia.com']
    start_urls = ['https://www.lianjia.com/city/']

    def parse(self, response):
        city_li_list = response.xpath('//div[@class="city_list_section"]//li[@class="city_list_li'
                                      ' city_list_li_selected"]//div[@class="city_list"]//ul//li')
        for city_li in city_li_list:
            city = city_li.xpath('.//text()').get()
            city_url = city_li.xpath('.//a/@href').get()
            yield scrapy.Request(url=city_url + 'ershoufang', callback=self.parse_house_region, meta={'city': city})

    def parse_house_region(self, response):
        position_list = response.xpath('//div[@class="position"]//div[@data-role="ershoufang"]/div//a')
        for position in position_list:
            position_url = response.urljoin(position.xpath('.//@href').get())
            yield scrapy.Request(url=position_url, callback=self.parse_house_list, meta=response.meta)

    def parse_house_list(self, response):
        house_url_list = response.xpath('//ul[@class="sellListContent"]//li/a/@href').getall()
        for house_url in house_url_list:
            yield scrapy.Request(url=house_url, callback=self.parse_house_detail, meta=response.meta)
        # 翻頁 todo

    def parse_house_detail(self, response):
        item = LianjiaItem()
        item['title'] = response.xpath('//div[@class="sellDetailHeader"]//div[@class="title"]/h1/text()').get()
        item['city'] = response.meta['city']
        item['region'] = ''.join(response.xpath('//div[@class="areaName"]//span[@class="info"]//text()').getall()).strip()
        item['total_price'] = response.xpath('//span[@class="total"]/text()').get() + '萬'
        item['unit_price'] = response.xpath('//span[@class="unitPriceValue"]/text()').get() + '元/平米'
        house_base_info = response.xpath('//div[@class="base"]//div[@class="content"]/ul//li')
        item['house_type'] = house_base_info[0].xpath('./text()').get()
        item['orientation'] = house_base_info[6].xpath('./text()').get()
        item['full_area'] = house_base_info[2].xpath('./text()').get()
        item['inside_area'] = house_base_info[4].xpath('./text()').get()
        item['building_type'] = response.xpath('//div[@class="area"]//div[@class="subInfo"]/text()').get()
        yield item

lianjia_main.py

from scrapy import cmdline

cmdline.execute('scrapy crawl lj'.split(' '))

執行lianjia_main.py後發現,可以正常抓取到資料儲存到houses.txt中:

接下來需實現分散式:
scrapy本身是不支援分散式的,此時需要用到scrapy_redis元件, pip install scrapy_redis 即可安裝使用
分散式爬蟲中需指定一臺機器為Redis伺服器,可以用來管理爬蟲伺服器請求的URL並去重,還可以用來儲存爬蟲伺服器爬取的資料;
需指定若干臺機器為爬蟲伺服器,用來執行爬蟲任務,從Redis伺服器獲取請求,並把爬取下來的資料傳送給Redis伺服器。

分散式爬蟲的部署準備:

  1. 伺服器端安裝scrapyd: pip3 install scrapyd 由於我只有一臺機器,這裡我使用的是虛擬機器下安裝的Ubuntu18.04系統

    由於我已安裝過,這裡沒有再次安裝。

  2. 從 '/usr/local/lib/python3.6/dist-packages/scrapyd' 下拷貝出 'default_scrapyd.conf' 放到 '/etc/scrapyd/scrapyd.conf'

    若提示沒有資料夾,則新建對應的路徑即可

  3. 修改 '/etc/scrapyd/scrapyd.conf' 中的 'bind_address' 為自己的IP(或改為 0.0.0.0 表示允許任意機器訪問)

  4. 在開發機(自己的windows電腦)上安裝scrapyd-client: pip install scrapyd-client

  5. 安裝scrapyd-client後有一個bug,無法直接使用命令scrapyd-deploy,解決辦法為:
    進入安裝python的資料夾下(配置python環境變數的那個),進入Scripts資料夾下,可以看到有個scrapyd-deploy,在此資料夾下新建檔案scrapyd-deploy.bat,編輯內容為

@echo off
C:\Python37-32\python.exe C:\Python37-32\Scripts\scrapyd-deploy %*

路徑根據實際情況更改,隨後進入命令列敲入scrapyd-deploy -h,若出現相關提示資訊即表示成功

  1. 修改專案中的 scrapy.cfg
[settings]
default = lianjia.settings

[deploy]
url = http://192.168.1.105:6800/
project = lianjia

192.168.1.105 為我要部署該專案的爬蟲伺服器的IP

  1. 在專案所在路徑下執行部署命令,將專案部署到cfg檔案所配置的爬蟲伺服器: scrapyd-deploy default -p lianjia
  2. 通過curl命令(需下載並配置環境變數)啟動爬蟲,格式如下: curl http://localhost:6800/schedule.json -d project=myproject -d spider=somespider

分散式爬蟲的部署:
由於我只有一臺機器,系統為windows7,為了模擬分散式的部署,我使用了VMware Workstation虛擬機器,裡面有兩個Ubuntu18,一臺作為Redis伺服器,ip為192.168.1.103;一臺作為爬蟲伺服器,ip為192.168.1.105,配合本機windows7一同爬取。

在上面的部署準備完成後,就可以開始部署了:

  1. 首先需要待部署的伺服器開啟scrapyd服務,這裡我在 /srv 資料夾下新建資料夾lj,用來儲存部署相關資訊,在lj資料夾下輸入scrapyd,回車,即可開啟該服務

    可以看到scrapyd服務的相關資訊,在瀏覽器中輸入 http://192.168.1.105:6800 出現如下頁面,表示服務開啟成功

  2. 在專案所在路徑下執行部署命令: scrapyd-deploy

    可以看到,部署成功

  3. 部署成功後就可以執行爬蟲了: curl http://192.168.1.105:6800/schedule.json -d project=lianjia -d spider=lj

    開啟 http://192.168.1.105:6800 ,點選jobs,可以看到爬蟲正在執行,點選log,可以檢視日誌輸出

  4. 此時分散式爬蟲並未部署,因為Redis伺服器還沒有配置,先關閉之前執行的爬蟲:curl http://192.168.1.105:6800/cancel.json -d project=lianjia -d job=716c852824cc11ebbc7a000c297c8a13

  5. 選擇ip為192.168.1.103的Ubuntu18為Redis伺服器,安裝redis:sudo apt install redis-server,然後修改配置檔案 '/etc/redis/redis.conf',取消bind註釋,修改為本機IP 102.168.1.103 或修改為 0.0.0.0,重啟服務 sudo service redis-server restart

  6. 修改爬蟲程式碼lj.py,使用scrapy_redis.spiders.RedisSpider替代原來的scrapy.Spider,或使用scrapy_redis.spiders.RedisCrawlSpider替代CrawlSpider,刪除start_urls並設定redis_key,這個redis_key是後面控制redis啟動爬蟲用的,爬蟲的第一個url,就是redis通過這個key傳送出去的

import scrapy
from ..items import LianjiaItem
from scrapy_redis.spiders import RedisSpider


class LjSpider(RedisSpider):
    name = 'lj'
    allowed_domains = ['lianjia.com']
    # start_urls = ['https://www.lianjia.com/city/']
    redis_key = 'lj'

    def parse(self, response):
        ......
  1. 在settings.py中對scrapy_redis進行相關配置
    刪除原來的pipeline配置並進行如下配置
# 確保request儲存到redis中
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 確保所有爬蟲共享相同的去重指紋
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 設定redis為item pipeline
ITEM_PIPELINES = {
   'scrapy_redis.pipelines.RedisPipeline': 300,
}
# 是否在關閉時候保留原來的排程器和去重記錄,True=保留,False=清空
SCHEDULER_PERSIST = True
EDIS_HOST = '192.168.1.103'
REDIS_PORT = 6379
  1. 重新部署專案並執行: 首先將專案重新部署到192.168.1.105的爬蟲伺服器上並執行,然後在本地windows上也執行該專案,檢視日誌,可以看到,兩個爬蟲專案都沒有開始爬取資料,而是等待Redis伺服器的排程開始

  1. 進入Redis伺服器,推入一個開始的url連結: redis-cli>lpush [redis_key] start_url 開始爬取

可以看到,兩個爬蟲伺服器同時開始爬取資料了,實現了分散式的爬取

  1. 檢視redis伺服器內的資料:

可以看到,有三種key:lj:requests、lj:dupefilter、lj:items,分別表示請求資訊、去重過濾資訊、爬取的資料資訊
檢視 lj:items 可以看到,裡面已經儲存了爬取到的資料,至此,分散式爬取鏈家網二手房資料的專案完成。

本文完