從爬蟲到機器學習預測,我是如何一步一步做到的?

Python資料科學發表於2019-02-27

作者:xiaoyu

微信公眾號:Python資料科學

知乎:python資料分析師


從爬蟲到機器學習預測,我是如何一步一步做到的?

前情回顧

前一段時間與大家分享了北京二手房房價分析的實戰專案,分為分析和建模兩篇。文章發出後,得到了大家的肯定和支援,在此表示感謝。

除了資料分析,好多朋友也對爬蟲特別感興趣,想知道爬蟲部分是如何實現的。本篇將分享這個專案的爬蟲部分,算是資料分析的一個 前傳篇。

爬蟲前的思考

爬蟲部分主要是通過爬取鏈x安x客來獲取二手房住房資訊,因為考慮到不同網站的房源資訊可以互補,所以選擇了兩個網站。

爬取目標是北京二手房,僅針對一個城市而言,資料量並不大。所以直接採用Scrapy來完成爬取工作,然後將資料儲存在csv格式的檔案中。最終爬取結果是這樣的,鏈x的爬蟲爬取了 30000+條資料,安x客的爬蟲爬取了 3000+條資料。不得不說鏈x的房源相對來講還是比較全的。

scrapy爬取鏈x

寫一個爬蟲最開始當然要想清楚需要獲取什麼樣的資料了。本次專案對與二手房相關的資料都比較感興趣,可以自然的想到,每個房源連結的具體詳細資訊是最全的。但考慮到爬蟲深度影響整體爬蟲效率問題,並且房源列表中資料已經能夠滿足基本的要求,並沒有必要對每個詳細連結進行深入的爬取,因此最終選擇爬取房源列表。以下是房源列表(部分截圖)中的房源資訊:

從爬蟲到機器學習預測,我是如何一步一步做到的?

確定以上爬取內容後,就開始爬蟲部分的工作。首先在item.py檔案中定義一個子類,該子類繼承了父類scrapy.Item,然後在子類中用scrapy.Field()定義以上資訊的欄位。如下程式碼,將所有需要的欄位資訊都設定好。

import scrapy

class LianjiaSpiderItem(scrapy.Item):
    # define the fields for your item here like:
    Id = scrapy.Field()
    Region = scrapy.Field()
    Garden = scrapy.Field()
    Layout = scrapy.Field()
    Size = scrapy.Field()
    Direction = scrapy.Field()
    Renovation = scrapy.Field()
    Elevator = scrapy.Field()
    Floor = scrapy.Field()
    Year = scrapy.Field()
    Price = scrapy.Field()
    District = scrapy.Field()
    pass
複製程式碼

在spider資料夾下的爬取檔案(自定義)中匯入所需庫,如下程式碼:

  • json:json格式的轉換;

  • scrapy:scrapy庫;

  • logging:日誌;

  • BeautifulSoup:使用bs4提取網頁資訊;

  • table:settings中自設的一個字典;

  • LianjiaSpiderItem:欄位Field;

# -*- coding:utf-8 -*-
import json
import scrapy
import logging
from bs4 import BeautifulSoup
from lianjia_spider.settings import table
from lianjia_spider.items import LianjiaSpiderItem
複製程式碼

下面進入關鍵部分,即爬蟲部分。這部分主要需要自己做的就是如何解析,而對於爬蟲是如何爬取的我們不用關心,因為它是框架已經在底層完成排程和爬取的實現,我們只要簡單呼叫即可。

具體詳細框架結構可參見:Python爬蟲之Scrapy學習(基礎篇)

爬蟲解析部分,是在繼承scrapy.Spider父類的子類LianjiaSpider中完成的。子類中設有三個函式,並通過callback回撥逐層實現解析功能,這三個函式是:

  • start_requests:覆蓋父類中原有函式,爬取初始url並存入訊息佇列中;

  • page_navigate:解析初始url頁面,迴圈爬取各初始url頁面下的所有頁碼連結;

  • parse:爬取每個頁碼下的所有詳細房源連結,提取相應的欄位資訊,並儲存至items中;

