爬了杭州的租房数据,原来……

大家好,我是“猫玛尼”,一名程序员。

 

在外打工,大部分人每个月总要花一笔钱在租房上面,一起来看看杭州的租房情况。

 

数据来源是F天下,该网站,按照百度的说法:“是全球最大的房地产家居网络平台”,数据源靠谱。

 

一共爬取到15485条出租房源数据,按照区域分布如下:

爬了杭州的租房数据,原来……

 

【数据分析】

一、房源分布

我们可以清晰地看到,几大城区,房源数量基本上都比较接近。上下两城和拱墅较少一点,这也符合实际情况,近几年杭州往外扩,余杭、萧山、滨江等地区房源自然也多了。后面5个周边,桐庐、富阳、临安、建德、杭州周边,房源较少。

爬了杭州的租房数据,原来……

 

二、租金差异

标价大,不一定就代表实际租金高,还需要考虑标价对应的出租面积,比如A房源4000块每月(面积150平米),B房源2600块每月(面积50平米),显然不能直接说4000块每月的贵。得把月租金,均摊到每平米,就能做出公平的比较。即一平米每月需要多少钱:

A房源:4000块每月 / 150平米 = 26.67

B房源:2600块每月 /  50平米  =  52

计算之后发现B房源更贵。

 

按照这个思路,我们计算出各个地区,一平米每月多少价格。计算的是平均数:

爬了杭州的租房数据,原来……

 

数值做了四舍五入,取整。其中江干、西湖、滨江、上城,价格都超过了50。

 

我们来计算一下各主要城区,租住一间20平米的房间,房租平均要花费多少钱:

江干:20 * 55 = 1100 

余杭:38 * 20 = 760

西湖:53 * 20 = 1060

萧山:40 * 20 = 800

滨江:60 * 20 = 1200

下城:47 * 20 = 940

上城:68 * 20 = 1360

拱墅:45 * 20 = 900

 

大家可以看下自己是高于平均还是低于平均。总体上,房租每个月花费1000,在杭州基本是少不了的。

 

这个统计,和我们平时的认知还是比较符合的,越往周边,租金越便宜。滨江,互联网公司较多,里面有好多拿着高工资的程序员、产品经理,他们消费能力强,当地的租金自然也水涨船高了。

 

从图表来看,余杭相对来说租金较便宜,如果不计较路程的话,租住在余杭也是个不错的选择。

 

三、租住方式

整租数量最大:

爬了杭州的租房数据,原来……

四、户型

经过统计,1室1厅、3室2厅、2室1厅、2室2厅最多,都是主流户型。再其他的户型,数量就很少了,我把他们合并成了“其他”:

爬了杭州的租房数据,原来……

五、房屋特色

爬了杭州的租房数据,原来……

 

这个统计,可以很清晰的看出卖家的营销套路,基本都是给房源标上类似“拎包入住”、“随时看房”、“随时入住”、“家电齐全”、“南北通透”。

 

这个从侧面也说明了,大家租房会比较看重:是否能够直接、简便的入住。

 

图中“合租男生”、“合租女生”看不太清,实际上这两个是差了一倍的,虽然数据样本总体不算大,但还是能看出来女生更受欢迎一些,我猜想可能是女生比较爱干净吧。

 

其实还有更多有意思的分析,篇幅原因,就分析到这里了。

 

【原始数据】

原始数据提取地址如下:

https://pan.baidu.com/s/1m3JXHFsmqg0StTFWmnKx3Q

 

【代码】

数据源:F天下(机智的你,应该知道是哪个网站)的租房栏目

 

只需要创建两张表,如下:

BEGIN;

