Xpath爬取链家二手房数据并利用Echarts可视化

前言

最近在自学python语言的过程中,接触了多种爬虫的手段,我本爱挑剔私以为Requests+Xpath是用起来最优雅的,可惜一直没有找到称手的可视化工具。前两天在读知乎的高赞回答有哪些值得推荐的数据可视化工具?时发现了Echarts,非常惊艳,遂决定爬取链家二手房数据进行可视化,在测试自己爬虫实力的同时检验下Echarts的实际效果。

img


策略

  • 采用Requests+Xpath工具采集链家网北京二手房前100页信息,数据标签:名称地址每平米单价网址总价,并保存为CSV(表格)
  • 调用百度地图API :实现具体地址到地图经纬度换算,并提取名称每平米单价地址经纬度三个标签保存为json格式(Echarts的数据导入只识别json格式)
  • 将数据导入Echarts,设置图标样式,导入第三方地图脚本,完成!

    (这步虽然写得最短,但是我爬最多坑的地方,主要原因是这货是纯Javascript写的,还好官网提供了大量案例,这才摸着石头过了河)

    来看下最终结果:

    3D图表:点此预览

    img

    2D图表:点此预览

    img

环境

  • Python 3.6.1
  • Windows 7 64位
  • PyCharm 2017.1.4

爬取数据

检查目标网页

我们先来看看目标网页的情况

img

发现:

  • 链家不需要登录即可查阅二手房信息
  • 用chrome检查网页请求,也没有cookietoken等一类变态的反爬手段

至此,放心了一大半,说明我们不需要做太多的伪装即可直接获取到我们的目标数据

定位目标数据

下面我们来看下目标数据的页面格式

翻页逻辑

链家二手房第一页的URL是这样的:

1
https://bj.lianjia.com/ershoufang/

这个url中并没有带有明显的页面标识,别急,我们来看看第二页的url:

1
https://bj.lianjia.com/ershoufang/pg2/

这下翻页逻辑就暴露了,这个pg后面的数字可能指代的就是页数,我们再看看后面的页数加以验证:

1
2
3
4
https://bj.lianjia.com/ershoufang/pg3/
https://bj.lianjia.com/ershoufang/pg4/
https://bj.lianjia.com/ershoufang/pg5/
.......

看来我们判断的没错,按照这个逻辑我们来看看/pg1/能不能打开第一页:

1
https://bj.lianjia.com/ershoufang/pg1/

打开没有问题!看来依次增加pg后面的数字即可实现翻页。

数据定位

用chrome右击的检查(Ctrl+shift+I)功能,我们可以方便地查看和定位网页元素:

img

名称

img

地址

img

每平米单价

img

网址

查看网页代码发现详情页面的网址就藏在之前存放名称的<a> 标签下的 <herf>标签内。

总价

img

编写爬虫代码

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
# coding:utf-8

import requests
import csv
from lxml import html

url='https://bj.lianjia.com/ershoufang/pg{page}/' #目标网页

if __name__ == '__main__':
start_page=0
end_page=100 #爬取终止页数
with open('bjrent.csv','w',newline='') as f: #以读写模式打开CSV
csv_writer=csv.writer(f)
print('start.....')
while start_page<=end_page: #循环页数终止判断
start_page+=1
print('get:{0}'.format(url.format(page=start_page)))
response=requests.get(url.format(page=start_page))
tree = html.fromstring(response.text)
el = tree.xpath('//div[@class="info clear"]')
for house in el: #遍历每个房产项目
house_title = house.xpath('div[@class="title"]/a/text()') #名称
house_addr = house.xpath('div[@class="address"]/div[@class="houseInfo"]/a/text()')[0] #地址
house_totalprice = house.xpath('div[@class="priceInfo"]/div[@class="totalPrice"]/span/text()') #单价
house_unitprice = house.xpath('div[@class="priceInfo"]/div[@class="unitPrice"]/span/text()') #总价
house_url = house.xpath('div[@class="title"]/a/@href') #详情页url网址
#整理数据
house_title = ''.join(house_title)
house_addr = ''.join(house_addr)
house_totalprice = ''.join(house_totalprice)
house_unitprice = ''.join(house_unitprice)
house_url = ''.join(house_url)

