Python 爬虫--上海链家二手房数据爬取与可视化分析
一、选题的背景
本次项目选择了中国的一线城市——上海市,通过了解上海市二手房的情况,可以帮助人们在购房、出租等方面做出更明智的决策。可以帮助人们了解上海市经济的发展趋势。随着互联网的发展,越来越多的房地产信息通过网络发布,使用爬虫技术可以方便地收集和分析这些信息。而本次项目选择的数据来源是链家。链家是一家著名的房地产经纪公司,在上海市有着广泛的房地产业务。通过爬取上海市链家发布的二手房信息,可以获得丰富的数据,为分析提供参考。
二、主题式网络爬虫设计方案
1.主题式网络爬虫名称
上海市链家二手房数据爬虫
2.主题式网络爬虫爬取的内容与数据特征分析
上海市链家二手房数据爬虫主要爬取了上海市链家二手房的所在行政区、小区名称、每平方米价格、整套房总价、建筑面积、所在楼层、房屋朝向、关注人数、所在区域、装修类型、房屋亮点和面积区间。
3.主题式网络爬虫设计方案概述(包括实现思路与技术难点)
思路:查看网页的结构,定位目标数据的位置,爬取数据,将数据进行清洗,最后将清洗后的数据进行可视化。
难点:如何应对网站的反爬虫机制,如何爬取多个页面的数据,请求异常的处理
三、主题页面的结构特征分析
1.主题页面的结构与特征分析
目标内容界面:
2.Htmls 页面解析
3.节点(标签)查找方法与遍历方法
查找方法:使用lxml库中的xpath函数查找
四、网络爬虫程序设计
1.数据爬取与采集
1 import csv 2 import os 3 import random 4 import time 5 import pandas as pd 6 from lxml import etree 7 import requests 8 from retry.api import retry_call 9 10 11 # 异常重试函数,防止网络不稳定导致抓取数据中断等。 12 def retry_request(crawl_url, req_type='get', retry_times=6): 13 while retry_times: 14 retry_times -= 1 15 try: 16 # 请求头 User-Agent 17 headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"} 18 resp = retry_call(getattr(requests, req_type), fargs=[crawl_url], 19 fkwargs={"timeout": 60, "headers": headers}, delay=0, tries=1) 20 # 判断请求状态码以及响应是否为空,为空则继续,反之则返回请求体 21 if resp.status_code == 200 and resp.content: 22 return resp.text 23 except Exception as e: 24 print(e) 25 # 重试次数用完,则直接返回异常数据 26 if not retry_times: 27 return '<html></html>' 28 # 随机休眠 1-3s 29 time.sleep(random.randint(1, 3)) 30 31 32 # 抓取翻页数据 33 def get_pages(url): 34 page_html = retry_request(url) 35 html_obj = etree.HTML(page_html) 36 # xpath获取每个房屋部分 37 ports = html_obj.xpath('//*[@class="info clear"]') 38 lis = [] 39 for port in ports: 40 # 解析详情url 41 try: 42 url = port.xpath('.//*[@class="title"]/a[1]/@href')[0] 43 except: 44 url = '' 45 # 解析总价 46 try: 47 total_price = port.xpath('.//*[@class="totalPrice totalPrice2"]//text()') 48 total_price = ''.join(total_price).strip().replace('\n', '') 49 except: 50 total_price = '' 51 # 解析单价 52 try: 53 unit_price = port.xpath('.//*[@class="unitPrice"]//text()') 54 unit_price = ''.join(unit_price) 55 except: 56 unit_price = '' 57 # 将当前房屋数据加到house列表里 58 list_data = [url, unit_price, total_price] 59 lis.append(list_data) 60 return lis 61 62 63 # 抓取房屋详情信息 64 def get_detail(infos, city_name): 65 de_url = infos[0] 66 page_html = retry_request(de_url) 67 html_obj = etree.HTML(page_html) 68 # 解析所在行政区 69 try: 70 xzu = html_obj.xpath('//*[@class="areaName"]//span[@class="info"]//text()') 71 xzu = ''.join(xzu) 72 xzq = xzu.split('\xa0')[0] 73 except Exception as e: 74 xzq = '' 75 # 解析所在地址 76 try: 77 szqy = html_obj.xpath('//*[@class="areaName"]//span[@class="info"]//text()') 78 szqy = ''.join(szqy) 79 szqy = szqy.split('\xa0')[1:] 80 szqy = ''.join(szqy).strip() 81 except Exception as e: 82 szqy = '' 83 # 解析小区名称 84 try: 85 xqmc = html_obj.xpath('//*[@class="communityName"]/a[1]//text()') 86 xqmc = ''.join(xqmc).strip() 87 except Exception as e: 88 xqmc = '' 89 # 解析所在楼层 90 try: 91 szlc = html_obj.xpath('//*[@class="base"]//ul/li[2]//text()') 92 szlc = ''.join(szlc).replace('所在楼层', '').strip() 93 except Exception as e: 94 szlc = '' 95 # 解析房屋面积 96 try: 97 house_area = html_obj.xpath('//*[@class="area"]/div[@class="mainInfo"]//text()') 98 house_area = ''.join(house_area).strip() 99 except Exception as e: 100 house_area = '' 101 # 解析房屋朝向 102 try: 103 fwcx = html_obj.xpath('//span[text()="房屋朝向"]/parent::*/text()') 104 fwcx = ''.join(fwcx) 105 fwcx = fwcx.replace('房屋朝向', '').strip() 106 except Exception as e: 107 fwcx = '' 108 # 解析关注人数 109 try: 110 gzrs = html_obj.xpath('//*[@id="favCount"]/text()')[0] 111 except Exception as e: 112 gzrs = '' 113 # 解析房屋核心卖点 114 try: 115 house_good = ''.join(html_obj.xpath('//div[text()="核心卖点"]/following-sibling::div[1]//text()')) 116 house_good = house_good.strip() 117 except Exception as e: 118 house_good = '' 119 # 解析装修状态 120 try: 121 zx_status = ''.join(html_obj.xpath('//span[text()="装修情况"]/parent::*/text()')) 122 zx_status = zx_status.strip() 123 except Exception as e: 124 zx_status = '' 125 # 获取房屋单价 126 unit_price = infos[1] 127 # 获取房屋总价 128 total_price = infos[2]
2.对数据进行清洗和处理
1 # 清洗数据 2 def clean_data(): 3 # 将 每平方米价格 清洗成整数 4 df['每平方米价格'] = df.每平方米价格.str.replace('[^\d]', '',regex=True) 5 df['每平方米价格'] = df.每平方米价格.astype('int') 6 # 将 每平方米价格 清洗成小数,并生成价格区间列 7 df['建筑面积'] = df.建筑面积.str.replace('[^\d\.]', '',regex=True) 8 df['建筑面积'] = df.建筑面积.astype('float') 9 df['面积区间'] = df.建筑面积.apply(lambda x: '20㎡以下' if x <= 20 else '20.1-45㎡' if x <= 45 10 else '45.1-80㎡' if x <= 80 11 else '80.1-120㎡' if x <= 120 else '120.1㎡以上') 12 # 将 整套房总价 清洗成整数 13 df['整套房总价'] = df.整套房总价.str.replace('[^\d]', '',regex=True) 14 df['整套房总价'] = df.整套房总价.astype('int') 15 16 # 清洗 关注人数 并转换数据类型为整数类型 17 df['关注人数'] = df.关注人数.astype('str') 18 df['关注人数'] = df.关注人数.str.replace('[^\d]', '',regex=True) 19 df['关注人数'] = df.关注人数.astype('int')20 21 return df 22 23 # 接收清洗的结果集 24 clean_df = clean_data()
3.文本分析:wordcloud 的分词可视化
1 # 提取 房屋亮点 列,生成词云图 2 useful_df = clean_df[clean_df.房屋亮点.notnull()] 3 sentence_str = ''.join(list(useful_df.房屋亮点)) 4 # 构造词字典,统计个各个词出现的总次数 5 data_dict = {} 6 # jieba分词 7 fc_list = jieba.lcut(sentence_str) 8 # 遍历每个词并统计 9 for key in fc_list: 10 # 长度等于1的字,不做统计 11 if len(key) < 2: 12 continue 13 index = data_dict.get(key) 14 if index: 15 value = index + 1 16 else: 17 value = 1 18 data_dict[key] = value 19 # 加载词云背景图 20 pic_read = Image.open('bgc_pic.webp') 21 # 使用numpy获取图片外形 22 pic_mask = np.array(pic_read) 23 # 构造词云 24 word_cloud = wordcloud.WordCloud( 25 background_color='skyblue', # 设定图背景色 26 font_path=r'C:\Windows\Fonts\STHUPO.ttf',# 加载图字体 27 width=800,# 设定图宽度 28 height=800, # 设定图高度 29 mask=pic_mask, # 设定图外形 30 ) 31 # 将词填充到图上 32 word_cloud.fit_words(data_dict) 33 plt.imshow(word_cloud) 34 # 关闭图坐标轴 35 plt.axis('off') 36 plt.show()
通过房屋亮点词云图可以了解到房主会着重介绍房屋的楼层与采光问题。其次是房屋的户型,位置,视野,税费,小区,精装修等。
同时,通过房主的介绍也能侧面了解到如今购房人购房时比较在意的点。
4.数据分析与可视化
(1)绘制各行政区房屋均价
1 clean_df_city = clean_df.groupby('行政区').mean().sort_values(by='每平方米价格', ascending=False) 2 3 # 设置图片大小 4 plt.figure(figsize=(14, 8)) 5 # 设置网格线 6 plt.grid(color='steelblue', linestyle='-.') 7 # 绘制饼图 8 plt.bar(x=clean_df_city.index, height=[round(i, 2) for i in list(clean_df_city.每平方米价格)], 9 align='center', color='skyblue') 10 # 加上每个值的数据标签 11 for a, b in enumerate([round(i, 2) for i in list(clean_df_city.每平方米价格)]): 12 plt.text(a, b / 1, "%d元" % round(b, 2), ha='center', fontsize=12, color='r') 13 # 设置x和y以及title标签 14 plt.xlabel('所在地区', labelpad=10, size=15) 15 plt.ylabel('房屋均价', labelpad=15, size=15) 16 plt.title('各行政区房屋均价展示', pad=20, size=20) 17 plt.show()
通过该图可以了解到房价最高的行政区是黄浦区,其房屋均价远高其他行政区,也能体现上海市的经济中心应该是在黄埔区,猜测其经济水平与发展应高于其他区。
(2)绘制各行政区房屋数量占比图
1 clean_df_xzfl = clean_df.groupby('面积区间').count() 2 plt.figure(figsize=(7, 5)) 3 # 构造饼图生成器 4 wedges, texts, autotexts = plt.pie(clean_df_xzfl.小区名称, 5 autopct="%4.2f%%", 6 textprops=dict(color="w")) 7 # 为饼图添加图例 8 plt.legend(wedges, 9 clean_df_xzfl.index, 10 fontsize=12, 11 title="房屋面积区间", 12 loc="center left", 13 bbox_to_anchor=(0.91, 0, 0.3, 1)) 14 # 绘制饼图形状大小 15 plt.setp(autotexts, size=15, weight="bold") 16 plt.setp(texts, size=15) 17 plt.title("各面积区间房屋数量占比图", size=20) 18 plt.show()
通过该图可以直观的了解到上海市的出售的二手房大多为中小型,其中45.1-80㎡的房屋居多,其次是80.1-120㎡的房屋,两者加起来占比达82.39%。
(3)构造 各行政区房屋数量分布地图
1 ct_map = Map( 2 init_opts=opts.InitOpts(width="1200px", height='600px') 3 ) 4 # 添加地图title并选定地区为上海市 5 ct_map.add('行政区房屋数量', data_pair=data_list, maptype='上海', is_map_symbol_show=False) 6 ct_map.set_series_opts(label_opts=opts.LabelOpts(is_show=True)) 7 ct_map.set_global_opts( 8 title_opts=opts.TitleOpts(title='各行政区房屋数量分布图', subtitle='数据来源:链家上海二手房'), 9 visualmap_opts=opts.VisualMapOpts( 10 # 设定各区间值,该值区间表示的是各个行政区拥有的房屋数量区间 11 pieces=[ 12 {"max": 1000, "min": 450, "label": ">450", "color": "#0000CD"}, 13 {"max": 450, "min": 350, "label": "350-450", "color": "#0000FF"}, 14 {"max": 350, "min": 250, "label": "250-350", "color": "#00BFFF"}, 15 {"max": 250, "min": 180, "label": "180-250", "color": "#00BFFF"}, 16 {"max": 180, "min": 120, "label": "120-180", "color": "#87CEFA"}, 17 {"max": 120, "min": 60, "label": "60-120", "color": "#87CEEB"}, 18 {"max": 60, "min": 30, "label": "30-60", "color": "#ADD8E6"}, 19 {"max": 30, "min": 10, "label": "10-30", "color": "#B0E0E6"}, 20 {"max": 10, "min": 1, "label": "1-10", "color": "#F0F8FF"}, 21 ], 22 is_piecewise=True 23 ) 24 )
通过该图可以看出身为上海市市*的黄浦区出售的二手房数量并不多,和其相连的浦东新区形成了强烈的对比。其原因之一也是因为浦东新区的占地面积较大。
5.数据持久化
(1)爬取数据的持久化
1 # pandas读取csv文件并保存为Excel文件 2 def csv2excel(city_name): 3 df = pd.read_csv(f'{city_name}链家二手房.csv', header=None, names=['行政区', '小区名称', '每平方米价格', 4 '整套房总价', '建筑面积', '所在楼层', '房屋朝向', 5
推荐阅读