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

文章是从公众号《猫玛尼》迁移过来,内容稍有调整。


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

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

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

img

【数据分析】

一、房源分布

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

img

二、租金差异

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

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

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

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

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

img

数值做了四舍五入,取整。其中江干、西湖、滨江、上城,价格都超过了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,在杭州基本是少不了的。

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

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

三、租住方式

整租数量最大:

img

四、户型

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

img

五、房屋特色

img

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

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

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

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

【原始数据】

原始数据提取地址如下:

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

【代码】

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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,发现并没有,所以只能采取普通的提取页面数据的方式来获取数据了。具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""
增量爬取
房天下-所有城市的主页
该爬虫,一般情况只需要爬取一次就够了:因为中国的城市变化,个人觉得是不频繁的
页面: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()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""
搜房网-租房信息
"""
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和代码:

1
2
3
4
5
6
SELECT
area_name,
count(*) AS c
FROM sou_fang_renting
GROUP BY area_name
ORDER BY c DESC;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""
算算你再杭州的租房成本
"""

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

------------- 本文结束感谢阅读 -------------
给猫玛尼加个鸡腿~