Python数据可视化及网页抓取项目实战

一年的计划在春天。到2020年春天,这种流行病可能会改变许多人的计划。比如,三四月份是传统企业招聘的高峰期之一。许多英俊的年轻人去拜访岳母,劝他们在新年买房。职场和房地产市场有“三金四银”之说。然而,这是真的吗?

最近我又学了Python(为什么是又呢?因为我学的时候忘了,哈哈),为什么不简单地验证一下呢?毕竟,数据不会说谎。

主要流程:

  1. 以房地产市场为分析对象,与公司目前的业务有一定的关系。

  2. 从武汉市住房保障和房屋管理局网站获取新建商品房公开交易统计数据。

  3. 阅读数据并将其可视化,并对图表进行简要分析,得出初步结论。

首先给大家展示一下最终的可视化数据图:

商品住宅成交统计数据(武汉

1、获取数据

首先,使用“为人类设计的HTTP库”请求从房管局网站获取包含公共交易统计数据的HTML页面。数据分为日统计和月统计。然后利用HTML和XML处理库lxml对HTML页面内容进行解析。分析之后,通过适当的XPath提取所需的数据。

一开始,我的想法是阅读每日数据,并分别计算每个月的数据。在爬行之后,我发现每月的统计数据就在目录页面旁边(笑声和泪水)。不过,Jpg每月公布的数据只到2019年11月,这还不足以支撑整个两年。因此,2019年12月的数据是根据每日统计数据(发布至2020年1月23日)计算得出的。正如所料,生命不可能徒劳无功:)

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) + ' 页,' + '开始获取月度数据..
')
        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('
', '').replace('
', '').replace('	', '')
        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 + '
')
    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

2、保存数据

获取数据后,需要保存,以便后续的数据处理和增量更新。在这里,我们使用mongodb(一个接近python的文档数据库)来存储数据。

踩坑:对于 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('已删除:月度成交数不符合期望。
')
        else:
            print('忽略:月度成交数不符合期望。
')
    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) + '
' + 'result:' + str(result) + '
')
        else:
            result = col_monthly.insert_one(item)
            print('写入数据成功:' + str(item) + '
' + 'result:' + str(result) + '

由于在实际应用中提取数据的限制不够严格,一些脏数据是在早期编写的,所以除了正常的插入和更新外,还有一个清理脏数据的try异常。

3、读取数据

数据采集保存完成后,使用mongodb图形用户界面工具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

可见,在读取月度数据的方法中,要检查数据是否完整,如果数据缺失,则要从日常数据中读取并计算相关逻辑。

4、数据可视化

因为这只是一个简单查看数据总体趋势的练习,所以您不想绘制稍微复杂的图表。使用图表库Matplotlib绘制简单的统计图表:

import matplotlib.pyplot as plt
import html_spider
import db_operator

def generate_plot(all_monthly_datas):
    """生成统计图表"""
    # 处理汉字未正常显示问题
    # 还需要手动下载 SimHei.ttf 字体并放到 /venv/lib/python3.7/site-packages/matplotlib/mpl-data/fonts 目录下)
    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())

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

5、简要分析

结合图表中近两年的数据曲线,可以直观地看出,近两年来,每年上半年都在上升。随着丈母娘的压力逐渐降低到年中应该买的,买不到的也不急,数据会回落,然后随着下半年,另一批准备看丈母娘的补品又会开始上涨。具体来说,2月份是全年最低的(估计是因为寒假),然后稳步上升到8月份左右。9月份会回落,然后再回升(除2018年7月外,还有明显回落,所以我们需要检查当时政策调控贷款等方面是否有调整影响)。

至于3、4月份,都是上涨的区域,但全年的高峰实际上出现在年底和年中。可以看出,从复苏的角度来看,“金山银丝”的观点有一定的依据,但不一定从高峰期的角度来看。

最后,我没有得出一个更为肯定的结论:是真是假。可能有很多事情没有一个明确的答案:)

最后

2019年仍明显高于2018年。不要太担心房地产市场的下跌,因为担心是没有用的~

原文地址:https://www.cnblogs.com/java2018/p/12530793.html