引言
昨天在老家,发布了一篇《python 自动抓取分析房价数据——安居客版》。在文末,第6小节提供了完整代码,可以在 python3 环境,通过命令行传入参数 cookie 自动抓取房价数据。今天回到深圳,才想到,这段脚本只能抓取西双版纳的房价数据,如果读者不自己修改,那么就无法抓取其他城市的房价数据。于是,决定“好事做到底,送佛送到西”,将脚本加以修改,以北上广深为例,提供灵活抓取分析其他城市房价的完整代码。
1. 完整 python 脚本
在上一篇的脚本基础上,稍加修改,将以下代码保存到文件 crawl_anjuke.py
中。
#!/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 statisticsn-------'.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 抓取北京房价数据
代码语言:javascript复制python crawl_anjuke.py --city beijing --limit 50 --cookie "sessid=5AACB464..."
3.2 抓取上海房价数据
代码语言:javascript复制python crawl_anjuke.py --city shanghai --limit 50 --cookie "sessid=5AACB464..."
3.3 抓取广州房价数据
代码语言:javascript复制python crawl_anjuke.py --city guangzhou --limit 50 --cookie "sessid=5AACB464..."
3.4 抓取深圳房价数据
代码语言:javascript复制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
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)
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
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: 最大值
df.groupby('city')['price'].describe()
.dataframe tbody tr th:only-of-type { vertical-align: middle; } .dataframe tbody tr th { vertical-align: top; } .dataframe thead th { text-align: right; }
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 数据可视化
按城市分组,显示价格分布小提琴图
和箱线图
。
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()