专注于做有价值的技术原创

0%

python 命令行抓取分析北上广深房价数据

引言

昨天在老家,发布了一篇《python 自动抓取分析房价数据——安居客版》。在文末,第6小节提供了完整代码,可以在 python3 环境,通过命令行传入参数 cookie 自动抓取房价数据。今天回到深圳,才想到,这段脚本只能抓取西双版纳的房价数据,如果读者不自己修改,那么就无法抓取其他城市的房价数据。于是,决定“好事做到底,送佛送到西”,将脚本加以修改,以北上广深为例,提供灵活抓取分析其他城市房价的完整代码。

1. 完整 python 脚本

在上一篇的脚本基础上,稍加修改,将以下代码保存到文件 crawl_anjuke.py 中。

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
#!/usr/local/bin/python

import requests
from bs4 import BeautifulSoup
import pandas as pd
import matplotlib.pyplot as plt
import time
import argparse

def get_headers(city, page, cookie):
headers = {
'authority': '{}.anjuke.com'.format(city),
'method': 'GET',
'path': '/community/p{}/'.format(page),
'scheme': 'https',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'no-cache',
'cookie': cookie,
'pragma': 'no-cache',
'referer': 'https://{}.anjuke.com/community/p{}/'.format(city, page),
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36'
}
return headers

def get_html_by_page(city, page, cookie):
headers = get_headers(city, page, cookie)
url = 'https://{}.anjuke.com/community/p{}/'.format(city, page)
res = requests.get(url, headers=headers)
if res.status_code != 200:
print('页面不存在!')
return None
return res.text

def extract_data_from_html(html):
soup = BeautifulSoup(html, features='lxml')
list_content = soup.find(id="list-content")
if not list_content:
print('list-content elemet not found!')
return None
items = list_content.find_all('div', class_='li-itemmod')
if len(items) == 0:
print('items is empty!')
return None
return [extract_data(item) for item in items]

def extract_data(item):
name = item.find_all('a')[1].text.strip()
address = item.address.text.strip()
if item.strong is not None:
price = item.strong.text.strip()
else:
price = None
finish_date = item.p.text.strip().split(':')[1]
latitude, longitude = [d.split('=')[1] for d in item.find_all('a')[3].attrs['href'].split('#')[1].split('&')[:2]]
return name, address, price, finish_date, latitude, longitude

def is_in_notebook():
import sys
return 'ipykernel' in sys.modules

def clear_output():
import os
os.system('cls' if os.name == 'nt' else 'clear')
if is_in_notebook():
from IPython.display import clear_output as clear
clear()

def crawl_all_page(city, cookie, limit=0):
page = 1
data_raw = []
while True:
try:
if limit != 0 and (page-1 == limit):
break
html = get_html_by_page(city, page, cookie)
data_page = extract_data_from_html(html)
if not data_page:
break
data_raw += data_page
clear_output()
print('crawling {}th page ...'.format(page))
page += 1
except:
print('maybe cookie expired!')
break
print('crawl {} pages in total.'.format(page-1))
return data_raw

def create_df(data):
columns = ['name', 'address', 'price', 'finish_date', 'latitude', 'longitude']
return pd.DataFrame(data, columns=columns)

def clean_data(df):
df.dropna(subset=['price'], inplace=True)
df = df.astype({'price': 'float64', 'latitude': 'float64', 'longitude': 'float64'})
return df

def visual(df):
fig, ax = plt.subplots()
df.plot(y='price', ax=ax, bins=20, kind='hist', label='房价频率直方图', legend=False)
ax.set_title('房价分布直方图')
ax.set_xlabel('房价')
ax.set_ylabel('频率')
plt.grid()
plt.show()

def run(city, cookie, limit):
data = crawl_all_page(city, cookie, limit)
if len(data) == 0:
print('empty: crawled noting!')
return
df = create_df(data)
df = clean_data(df)
visual(df)

_price = df['price']
_max, _min, _average, _median, _std = _price.max(), _price.min(), _price.mean(), _price.median(), _price.std()
print('\n{} house price statistics\n-------'.format(city))
print('count:\t{}'.format(df.shape[0]))
print('max:\t¥{}\nmin:\t¥{}\naverage:\t¥{}\nmedian:\t¥{}\nstd:\t¥{}\n'.format(_max, _min, round(_average, 1), _median, round(_std, 1)))

df.sort_values('price', inplace=True)
df.reset_index(drop=True, inplace=True)
#  保存 csv 文件
dt = time.strftime("%Y-%m-%d", time.localtime())
csv_file = 'anjuke_{}_community_price_{}.csv'.format(city, dt)
df.to_csv(csv_file, index=False)

def get_cli_args():
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--city', type=str, help='city.')
parser.add_argument('-k', '--cookie', type=str, help='cookie.')
parser.add_argument('-l', '--limit', type=int, default=0, help='page limit (30 records per page).')
args = parser.parse_args()
return args

if __name__ == '__main__':
args = get_cli_args()
run(args.city, args.cookie, args.limit)

2. 新增参数说明

2.1 city

顾名思义,city 就是指定脚本将要抓取的城市。这个参数来自哪里,是不是随便传呢?当然不是,因为数据来自网站,因此,就必须是网站支持的城市。在安居客网站,体现为二级域名,如北京站是 beijing.anjuke.com ,那么获取北京站的 city 即为 beijing 。

2.2 limit