下面是三個函式的功能描述,以及程式碼實現。

start_requests

任何爬蟲都需要有初始url,然後由初始url繼續深入爬取進一步的url,直到爬取到所需資料。由於鏈家二手房url的特徵是,由一個基礎url和各大區拼音拼接組成,因此在start_requests函式中定義了base_url的基礎url,和需要拼接的北京各大區的拼音列表。

然後由這些拼接的各大區url作為所有的初始url連結,並由scrapy.Request方法對每個連結發出非同步請求,程式碼如下:

class LianjiaSpider(scrapy.Spider):
    name = 'lianjia'
    base_url = 'https://bj.lianjia.com/ershoufang/'

    def start_requests(self):
        district = ['dongcheng', 'xicheng', 'chaoyang', 'haidian', 'fengtai', 'shijingshan', 'tongzhou', 'changping',
                    'daxing', 'yizhuangkaifaqu', 'shunyi', 'fangshan', 'mentougou', 'pinggu', 'huairou',
                    'miyun', 'yanqing', 'yanjiao', 'xianghe']
        for elem in district:
            region_url = self.base_url + elem
            yield scrapy.Request(url=region_url, callback=self.page_navigate)
複製程式碼

page_navigate

對每個大區url發出非同步請求後,我們需要對各大區內的所有房源列表url進行進一步的爬取,而為了能夠順利的將全部內容爬取,我們就要解決頁碼迴圈的問題。在page_navigate函式中,使用BeautifulSoup解析html,提取頁面中的pages資料。

BeautifulSoup的具體使用方法參見:Python爬蟲之BeautifulSoup解析之路

爬取獲得的pages資料是json字串,所以需要使用json.loads將其轉換為字典格式,然後得到max_number。最後通過for迴圈不斷髮送每個頁碼url的連結完成非同步請求,並使用callback呼叫進入下一步的函式中,程式碼如下:


    def page_navigate(self, response):
        soup = BeautifulSoup(response.body, "html.parser")
        try:
            pages = soup.find_all("div", class_="house-lst-page-box")[0]
            if pages:
                dict_number = json.loads(pages["page-data"])
                max_number = dict_number['totalPage']
                for num in range(1, max_number + 1):
                    url = response.url + 'pg' + str(num) + '/'
                    yield scrapy.Request(url=url, callback=self.parse)
        except:
            logging.info("*******該地區沒有二手房資訊********")

複製程式碼

parse

parse函式中,首先通過BeautifulSoup解析每個頁碼下的所有房源列表資訊,得到house_info_list。鏈x房源列表中沒有所在大區資訊,但是房源所在區域對於後續資料分析是很重要的,而僅通過頁面解析我們沒辦法獲取。為了獲得這個欄位該如何實現呢?

我們可以通過response.url來判斷,因為url正好是我們開始用所在區域拼接而成的,我們構造url的時候已經包含了大區資訊。那麼簡單的通過辨識url中的大區拼音,就可以解決該問題了。然後使用字典table將對應的中文所在區名對映到Region欄位中。

接下來開始對房源列表 house_info_list中的每個房源資訊info進行解析。根據鏈x的頁面結構,可以看到,每個info下有三個不同位置的資訊組,可通過class_引數進行定位。這三個位置資訊分別是house_info,position_info,price_info,每組位置下包含相關欄位資訊。

  • house_info:如圖包含Garden,Size,Layout,Direction,Renovation,Elevator房屋構造等欄位資訊;

  • position_info:如圖包含Floor,Year,District等位置年限欄位資訊;

  • price_info:如圖包含Total_price,price等欄位資訊;

這裡說的位置不同是在前端html頁面中的標籤位置不同。

從爬蟲到機器學習預測,我是如何一步一步做到的?