### 房天下所有城市的主页信息
DROP TABLE IF EXISTS `sou_fang_city_index`;
CREATE TABLE `sou_fang_city_index` (
  `id`             INT         NOT NULL AUTO_INCREMENT
  COMMENT '数据库自增ID',
  `create_time`    DATETIME    NOT NULL DEFAULT '1970-01-01 00:00:01'
  COMMENT '数据创建时间',
  `modify_time`    DATETIME    NOT NULL DEFAULT '1970-01-01 00:00:01'
  COMMENT '数据修改时间',

  `province_name`  VARCHAR(40) NULL
  COMMENT '省份名称',
  `city_name`      VARCHAR(10) NOT NULL
  COMMENT '城市名称',
  `city_index_url` VARCHAR(40) NOT NULL
  COMMENT '城市首页链接',

  PRIMARY KEY (`id`),
  UNIQUE KEY `uk`(`city_index_url`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '房天下所有城市的主页信息';

# 房天下租房数据
DROP TABLE IF EXISTS `sou_fang_renting`;
CREATE TABLE `sou_fang_renting` (
  `id`            INT          NOT NULL AUTO_INCREMENT
  COMMENT '数据库自增ID',
  `create_time`   DATETIME     NOT NULL DEFAULT '1970-01-01 00:00:01'
  COMMENT '数据创建时间',
  `modify_time`   DATETIME     NOT NULL DEFAULT '1970-01-01 00:00:01'
  COMMENT '数据修改时间',

  `city_index_id` INT          NOT NULL
  COMMENT 'sou_fang_city_index的自增ID',
  `province_name` VARCHAR(40)  NULL
  COMMENT '省份名称',
  `city_name`     VARCHAR(10)  NOT NULL
  COMMENT '城市名称',
  `area_name`     VARCHAR(20)  NOT NULL
  COMMENT '区域名称',

  `detail_url`    VARCHAR(120) NOT NULL
  COMMENT '房屋详情的url',

  `name`          VARCHAR(50)
  COMMENT '名称',
  `rent_way`      VARCHAR(4)
  COMMENT '出租方式',
  `door_model`    VARCHAR(4)
  COMMENT '户型',
  `area`          VARCHAR(10)
  COMMENT '建筑面积',
  `toward`        VARCHAR(10)
  COMMENT '朝向',
  `unit_price`    VARCHAR(10)
  COMMENT '单价',
  `feature`       VARCHAR(100)
  COMMENT '特色',

  PRIMARY KEY (`id`),
  UNIQUE KEY (`detail_url`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '房天下租房数据';

# 搜房网-小区详情首页-小区详情-原始数据
DROP TABLE IF EXISTS `fang_community_detail`;
CREATE TABLE `fang_community_detail` (
  `id`                        INT      NOT NULL AUTO_INCREMENT
  COMMENT '数据库自增ID',
  `create_time`               DATETIME NOT NULL DEFAULT '1970-01-01 00:00:01'
  COMMENT '数据创建时间',
  `modify_time`               DATETIME NOT NULL DEFAULT '1970-01-01 00:00:01'
  COMMENT '数据修改时间',

  `community_id`              INT      NOT NULL
  COMMENT 'fang_community的自增ID',

  # 基本信息
  `address`                   VARCHAR(128)
  COMMENT '小区地址',
  `area`                      VARCHAR(32)
  COMMENT '所属区域',
  `postcode`                  VARCHAR(8)
  COMMENT '邮编',
  `property_description`      VARCHAR(32)
  COMMENT '产权描述',
  `property_category`         VARCHAR(8)
  COMMENT '物业类别',
  `completion_time`           VARCHAR(20)
  COMMENT '竣工时间',
  `building_type`             VARCHAR(64)
  COMMENT '建筑类别',
  `building_area`             VARCHAR(32)
  COMMENT '建筑面积',
  `floor_area`                VARCHAR(32)
  COMMENT '占地面积',
  `current_number`            VARCHAR(10)
  COMMENT '当期户数',
  `total_number`              VARCHAR(10)
  COMMENT '总户数',
  `greening_rate`             VARCHAR(10)
  COMMENT '绿化率',
  `plot_ratio`                VARCHAR(10)
  COMMENT '容积率',
  `property_fee`              VARCHAR(20)
  COMMENT '物业费',
  `property_office_telephone` VARCHAR(100)
  COMMENT '物业办公电话',
  `property_office_location`  VARCHAR(40)
  COMMENT '物业办公地点',
  `additional_information`    VARCHAR(32)
  COMMENT '附加信息',

  PRIMARY KEY (`id`),
  UNIQUE KEY (`community_id`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COMMENT = '搜房网-小区详情首页-小区详情-原始数据';

COMMIT;

 

我先是爬取了所有的城市数据,虽然我们这次只关心杭州的情况,不过抓下来所有的城市,以后也用得到。打开网站我就去找Json数据API,发现并没有,所以只能采取普通的提取页面数据的方式来获取数据了。具体的代码如下:

"""
增量爬取
房天下-所有城市的主页
该爬虫,一般情况只需要爬取一次就够了:因为中国的城市变化,个人觉得是不频繁的
页面:http://www.fang.com/SoufunFamily.htm
"""

from scrapy import Selector
from scrapy.spiders import Spider

from thor_crawl.spiders.spider_setting import DEFAULT_DB_ENV
from thor_crawl.utils.commonUtil import CommonUtil
from thor_crawl.utils.db.daoUtil import DaoUtils


class CityIndex(Spider):
    name = 'sou_fang_city_index'
    handle_httpstatus_list = [204, 206, 404, 500]

    start_urls = ['http://www.fang.com/SoufunFamily.htm']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # ============ 工具 ============
        self.dao = DEFAULT_DB_ENV
        self.common_util = CommonUtil()

        # ============ 持久化相关变量定义 ============
        self.save_threshold = 20  # 一次性插入数据库阈值
        self.persistent_data = list()  # 内存暂存处理的数据,批量插入数据库
        self.main_table = 'sou_fang_city_index'  # 数据库存储表

    def __del__(self):
        self.save_final()

    def closed(self, res):
        self.save_final()

    def parse(self, response):
        try:
            body = response.body.decode('gb18030').encode('utf-8')
        except UnicodeDecodeError as e:
            print(e)
            body = response.body
        hxf = Selector(text=body)

        trs = hxf.xpath('//div[@id="c02"]/table/tr')  # 获取所有的行数据
        this_province = '未知'

        for tr in trs[:-1]:
            province_name = self.common_util.get_extract(tr.xpath('td[2]/strong/text()'))  # 获取省份名称文本值
            this_province = this_province if province_name is None or province_name == '' else province_name  # 为空的话取之前的省份名称

            cities = tr.xpath('td[3]/a')  # 获取所有的城市列表
            for city in cities:
                city_name = self.common_util.get_extract(city.xpath('text()'))  # 获取城市名称文本值
                city_index_url = self.common_util.get_extract(city.xpath('@href'))  # 获取城市首页链接
                self.persistent_data.append(
                    {
                        'province_name': this_province,
                        'city_name': city_name,
                        'city_index_url': city_index_url
                    }
                )
        self.save()

    def save(self):
        if len(self.persistent_data) > self.save_threshold:
            try:
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
            except AttributeError as e:
                self.dao = DaoUtils()
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
                print('save except:', e)
            finally:
                self.persistent_data = list()

    def save_final(self):
        if len(self.persistent_data) > 0:
            try:
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
            except AttributeError as e:
                self.dao = DaoUtils()
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
                print('save_final except:', e)
            finally:
                self.persistent_data = list()

 

然后是爬取杭州所有的出租房源数据,思路是通过杭州这个城市站的首页的“租房”菜单,进入房源列表,然后,根据不同的城区,去爬取数据,具体代码如下:

"""
搜房网-租房信息
"""
import re

import scrapy
from scrapy import Selector
from scrapy.spiders import Spider

from thor_crawl.spiders.spider_setting import DEFAULT_DB_ENV
from thor_crawl.utils.commonUtil import CommonUtil
from thor_crawl.utils.db.daoUtil import DaoUtils


class Renting(Spider):
    name = 'sou_fang_renting'
    handle_httpstatus_list = [302, 204, 206, 404, 500]

    start_urls = ['http://www.souFang.com/SoufunFamily.htm']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # ============ 工具 ============
        self.dao = DEFAULT_DB_ENV
        self.common_util = CommonUtil()

        # ============ 持久化相关变量定义 ============
        self.save_threshold = 20  # 一次性插入数据库阈值
        self.persistent_data = list()  # 内存暂存处理的数据,批量插入数据库
        self.main_table = 'sou_fang_renting'  # 数据库存储表

        # ============ 业务 ============
        province_name = '浙江'
        city_name = '杭州'
        self.target = 'SELECT id, province_name, city_name, city_index_url ' \
                      'FROM sou_fang_city_index ' \
                      'WHERE province_name = "{province_name}" and city_name = "{city_name}"'.format(province_name=province_name, city_name=city_name)
        self.url_template = 'http://{city_code}.zu.fang.com/'  # 租房首页的模板URL

    def __del__(self):
        self.save_final()

    def start_requests(self):
        start_requests = list()
        for row in self.dao.get_all(self.target):
            if row['city_index_url'] != '':
                meta = {
                    'city_index_id': row['id'],
                    'province_name': row['province_name'],
                    'city_name': row['city_name']
                }
                url = self.url_template.format(city_code=re.search(r'http://(.+)\.fang\.com', row['city_index_url']).group(1))
                start_requests.append(scrapy.FormRequest(url=url, method='GET', meta=meta))
        return start_requests

    def closed(self, res):
        self.save_final()

    # 拿到所有的地区,去掉"不限"
    def parse(self, response):
        try:
            body = response.body.decode('gb18030').encode('utf-8')
        except UnicodeDecodeError as e:
            print(e)
            body = response.body
        meta = response.meta
        url = response.url
        hxf = Selector(text=body)

        a_tag_list = hxf.xpath('//dl[@id="rentid_D04_01"]/dd/a')
        print('a_tag_list len: ', len(a_tag_list))

        if a_tag_list is None or len(a_tag_list) <= 1:
            print('------parse, no data in ', meta['province_name'], meta['city_name'])
        else:
            for a_tag in a_tag_list:
                meta['area_name'] = self.common_util.get_extract(a_tag.xpath('text()'))
                meta['area_url'] = self.common_util.get_extract(a_tag.xpath('@href'))
                meta['base_url'] = url

                if meta['area_name'] is not None and meta['area_name'] != '' and meta['area_name'] != '不限':
                    print(url + meta['area_url'])
                    yield scrapy.FormRequest(url=url + meta['area_url'], method='GET', meta=meta, callback=self.parse_area)

    def parse_area(self, response):
        try:
            body = response.body.decode('gb18030').encode('utf-8')
        except UnicodeDecodeError as e:
            print(e)
            body = response.body
        meta = response.meta
        url = response.url
        hxf = Selector(text=body)

        dl_tag_list = hxf.xpath('//div[@class="houseList"]/dl')
        print('dl_tag_list len: ', len(dl_tag_list))

        if dl_tag_list is None or len(dl_tag_list) <= 1:
            print('------parse_area, no data in ', meta['province_name'], meta['city_name'], meta['area_name'])
        else:
            for dl_tag in dl_tag_list:
                feature = ''
                feature_span_list = dl_tag.xpath('dd/p[5]/span')
                for feature_span in feature_span_list:
                    feature += self.common_util.get_extract(feature_span.xpath('text()')) + ','
                feature = feature[:-1] if len(feature) > 1 else feature
                self.persistent_data.append(
                    {
                        'city_index_id': meta['city_index_id'],
                        'province_name': meta['province_name'],
                        'city_name': meta['city_name'],
                        'area_name': meta['area_name'],
                        'detail_url': self.common_util.get_extract(dl_tag.xpath('dd/p[1]/a/@href')),
                        'name': self.common_util.get_extract(dl_tag.xpath('dd/p[1]/a/text()')),
                        'rent_way': self.common_util.get_extract(dl_tag.xpath('dd/p[2]/text()[1]')),
                        'door_model': self.common_util.get_extract(dl_tag.xpath('dd/p[2]/text()[2]')),
                        'area': self.common_util.get_extract(dl_tag.xpath('dd/p[2]/text()[3]')),
                        'toward': self.common_util.get_extract(dl_tag.xpath('dd/p[2]/text()[4]')),
                        'unit_price': self.common_util.get_extract(dl_tag.xpath('dd//span[@class="price"]/text()')),
                        'feature': feature
                    }
                )
        # 下一页
        page_a_list = hxf.xpath('//div[@class="fanye"]/a')
        if len(page_a_list) > 0:
            for page_a in page_a_list:
                if self.common_util.get_extract(page_a.xpath('text()')) == '下一页':
                    yield scrapy.FormRequest(
                        url=meta['base_url'] + self.common_util.get_extract(page_a.xpath('@href')), method='GET', meta=meta, callback=self.parse_area
                    )

        self.save()

    def save(self):
        if len(self.persistent_data) > self.save_threshold:
            try:
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
            except AttributeError as e:
                self.dao = DaoUtils()
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
                print('save except:', e)
            finally:
                self.persistent_data = list()

    def save_final(self):
        if len(self.persistent_data) > 0:
            try:
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
            except AttributeError as e:
                self.dao = DaoUtils()
                self.dao.customizable_add_ignore_batch(self.main_table, self.persistent_data)
                print('save_final except:', e)
            finally:
                self.persistent_data = list()

 

 

最后是做统计的sql和代码:

SELECT
  area_name,
  count(*) AS c
FROM sou_fang_renting
GROUP BY area_name
ORDER BY c DESC;

 

"""
算算你再杭州的租房成本
"""

from thor_crawl.utils.db.daoUtil import DaoUtils
from thor_crawl.utils.db.mysql.mySQLConfig import MySQLConfig


class RentInHz:
    def __init__(self, *args, **kwargs):
        # ============ 工具 ============
        self.dao = DaoUtils(**{'dbType': 'MySQL', 'config': MySQLConfig.localhost()})

    def calc(self):
        hz_data = self.dao.get_all('SELECT area_name, area, unit_price FROM sou_fang_renting')

        temp = dict()
        for row in hz_data:
            if row['area_name'] in temp:
                temp[row['area_name']].append(row)
            else:
                temp[row['area_name']] = list()
                temp[row['area_name']].append(row)

        result = list()
        for x, y in temp.items():
            total = 0
            num = 0
            for row in y:
                try:
                    # print(float(row['unit_price']))
                    # print(float(str(row['area']).replace('㎡', '')))
                    total += float(row['unit_price']) / float(str(row['area']).replace('㎡', ''))
                    num += 1
                except ValueError as e:
                    print(e, x, row)
            result.append({'城市': x, '平均数': total / num})
        print(result)

    def feature(self):
        hz_data = self.dao.get_all('SELECT feature FROM sou_fang_renting')

        feature_list = list()
        for row in hz_data:
            if row['feature'] is not None and row['feature'] != '':
                for x in str(row['feature']).split(","):
                    feature_list.append(x)

        temp = dict()
        for row in feature_list:
            if row in temp:
                temp[row] = temp[row] + 1
            else:
                temp[row] = 1
        print(temp)


if __name__ == '__main__':
    tj = RentInHz()
    tj.feature()

 

【我平时的开发环境和框架】

饭碗:Mac Pro 13寸

IDE:IntelliJ IDEA2018、PyCharm2017

JDK:8

打包:Maven 3

Python:2、3

Python 爬虫框架:Scrapy 1.3.3

 

欢迎围观《猫玛尼》