Python 网页抓取与数据可视化练习:‘金三银四’ 是真的吗?

年之计在于春,2020 的春天因为疫情可能改变了许多人的计划,如三四月份是企业传统招聘高峰期之一,再有许多帅小伙过年拜见了丈母娘催促着得买房等,职场与楼市素有 ‘金三银四’ 的说法,然而,这是真的吗?

最近又学习了一下 Python(为什么是又?因为学了就忘..),想到何不简单验证一下,毕竟数据不会撒谎。

主要流程:

  1. 选取楼市情况作为分析对象,与目前公司业务有点相关性。
  2. 从 武汉市住房保障和房屋管理局 网站获取公开的新建商品房成交统计数据。
  3. 读取数据并可视化,结合图表简要分析得出初步结论。

先贴最终生成的可视化数据图:

Step 1:获取数据

先使用 ‘为人类设计的 HTTP 库’ - requests 从房管局网站上获取包含公开成交统计数据的 HTML 页面,数据分为按日统计发布的及按月统计发布的。然后使用 HTML 与 XML 处理库 lxml 解析 HTML 页面内容,分析后通过合适的 xpath 提取所需数据。

最开始我的想法是读取每日数据再分别计算出每个月的数据,爬完后发现目录页下面紧挨着的就是按月统计数据(笑哭.jpg ,但是按月的数据只发布到了2019年11月,连整两年都凑不足可不行,于是结合按日统计数据(发布到了2020年01月23日)计算出了2019年12月的数据,果然人生没有白走的路:)

import requests
import lxml.html
import html
import time

import db_operator

def get_all_monthly_datas():
    """按月获取所有成交数据"""
    # 索引页(商品住房销售月度成交统计)
    index_url = 'http://fgj.wuhan.gov.cn/spzfxysydjcjb/index.jhtml'
    max_page = get_max_page(index_url)
    if max_page > 0:
        print('共 ' + str(max_page) + ' 页,' + '开始获取月度数据..\n')
        for index in range(1, max_page + 1):
            if index >= 2:
                index_url = 'http://fgj.wuhan.gov.cn/spzfxysydjcjb/index_' + str(index) + '.jhtml'
            detail_urls = get_detail_urls(index, index_url)
            for detail_url in detail_urls:
                print('正在获取月度统计详情:' + detail_url)
                monthly_detail_html_str = request_html(detail_url)
                if monthly_detail_html_str:
                    find_and_insert_monthly_datas(monthly_detail_html_str)
    else:
        print('总页数为0。')


def request_html(target_url):
    """请求指定 url 页面"""
    headers = {
        'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Mobile Safari/537.36',
    }
    html_response = requests.get(target_url, headers=headers)
    html_bytes = html_response.content
    html_str = html_bytes.decode()
    return html_str


def get_max_page(index_url) -> int:
    """从索引页中获取总页数"""
    print('获取总页数中..')
    index_html_str = request_html(index_url)
    selector = lxml.html.fromstring(index_html_str)
    max_page_xpath = '//div[@class="whj_padding whj_color pages"]/text()'
    result = selector.xpath(max_page_xpath)
    if result and len(result) > 0:
        result = result[0]
        index_str = result.replace('\r', '').replace('\n', '').replace('\t', '')
        max_page = index_str.split('\xa0')[0]
        max_page = max_page.split('/')[1]
        return int(max_page)
    return 0


def get_detail_urls(index, index_url):
    """获取统计数据详情页 url 列表"""
    print('正在获取统计列表页面数据:' + index_url + '\n')
    index_html_str = request_html(index_url)
    selector = lxml.html.fromstring(index_html_str)
    # 提取 url 列表。
    # 疑问:这里使用 '//div[@class="fr hers"]/ul/li/a/@href' 期望应该能提取到更准确的数据,但是结果为空
    detail_urls_xpath = '//div/ul/li/a/@href'
    detail_urls = selector.xpath(detail_urls_xpath)
    return detail_urls


复制代码

Stp 2:保存数据

获取到数据后需要保存下来,以便后续的数据处理与增量更新等。这里使用与 Python 相亲相爱的文档型数据库 MongoDB 存储数据。

踩坑:对于 macOS 系统网上许多 MongoDB 安装说明已经失效,需要参考 mongodb/homebrew-brew 引导安装。

启动服务后就可以写入数据:

from pymongo import MongoClient
from pymongo import collection
from pymongo import database

client: MongoClient = MongoClient()
db_name: str = 'housing_deal_data'
col_daily_name: str = 'wuhan_daily'
col_monthly_name: str = 'wuhan_monthly'
database: database.Database = client[db_name]
col_daily: collection = database[col_daily_name]
col_monthly: collection = database[col_monthly_name]


def insert_monthly_data(year_month, monthly_commercial_house):
    """写入月度统计数据"""
    query = {'year_month': year_month}
    existed_row = col_monthly.find_one(query)
    try:
        monthly_commercial_house_value = int(monthly_commercial_house)
    except:
        if existed_row:
            print('月度数据已存在 =>')
            col_monthly.delete_one(query)
            print('已删除:月度成交数不符合期望。\n')
        else:
            print('忽略:月度成交数不符合期望。\n')
    else:
        print(str({year_month: monthly_commercial_house_value}))
        item = {'year_month': year_month,
                'commercial_house': monthly_commercial_house_value,}
        if existed_row:
            print('月度数据已存在 =>')
            new_values = {'$set': item}
            result = col_monthly.update_one(query, new_values)
            print('更新数据成功:' + str(item) + '\n' + 'result:' + str(result) + '\n')
        else:
            result = col_monthly.insert_one(item)
            print('写入数据成功:' + str(item) + '\n' + 'result:' + str(result) + '\n')