抓取最大分页数。之所以需要这个参数,因为抓取城市所有小区的数据,需要分页一次次抓取,通过观察,安居客分页是通过 url 传入的。正常思路,容易想到,从第1页开始,每成功获取1页数据,将页面变量加1, 直到获取不到数据。但是,在抓取深圳数据时,我发现,网站上看到最多只能查看到50页, 如下图所示。但实际,在抓取50页面后面的数据时,会返回 第1页的数据。这样,导致自动累加的策略失效,不能跳出循环。因此,需要增加 limit 参数,来手动指定加载最大的页面数。这个数,需要自己打开对应城市,如下图,找到最大页面数。以深圳为例(https://shenzhen.anjuke.com/community/p50/) ,limit 设置为 50 。

注:cookie 参数和上一篇 《python 自动抓取分析房价数据——安居客版》 一样

3. 命令行抓取北上广深数据

3.1 抓取北京房价数据

1
python crawl_anjuke.py --city beijing --limit 50 --cookie "sessid=5AACB464..."

3.2 抓取上海房价数据

1
python crawl_anjuke.py --city shanghai --limit 50 --cookie "sessid=5AACB464..."

3.3 抓取广州房价数据

1
python crawl_anjuke.py --city guangzhou --limit 50 --cookie "sessid=5AACB464..."

3.4 抓取深圳房价数据

1
python crawl_anjuke.py --city shenzhen --limit 50 --cookie "sessid=5AACB464..."

4. 数据分析

4.1 加载数据

运行 3 小节命令后,会在当前目录生成如下四个 csv 文件。后面日期为运行命令当天的日期。

  • anjuke_beijing_community_price_2019-09-17.csv
  • anjuke_shanghai_community_price_2019-09-17.csv
  • anjuke_guangzhou_community_price_2019-09-17.csv
  • anjuke_shenzhen_community_price_2019-09-17.csv
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
import pandas as pd
import time

dt = time.strftime("%Y-%m-%d", time.localtime())
cities = ['beijing', 'shanghai', 'guangzhou', 'shenzhen']
csv_files = ['anjuke_{}_community_price_{}.csv'.format(city, dt) for city in cities]

dfs = []

city_cn = {
'beijing': '北京',
'shanghai': '上海',
'guangzhou': '广州',
'shenzhen': '深圳'
}

for city in cities:
f = 'anjuke_{}_community_price_{}.csv'.format(city, dt)
df = pd.read_csv(f)
df.insert(0, 'city', city_cn[city])
dfs.append(df)

df = pd.concat(dfs, ignore_index=True)

df.sample(10)

city name address price finish_date latitude longitude
1313 北京 丽水莲花小区 [西城区-广安门外]莲花河胡同1号 101969.0 2006 39.890680 116.330223
2140 上海 海棠苑 [普陀区-真如]真北路1902弄1-65号 51336.0 1997 31.247430 121.391095
2643 上海 西凌新邨 [黄浦区-蓬莱公园]西凌家宅路27弄,111弄,137弄,西藏南路1374弄,制造局路365... 75157.0 1987 31.204728 121.487230
3501 广州 锦鸿花园 [海珠区-赤岗]石榴岗路17号 21058.0 1996 23.086848 113.339585
884 北京 华阳家园 [朝阳区-团结湖]姚家园路 68591.0 2003 39.931654 116.474879
335 北京 远洋傲北 [昌平区-北七家]立汤路36号 40920.0 2011 40.144035 116.416105
2391 上海 三杨新村一街坊 [浦东新区-三林]海阳路215弄 61022.0 2013 31.155239 121.489228
1843 上海 莘城苑 [闵行区-莘庄]疏影路1111弄 38664.0 2004 31.116479 121.360167
5640 深圳 赛格景苑 [福田区-景田北]景田北路 73620.0 1998 22.554100 114.037624
3874 广州 晓园新村 [海珠区- 昌岗]江南大道南3号 33617.0 2005 23.086375 113.281508

4.2 按城市分组的房价统计数据

  • count: 数据记录数
  • mean: 平均值
  • std: 标准差
  • min: 最小值
  • 25%: 1/4 位数
  • 50%: 中位数
  • 75%: 3/4 位数
  • max: 最大值
1
df.groupby('city')['price'].describe()

count mean std min 25% 50% 75% max
city
上海 1500.0 59444.306000 29571.455036 5119.0 39905.75 54900.5 73924.50 343089.0
北京 1500.0 65946.664000 32268.489449 6787.0 42127.00 60733.0 84843.00 204745.0
广州 1500.0 33767.053333 22430.584341 5530.0 17548.25 28889.0 43684.75 277682.0
深圳 1500.0 58886.890000 36035.745033 5510.0 37739.50 53107.0 72797.00 330579.0

4.3 数据可视化

按城市分组,显示价格分布小提琴图箱线图

1
2
3
4
5
6
7
8
9
10
11
import seaborn as sb
import matplotlib.pyplot as plt
# %matplotlib inline

plt.figure(figsize=(12, 6))
plt.subplot(1,2, 1)
ax1 = sb.violinplot(data = df, x = 'city', y = 'price', inner = 'quartile')
plt.subplot(1,2, 2)
sb.boxplot(data = df, x = 'city', y = 'price')
plt.ylim(ax1.get_ylim())
plt.show()

青笔 wechat
我是一条小青蛇 我有很多小秘密