56

从爬虫到机器学习预测,我是如何一步一步做到的?

 5 years ago
source link: https://segmentfault.com/a/1190000016166016?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:xiaoyu

微信公众号: Python数据科学

知乎: python数据分析师

zuy6Vfz.jpg!web

前情回顾

前一段时间与大家分享了北京二手房房价分析的实战项目,分为分析和建模两篇。文章发出后,得到了大家的肯定和支持,在此表示感谢。

除了数据分析,好多朋友也对爬虫特别感兴趣,想知道爬虫部分是如何实现的。本篇将分享这个项目的爬虫部分,算是数据分析的一个 前传 篇。

爬虫前的思考

爬虫部分主要是通过爬取 链x安x客 来获取二手房住房信息,因为考虑到不同网站的房源信息可以互补,所以选择了两个网站。

爬取目标是北京二手房,仅针对一个城市而言,数据量并不大。所以直接采用 Scrapy 来完成爬取工作,然后将数据存储在csv格式的文件中。最终爬取结果是这样的,链x的爬虫爬取了 30000+ 条数据,安x客的爬虫爬取了 3000+ 条数据。不得不说链x的房源相对来讲还是比较全的。

scrapy爬取链x

写一个爬虫最开始当然要想清楚需要获取什么样的数据了。本次项目对与二手房相关的数据都比较感兴趣,可以自然的想到,每个房源链接的具体详细信息是最全的。但考虑到爬虫深度影响整体爬虫效率问题,并且房源列表中数据已经能够满足基本的要求,并没有必要对每个详细链接进行深入的爬取,因此最终选择爬取房源列表。以下是房源列表(部分截图)中的房源信息:

2ieUbq7.png!web

确定以上爬取内容后,就开始爬虫部分的工作。首先在 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页面中的标签位置不同。

bMf2eqU.png!web

具体操作方法参见下面代码:

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的反爬相对没那么严格,速度可以很快。

总结

以上是对本项目爬虫部分核心内容的分享,至此这个项目完成了 从爬虫到数据分析,再到数据挖掘预测的 "三部曲"完整过程 。虽然这个项目比较简单,仍有很多地方需要完善,但是希望通过这个项目能让大家对整个过程有个很好的认识和了解。

Ijau6zN.jpg!web

关注微信公众号: Python数据科学 ,发现更多精彩内容。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK