# 1 前言
这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。
为了大家能够顺利以及最少的精力通过毕设,学长分享优质毕业设计项目,今天要分享的是
基于大数据上海租房数据爬取与分析可视化
学长这里给一个题目综合评分(每项满分5分)
基于Python的上海自如租房大数据聚类分析与可视化,爬取自如所有上海房源,进行k-means聚类分析,将房源划分为不同等级。并对数据进行可视化分析。
聚类后的dataframe结果
饼图
3D柱状图
此次侧重XPath的使用和反爬虫小技巧。XPath是用路径表达式在XML文档中选取节点,这里也同样适用于HTML文档的搜索。
打开上海链家网的租房页面,选择筛选条件,示例如下。确认后地址栏的URL会根据筛选条件而发生变化。(当然如果没有想好想要住的区域,地铁线,租金,面积,朝向,户型也没有关系,可以直接爬取全部的上海房源数据。)
共计28页房源信息,点击切换下一页,观察URL会发现链家网是静态的网页,页面切换通过在URL中加入pg{i}参数实现。因此我们只要能爬取一页的信息,就可以通过参数循环来爬取所有页面。
按F12打开开发者工具,在页面中选择一个元素以进行检查。可以看到右侧的房源列表模块和左边的房源信息是一一对应的。左侧的每一条房源信息都等价于右侧的class属性为content__list
–item的一个div图层。因此我们只需要观察了解第一个房源信息即可。
继续展开这个div图层,会发现我们需要的信息基本都被包含在属性为content__list–item–
main的子div图层中,尤其是其中几个class为title,description,price的元素里。
以属性为content__list–item–
title的paragraph为例,其中包含了租赁方式,街区,房屋朝向等信息。我们可以用XPath来匹配和定位到这个段落,取出里面的文本。常用的匹配规则为
/ 代表选取直接子节点,// 代表选择所有子孙节点,. 代表选取当前节点,… 代表选取当前节点的父节点,@
则是加了属性的限定,选取匹配属性的特定节点。下面是租赁方式(整租/合租)的匹配方式。
LeaseMethod = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--title"]/a/text()')[0].strip().split(' ')[0].split('·')[0]
以此类推,我们可以轻易的取出街区,租赁方式,朝向,每月租金,行政区,板块,房屋面积,格局和发布时长等信息。
网站的反爬措施有很多,比如检测访问请求头。且如果一个header短期频繁发送请求,也很容易被识别。这种情况下可以通过添加多个请求头,每次随机选取一个header,伪装成浏览器访问;且设置time
sleep,每次发送请求随机间隔一段时间来防止出现error403/404。还有一些方法如添加Referer,host,代理IP等,这里不做过多阐述,感兴趣的小伙伴欢迎自行探索。
完整代码和最终结果如下。可以看到第一步在链家主页搜索时显示的828条记录已经全部获取到数据框中。可以进行下一步的分析啦!
import requests
from lxml import etree
import random
import time
import pandas as pd
#伪装请求头
user_agents = [
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36 OPR/26.0.1656.60',
'Opera/8.0 (Windows NT 5.1; U; en)',
'Mozilla/5.0 (Windows NT 5.1; U; en; rv:1.8.1) Gecko/20061208 Firefox/2.0.0 Opera 9.50',
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; en) Opera 9.50',
'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0',
'Mozilla/5.0 (X11; U; Linux x86_64; zh-CN; rv:1.9.2.10) Gecko/20100922 Ubuntu/10.10 (maverick) Firefox/3.6.10',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.133 ',
'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14",
"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)",
'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11',
'Opera/9.25 (Windows NT 5.1; U; en)',
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)',
'Mozilla/5.0 (compatible; Konqueror/3.5; Linux) KHTML/3.5.5 (like Gecko) (Kubuntu)',
'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.12) Gecko/20070731 Ubuntu/dapper-security Firefox/1.5.0.12',
'Lynx/2.8.5rel.1 libwww-FM/2.14 SSL-MM/1.4.1 GNUTLS/1.2.9',
"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.7 (KHTML, like Gecko) Ubuntu/11.04 Chromium/16.0.912.77 Chrome/16.0.912.77 Safari/535.7",
"Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:10.0) Gecko/20100101 Firefox/10.0 "
]
def getHeaders():
user_agent = user_agents[random.randint(0, len(user_agents)-1)]
headers = {
'User-Agent': user_agent
}
return headers
#对一个URL发送请求,解析结果,获取所需数据
def get_data(url):
#反爬虫策略1:随机取headers
response = requests.get(url, headers=getHeaders(), stream=True)
tree = etree.HTML(response.text)
# 定位到content__list
li_list = tree.xpath('//div[@class="content w1150"]/div[@class="content__article"]/div[@class="content__list"]/div')
# all_house_list = []
for li in li_list:
#下面是两种定位方式,都可
# Nbhood = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--title"]/a[@class="twoline"]/text()')[0].strip().split(' ')[0].split('·')[1]
Nbhood = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--title"]/a/text()')[0].strip().split(' ')[0].split('·')[1]
LeaseMethod = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--title"]/a/text()')[0].strip().split(' ')[0].split('·')[0]
HouseOrientation = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--title"]/a/text()')[0].strip().split(' ')[2]
Rent = li.xpath('.//div[@class="content__list--item--main"]/span[@class="content__list--item-price"]/em/text()')[0]
District = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--des"]/a/text()')[0]
Location = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--des"]/a/text()')[1]
Size = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--des"]/text()')[4].strip()
HouseType = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--des"]/text()')[6].strip()
releaseTime = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--brand oneline"]/span[@class="content__list--item--time oneline"]/text()')[0]
Link = li.xpath('.//div[@class="content__list--item--main"]/p[@class="content__list--item--title"]/a[@class="twoline"]//@href')[0]
all_house_list.append((Nbhood,LeaseMethod,HouseOrientation,Rent,District,Location,Size,HouseType,releaseTime))
return all_house_list
#循环爬取所需租房信息
pages = ['https://sh.lianjia.com/ditiezufang/li143685063/pg{}rt200600000001l1l0ra1ra2ra0rp5rp6/'.format(x) for x in range(1,29)]
all_house_list = []
count = 0
for page in pages:
a = get_data(page)
#反爬虫策略2:每次爬取随机间隔3-10s
time.sleep(random.randint(3,10))
count=count+1
print ('the '+str(count)+' page is sucessful')
name = ["街区", "租赁方式", "朝向", "每月租金", "行政区","板块","房屋面积","格局","发布时长"]
page_data = pd.DataFrame( columns= name,data=all_house_list)
基本原理
k-Means算法是一种使用最普遍的聚类算法,它是一种无监督学习算法,目的是将相似的对象归到同一个簇中。簇内的对象越相似,聚类的效果就越好。该算法不适合处理离散型属性,但对于连续型属性具有较好的聚类效果。
聚类效果判定标准
使各个样本点与所在簇的质心的误差平方和达到最小,这是评价k-means算法最后聚类效果的评价标准。
算法实现步骤
1)选定k值
2)创建k个点作为k个簇的起始质心。
3)分别计算剩下的元素到k个簇的质心的距离,将这些元素分别划归到距离最小的簇。
4)根据聚类结果,重新计算k个簇各自的新的质心,即取簇中全部元素各自维度下的算术平均值。
5)将全部元素按照新的质心重新聚类。
6)重复第5步,直到聚类结果不再变化。
7)最后,输出聚类结果。
算法缺点
虽然K-Means算法原理简单,但是有自身的缺陷:
1)聚类的簇数k值需在聚类前给出,但在很多时候中k值的选定是十分难以估计的,很多情况我们聚类前并不清楚给出的数据集应当分成多少类才最恰当。
2)k-means需要人为地确定初始质心,不一样的初始质心可能会得出差别很大的聚类结果,无法保证k-means算法收敛于全局最优解。
3)对离群点敏感。
4)结果不稳定(受输入顺序影响)。
5)时间复杂度高O(nkt),其中n是对象总数,k是簇数,t是迭代次数。
K值的选定说明
根据聚类原则:组内差距要小,组间差距要大。我们先算出不同k值下各个SSE(Sum of
squared
errors)值,然后绘制出折线图来比较,从中选定最优解。从图中,我们可以看出k值到达5以后,SSE变化趋于平缓,所以我们选定5作为k值。
初始的K个质心选定说明
初始的k个质心选定是采用的随机法。从各列数值最大值和最小值中间按正太分布随机选取k个质心。
关于离群点
离群点就是远离整体的,非常异常、非常特殊的数据点。因为k-means算法对离群点十分敏感,所以在聚类之前应该将这些“极大”、“极小”之类的离群数据都去掉,否则会对于聚类的结果有影响。离群点的判定标准是根据前面数据可视化分析过程的散点图和箱线图进行判定。根据散点图和箱线图,需要去除离散值的范围如下:
1)单价:基本都在100000以内,没有特别的异常值。
2)总价:基本都集中在3000以内,这里我们需要去除3000外的异常值。
3)建筑面积:基本都集中在500以内,这里我们需要去除500外的异常值。
数据的标准化
因为总价的单位为万元,单价的单位为元/平米,建筑面积的单位为平米,所以数据点计算出欧几里德距离的单位是没有意义的。同时,总价都是3000以内的数,建筑面积都是500以内的数,但单价基本都是20000以上的数,在计算距离时单价起到的作用就比总价大,总价和单价的作用都远大于建筑面积,这样聚类出来的结果是有问题的。这样的情况下,我们需要将数据标准化,即将数据按比例缩放,使之都落入一个特定区间内。去除数据的单位限制,将其转化为无量纲的纯数值,便于不同单位或量级的指标能够进行计算和比较。
我们将单价、总价和面积都映射到500,因为面积本身就都在500以内,不要特别处理。单价在计算距离时,需要先乘以映射比例0.005,总价需要乘以映射比例0.16。进行数据标准化前和进行数据标准化后的聚类效果对比如下:图32、图33是没有数据标准化前的聚类效果散点图;图34、图35是数据标准化后的聚类效果散点图。
数据标准化前的单价与建筑面积聚类效果散点图:
聚类结果如下
from sklearn.cluster import KMeans # 导入Kmeans
k=5 # 聚类类别数
kmodel = KMeans(n_clusters=k, n_jobs=4)
kmodel.fit(housing_prepared)
kmodel.cluster_centers_[0:5,:3]
ss.inverse_transform(kmodel.cluster_centers_[0:5,:3],copy=True)
housing_data = housing.copy()
housing_data.head(1)
c = pd.DataFrame(kmodel.cluster_centers_).sort_values(2)
c['房屋等级']=['交通不便普通','交通一般普通','交通便利普通','高端房源','豪宅']
#c
labels = c
# 高价值
expensive = labels[labels['房屋等级']=='豪宅'].index[0]
high = labels[labels['房屋等级']=='高端房源'].index[0]
mid = labels[labels['房屋等级']=='交通便利普通'].index[0]
normal = labels[labels['房屋等级']=='交通一般普通'].index[0]
low = labels[labels['房屋等级']=='交通不便普通'].index[0]
#print(expensive,high,mid,low)
kmeans_type_shift = {
expensive:'豪宅',
high:'高端房源',
mid:'交通便利普通',
normal:'交通一般普通',
low:'交通不便普通'
}
housing_data['聚类类别'] = housing_data['聚类类别'].map(kmeans_type_shift)
housing_data.head()
housing_data.shape
#housing_data.info()
housing_data.to_csv('housing_with_type.csv')