具體操作方法參見下面程式碼:


    def parse(self, response):
        item = LianjiaSpiderItem()
        soup = BeautifulSoup(response.body, "html.parser")

        #獲取到所有子列表的資訊
        house_info_list = soup.find_all(name="li", class_="clear")

        # 通過url辨認所在區域
        url = response.url
        url = url.split('/')
        item['Region'] = table[url[-3]]

        for info in house_info_list:
            item['Id'] = info.a['data-housecode']

            house_info = info.find_all(name="div", class_="houseInfo")[0]
            house_info = house_info.get_text()
            house_info = house_info.replace(' ', '')
            house_info = house_info.split('/')
            # print(house_info)
            try:
                item['Garden'] = house_info[0]
                item['Layout'] = house_info[1]
                item['Size'] = house_info[2]
                item['Direction'] = house_info[3]
                item['Renovation'] = house_info[4]
                if len(house_info) > 5:
                    item['Elevator'] = house_info[5]
                else:
                    item['Elevator'] = ''
            except:
                print("資料儲存錯誤")

            position_info = info.find_all(name='div', class_='positionInfo')[0]
            position_info = position_info.get_text()
            position_info = position_info.replace(' ', '')
            position_info = position_info.split('/')
            # print(position_info)
            try:
                item['Floor'] = position_info[0]
                item['Year'] = position_info[1]
                item['District'] = position_info[2]
            except:
                print("資料儲存錯誤")

            price_info = info.find_all("div", class_="totalPrice")[0]
            item['Price'] = price_info.span.get_text()

            yield item

複製程式碼

對於鏈x的爬取,沒用xpath的原因是提取一些標籤實在不是很方便(只是針對於鏈x),因此博主採用了beautifulSoup。

scrapy爬取安x客

這部分之前就有分享過,可以參見:Scrapy爬取二手房資訊+視覺化資料分析

以下是核心的爬蟲部分,與鏈x爬取部分的思想一致,不同的是使用了xpath進行解析和ItemLoader對item載入儲存。

# -*- coding:utf-8 -*-

import scrapy
from scrapy.loader import ItemLoader
from anjuke.items import AnjukeItem

class AnjukeSpider(scrapy.Spider):
    name = 'anjuke'
    custom_settings = {
        'REDIRECT_ENABLED': False
    }
    start_urls = ['https://beijing.anjuke.com/sale/']

    def start_requests(self):
        base_url = 'https://beijing.anjuke.com/sale/'
        for page in range(1, 51):
            url = base_url + 'p' + str(page) + '/'
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        num = len(response.xpath('//*[@id="houselist-mod-new"]/li').extract())
        house_info = response.xpath('//*[@id="houselist-mod-new"]')
        print(house_info)
        for i in range(1, num + 1):
            l = ItemLoader(AnjukeItem(), house_info)

            l.add_xpath('Layout', '//li[{}]/div[2]/div[2]/span[1]/text()'.format(i))
            l.add_xpath('Size', '//li[{}]/div[2]/div[2]/span[2]/text()'.format(i))
            l.add_xpath('Floor', '//li[{}]/div[2]/div[2]/span[3]/text()'.format(i))
            l.add_xpath('Year', '//li[{}]/div[2]/div[2]/span[4]/text()'.format(i))
            l.add_xpath('Garden', '//li[{}]/div[2]/div[3]/span/text()'.format(i))
            l.add_xpath('Region', '//li[{}]/div[2]/div[3]/span/text()'.format(i))
            l.add_xpath('Price', '//li[{}]/div[3]/span[1]/strong/text()'.format(i))

            yield l.load_item()
複製程式碼

安x客的反爬比較嚴重,如果不使用代理ip池,速度過快非常容易掛掉。而鏈x的反爬相對沒那麼嚴格,速度可以很快。

總結

以上是對本專案爬蟲部分核心內容的分享,至此這個專案完成了從爬蟲到資料分析,再到資料探勘預測的 "三部曲"完整過程。雖然這個專案比較簡單,仍有很多地方需要完善,但是希望通過這個專案能讓大家對整個過程有個很好的認識和了解。

從爬蟲到機器學習預測,我是如何一步一步做到的?

關注微信公眾號:Python資料科學,發現更多精彩內容。

相關文章