#数据写入CSV
csv_writer.writerow([house_title, house_addr, house_unitprice, house_url, house_totalprice+str('万')])
print('end.....')

注:

查看数据

爬取完成后会在指定目录下生成一个.csv文件,我们可以打开它来查看爬取的数据。

img

链家100页的爬取我们得到了3000个房源数据,实际上看链家的年销售数据,它的房源数据肯定是3W+的,也许我们可以通过在100页上继续叠加爬取实现,而事实也是这样的。

只不过我们目前的3000个样本数据已经很能说明问题了,在这里我就不全部爬完了。

调用百度地图API实现经纬度换算

百度API的调用方法可看这篇文章,写得很好。

获取密匙(AK)

经纬度换算前,需要注册百度地图api(你要用百度的账号)以获取免费的密钥,才能完全使用该api。登录网址:百度开放平台

首页点击申请密钥按钮,经过填写个人信息、邮箱注册等,成功之后在开放平台上点击“创建应用”,填写相关信息,在这里特别说明的是,在IP白名单框里,如果不清楚自己的IP地址,最好设置为:0.0.0.0/0,虽然百度提醒它会有泄露使用的风险,但是有时候你把你自己的IP地址输进去可能也不行。提交后,在你创建应用的访问应用(AK)那一栏就是你的密钥。

经纬度换算

注册密钥后就可以在百度Web服务API下的Geocoding API接口来获取你所需要地址的经纬度坐标并转化为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
# coding:utf-8

import json
from urllib.request import urlopen, quote
import csv

def getlnglat(address):
url = 'http://api.map.baidu.com/geocoder/v2/'
output = 'json'
ak = '***************' #在这里填写你的密匙
add = quote(str('北京市') + address) #由于本文城市变量为中文,为防止乱码,先用quote进行编码
uri = url + '?' + 'address=' + add + '&output=' + output + '&ak=' + ak #传参指定格式,见API文档
req = urlopen(uri)
res = req.read().decode() #将其他编码的字符串解码成unicode
temp = json.loads(res) #对json数据进行解析
return temp

file = open(r'D:\demo\tanjinhouse\beijinghouse\point.json','w',encoding='utf-8') #建立json数据文件
with open(r'D:\demo\tanjinhouse\beijinghouse\bjrent.csv', 'r') as csvfile: #打开上一步爬取出来csv保存地址
reader = csv.reader(csvfile)
for line in reader: #读取csv里的数据
try:
# 忽略第一行
# if reader.line_num == 1: #由于第一行为变量名称,故忽略掉
# continue
# line是个list,取得所有需要的值
b = line[1].strip() # 将第二列addr读取出来并清除不需要字符
c = line[2].strip('单价元/平米') # 将第三列price读取出来并清除不需要字符
lng = getlnglat(b)['result']['location']['lng'] # 采用构造的函数来获取经度
lat = getlnglat(b)['result']['location']['lat'] # 获取纬度
# str_temp = '{"lat":' + str(lat) + ',"lng":' + str(lng) + ',"count":' + str(c) +'},'
str_temp = '{"name":"' + str(b) + '","value":[' + str(lng) + ',' + str(lat) + ',' + str(c) + ']},'
# print(str_temp) #也可以通过打印出来,把数据copy到百度热力地图api的相应位置上
file.write(str_temp) # 写入文档
except:
print("something wrong about city:" + str(b)) #转换失败即打印错误项目
file.close() #保存

注:百度开发平台的经纬度转换对于免费账户来说每天是有上限的,如果你的转换出现了连续的报错,有可能就是这个原因。

最终保存出来的是json文档。

使用Echarts实现可视化

ECharts是一个免费的,功能强大的图表和可视化库,为您的提供了一种简单的方法,即添加直观,交互式和高度可定制的图表。它是用纯JavaScript编写的,基于zrender,这是一个全新的轻量图表库。

使用前请看:

当你发现看不进去(和我一样),趁早看这个:

