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

2019-10-23 17:24:35 浏览数 (1)

引言

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

1. 完整 python 脚本

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

代码语言:javascript复制
#!/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
代码语言:javascript复制
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: 最大值
代码语言:javascript复制
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 数据可视化

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

代码语言:javascript复制
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()

0 人点赞