复制代码

由于在实践过程中提取数据限制不够严格导致前期写入了一些脏数据,所以这里除了正常的 insert 、 update 之外,还有一个 try-except 用来清理脏数据。

Step 3:读取数据

获取并保存数据执行完成后,使用 MongoDB GUI 工具 Robo 3T 查看,总体确认数据完整基本符合期望。

接下来从数据库读取数据:

def read_all_monthly_datas():
    """从数据库读取所有月度统计数据"""
    return {"2018年": read_monthly_datas('2018'),
            "2019年": read_monthly_datas('2019'),}


def read_monthly_datas(year: str) -> list:
    """从数据库读取指定年份的月度统计数据"""
    query = {'year_month': {'$regex': '^' + year}}
    result = col_monthly.find(query).limit(12).sort('year_month')

    monthly_datas = {}
    for data in result:
        year_month = data['year_month']
        commercial_house = data['commercial_house']
        if commercial_house > 0:
            month_key = year_month.split('-')[1]
            monthly_datas[month_key] = data['commercial_house']

    # 如果读取结果小于 12,即有月度数据缺失,则尝试读取每日数据并计算出该月统计数据
    if len(monthly_datas) < 12:
        for month in range(1, 13):
            month_key = "{:0>2d}".format(month)
            if month_key not in monthly_datas.keys():
                print('{}年{}月 数据缺失..'.format(year, month_key))
                commercial_house = get_month_data_from_daily_datas(year, month_key)
                if commercial_house > 0:
                    monthly_datas[month_key] = commercial_house
    return monthly_datas


def get_month_data_from_daily_datas(year: str, month: str) -> int:
    """从每日数据中计算月度统计数据"""
    print('从每日数据中获取 {}年{}月 数据中..'.format(year, month))
    query = {'year_month_day': {'$regex': '^({}-{})'.format(year, month)}}
    result = col_daily.find(query).limit(31)
    sum = 0
    for daily_data in result:
        daily_num = daily_data['commercial_house']
        sum += daily_num
    print('{}年{}月数据:{}'.format(year, month, sum))
    return sum
 
 
复制代码

可以看到读取月度数据方法中有校验数据是否完整以及数据缺失则从每日数据中读取计算相关的逻辑。

Step 4:数据可视化

由于只是练习简单查看数据总体趋势,所以没有想要绘制稍复杂的图表,使用图表库 matplotlib 绘制简单统计图:

import matplotlib.pyplot as plt
import html_spider
import db_operator

def generate_plot(all_monthly_datas):
    """生成统计图表"""
    # 处理汉字未正常显示问题
    plt.rcParams['font.sans-serif'] = ['SimHei']
    plt.rcParams['font.family'] = 'sans-serif'

    # 生成统计图表
    fig, ax = plt.subplots()
    plt.title(u"商品住宅成交统计数据(武汉)", fontsize=20)
    plt.ylabel(u"成交量", fontsize=14)
    plt.xlabel(u"月份", fontsize=14)
    for year, monthly_datas in all_monthly_datas.items():
        ax.plot(list(monthly_datas.keys()), list(monthly_datas.values()), label=year)
    ax.legend()
    plt.show()


# 爬取网页数据(并写入数据库)
# html_spider.get_all_daily_datas()
html_spider.get_all_monthly_datas()
# 读取数据,生成统计图表
generate_plot(db_operator.read_all_monthly_datas())


复制代码

执行完毕绘制生成的就是开始贴出的数据图。

源码加群:850591259

Step 5:简要分析

结合图表中过去两年的数据曲线可以直观的看出,近两年每年都是上半年上涨,随着丈母娘压力逐步降低到年中该买的买了,没买的就是不着急的了,数据会回落然后随着下半年又一拨准备见丈母娘的补充又开始上升。具体来看,2 月份全年最低(猜测是因为过年放寒假),之后稳步上升至 8 月份左右在 9 月份会回落后再次上涨(除了 2018年7月 份也有个明显回落,得查一下是不是当时有政策调控贷款等方面的调整影响)。

针对看3、4月份,都属于上升区,但全年的高峰其实分别出现在年末与年中。由此可见如果从回暖角度看 ‘金山银四’ 的说法有一定依据,但如果从高峰期角度看则不尽然。

最终没有得出一个比较肯定的 YES or NO 的结论,可能很多事的确是没有明确答案的 :)

one more thing

2019 年整体还是明显高于 2018 年的,不用太担心楼市走低(担心也没啥用,狗头.jpg

原本这篇练习的标题应该是 - ‘金九银十’ 是真的吗?硬是被自己拖成了 ‘金三银四’,哎,拖延症要不得。

发布了2 篇原创文章 · 获赞 0 · 访问量 908

猜你喜欢

转载自blog.csdn.net/weixin_43881394/article/details/105050977
今日推荐