ECharts 提供了常规的折线图柱状图散点图饼图K线图,用于统计的盒形图,用于地理数据可视化的地图热力图线图,用于关系数据可视化的关系图treemap,多维数据可视化的平行坐标,还有用于 BI 的漏斗图仪表盘

但这都不是其最强大之处,ECharts最让人惊艳之处在于其图表的超高交互性和可定制性,以及最新的多维可视化图表(GL图表)。

感受一下:

来自pissangBar3D - Global Population图表(我也是参考了他的GL图表)

ECharts图表的制作过程分为以下几步:

  • 首先我们需注册ECharts账号(百度账号)
  • 创建一个新的图表
  • 导入数据
  • 编写图表设置代码(JavaScript)
  • 添加第三方脚本
  • 保存分享

制作2D图表

新建图表后,点击导入数据将我们之前保存的json数据导入

然后编写图表设置代码

这里我直接上代码:

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
var uploadedDataURL = "/asset/get/s/data-1503645606962-ryy3JIad-.json"; # 导入的数据

var t_pos = {
left: 0,
top: 0
}

var last_point = [0, 0];
var counter = 0;
myChart.showLoading();
myChart.setOption(option = {
title: {
x: 'right',
text: "北京房价可视化",
subtext: '数据提取自链家 By--Hahajoker',
textStyle: {
color: '#fff',
fontSize: 30
}
},
tooltip: {
trigger: 'item',
transitionDuration: 0,
position: function(point, params, dom, rect, size) {
var least_area = 20;
var offset_x = 30; /* 相对于point的偏移 */
var offset_y = 30;

/* last_point的least_area范围内不会产生新的t_pos */
if (Math.abs(point[0] - last_point[0]) < least_area &&
Math.abs(point[1] - last_point[1]) < least_area &&
counter >= 2) {
return t_pos;
}
if (Math.abs(point[0] - last_point[0]) >= least_area ||
Math.abs(point[1] - last_point[1]) >= least_area) {
counter = 0;
}
counter += 1;
/* 使real_x,real_y有数值 */
if (counter == 1) {
t_pos.left = point[0] + offset_x;
t_pos.top = point[1] + offset_y;
last_point = [point[0], point[1]];
}
/* 此处进行修正tooltip的位置 */
if (counter == 2) {
var real_x = $(dom).position().left;
var real_y = $(dom).position().top;
t_pos.left += point[0] - real_x + offset_x;
t_pos.top += point[1] - real_y + offset_y;
}
return t_pos;
},
formatter: function(params) {
return params.name + ' : ' + params.value[2] + '元/平方米';
}
},
animation: false,
bmap: {
center: [116.39, 39.90], # 地图中心坐标
zoom: 11, # 地图大小
roam: true,
mapStyle: {
'styleJson': [{
"featureType": "water",
"elementType": "all",
"stylers": {
"color": "#021019"
}
}, {
"featureType": "highway",
"elementType": "geometry.fill",
"stylers": {
"color": "#000000"
}
}, {
"featureType": "highway",
"elementType": "geometry.stroke",
"stylers": {
"color": "#147a92"
}
}, {
"featureType": "arterial",
"elementType": "geometry.fill",
"stylers": {
"color": "#000000"
}
}, {
"featureType": "arterial",
"elementType": "geometry.stroke",
"stylers": {
"color": "#0b3d51"
}
}, {
"featureType": "local",
"elementType": "geometry",
"stylers": {
"color": "#000000"
}
}, {
"featureType": "land",
"elementType": "all",
"stylers": {
"color": "#08304b"
}
}, {
"featureType": "railway",
"elementType": "geometry.fill",
"stylers": {
"color": "#000000"
}
}, {
"featureType": "railway",
"elementType": "geometry.stroke",
"stylers": {
"color": "#08304b"
}
}, {
"featureType": "subway",
"elementType": "geometry",
"stylers": {
"lightness": -70
}
}, {
"featureType": "building",
"elementType": "geometry.fill",
"stylers": {
"color": "#000000"
}
}, {
"featureType": "all",
"elementType": "labels.text.fill",
"stylers": {
"color": "#857f7f"
}
}, {
"featureType": "all",
"elementType": "labels.text.stroke",
"stylers": {
"color": "#000000"
}
}, {
"featureType": "building",
"elementType": "geometry",
"stylers": {
"color": "#022338"
}
}, {
"featureType": "green",
"elementType": "geometry",
"stylers": {
"color": "#062032"
}
}, {
"featureType": "boundary",
"elementType": "all",
"stylers": {
"color": "#1e1c1c"
}
}, {
"featureType": "manmade",
"elementType": "all",
"stylers": {
"color": "#022338"
}
}]
}
},
visualMap: {
//type: 'piecewise',
top: '5%',
splitNumber: 5,
min: 0,
max: 10,
seriesIndex: 0,
calculable: true,
inRange: {
color: ['green', '#eac736', '#d94e5d']
},
textStyle: {
color: '#fff',
formatter: 'aaaa{value}bbbb{value2}' // 范围标签显示内容。
}

},
series: [{
type: 'scatter',
coordinateSystem: 'bmap',
symbolSize: 8,
label: {
normal: {
show: false
},
emphasis: {
show: false
}
},
}]
});
if (!app.inNode) {
// 添加百度地图插件
var bmap = myChart.getModel().getComponent('bmap').getBMap();
bmap.addControl(new BMap.MapTypeControl());
}


if (option && typeof option === "object") {
myChart.setOption(option, true);
}

;
$.getJSON(uploadedDataURL, function(linedata) {

myChart.hideLoading();
myChart.setOption({

visualMap: {
max: linedata[1]
},
series: [{
data: linedata[0]
}]
});
});

在这个图表中我们引入了两个第三方脚本url:

1
http://api.map.baidu.com/api?v=2.0&ak=ZUONbpqGBsYGXNIYHicvbAbM
1
/dep/echarts/latest/extension/bmap.min.js

脚本选项中将他们依次添加

点击刷新即可生成图表:

点此预览

img

制作3D图表

新建图表后,点击导入数据将我们之前保存的json数据导入

然后编写图表设置代码

我的图表代码:

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
var uploadedDataURL = "/asset/get/s/data-1503645606962-ryy3JIad-.json";   #导入数据

mapboxgl.accessToken = mapboxglToken;

myChart.showLoading();

$.getJSON(uploadedDataURL, function(linedata) {

myChart.hideLoading();

myChart.setOption({

visualMap: {
show: false,
calculable: true,
realtime: false,
inRange: {
color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']
},
outOfRange: {
colorAlpha: 0
},
max: linedata[1]
},
mapbox: {
center: [116.39, 39.90], #地图中心坐标
zoom: 10,
pitch: 50,
bearing: -10,
style: 'mapbox://styles/mapbox/light-v9',
boxHeight: 50,
// altitudeScale: 3e2,
postEffect: {
enable: true,
SSAO: {
enable: true,
radius: 2,
intensity: 1.5
}
},
light: {
main: {
intensity: 1,
shadow: true,
shadowQuality: 'high'
},
ambient: {
intensity: 0.
},
ambientCubemap: {
texture: '/asset/get/s/data-1491896094618-H1DmP-5px.hdr',
exposure: 1,
diffuseIntensity: 0.5
}
}
},
series: [{
type: 'bar3D',
shading: 'realistic',
coordinateSystem: 'mapbox',
barSize: 0.2,
silent: true,
data: linedata[0]
}]
});
});

在这个图表中我们引入了三个第三方脚本url:

1
https://api.mapbox.com/mapbox-gl-js/v0.38.0/mapbox-gl.js
1
http://echarts.baidu.com/resource/echarts-gl-latest/dist/echarts-gl.min.js
1
http://echarts.baidu.com/resource/echarts-gl-latest/mapboxgl-token.js

脚本选项中将他们依次添加

点击刷新即可生成图表:

点此预览

img

在这里,我本来在考虑看能不能有方法将3D图表的三维数据导出,这样我就能利用3D打印将这个图表实体化了打印出来了,可惜Echarts并没有给出这样的接口,如果你们有好的建议请告诉我,不胜感激。

用钱砸我,不要停!