【二】PYTHON爬取全国新房房价与浅析
PART ONE:【数据采集】爬取某房产网站的新房数据
结论:获取全国新房数据近3.5W条
代码部分:
爬虫用的是比较简单的经典结构,首先是爬取器——html_parser.py,先爬取所有的城市子链接,再通过城市子链接爬取该城市下的所有新房数据。
# -*- coding:utf-8
from urllib.parse import urljoin
from bs4 import BeautifulSoup
import re
import time
class HtmlParser(object):
def cityurlparser(self, html_cont):
soup = BeautifulSoup(html_cont, 'html.parser', from_encoding='utf8')
#创建city_url_half元素集合,为下面操作伏笔
city_url_half = set()
city_urls = []
linklist = soup.find('div', class_="outCont").find_all('a', href=True)
#给city_url_half元素集合里增加各个城市的根链接
[city_url_half.add(city_url["href"]) for city_url in linklist]
for a in city_url_half:
b = a.replace('http://', 'http://newhouse.').replace('com/', 'com/house/s/')
city_urls.append(b)
return city_urls
def parse(self,page_url,html_cont):
if page_url is None or html_cont is None:
return
soup=BeautifulSoup(html_cont,'html.parser',from_encoding='utf8')
new_urls=self._get_new_urls(page_url,soup)
new_data=self._get_new_data(soup)
city=self._get_city(soup)
return new_urls,new_data,city
#获取分页链接(一个城市子链接下有不同的分页)
def _get_new_urls(self,page_url,soup):
new_urls=set()
links=soup.find('div',class_="page").find_all('a', href=re.compile(r'/house/s'))
for link in links:
new_url=link['href']
new_full_url=urljoin(page_url,new_url)
# print(new_full_url)
new_urls.add(new_full_url)
return new_urls
#获取页面内容
def _get_new_data(self,soup):
res_data=[]
nodes=soup.find_all('div',class_="nlc_details")
time.sleep(3)
for node in nodes:
house_data={}
house_name=node.find('div',class_="nlcd_name").get_text()
house_data['house_name']=''.join(house_name.split())
house_tag=node.find('div',class_="nlcd_name").find('a',href=True).get("href")
house_data['house_tag']=''.join(house_tag.split())
try:
house_address=node.find('div',class_="address").get_text()
house_data['house_address']=''.join(house_address.split())
except:
house_data['house_address']=""
try:
house_price=node.find('div',class_="nhouse_price").get_text()
house_data['house_price']=''.join(house_price.split())
except:
house_data['house_price']=""
res_data.append(house_data)
#print("house_data:",house_data)
return res_data
def _get_city(self,soup):
house_city=soup.find('div',class_="s4Box").get_text()
return house_city
URL管理器——url_manager.py,对爬取到的url地址进行新老判断,避免重复解析。
# -*- coding:utf-8
class UrlManager(object):
def __init__(self):
self.new_urls=set()
self.old_urls=set()
def add_new_url(self, url):# object) -> object:
if url is None:
return
if url not in self.new_urls and url not in self.old_urls:
self.new_urls.add(url)
def add_new_urls(self, urls):
if urls is None or len(urls)==0:
return
for url in urls :
self.add_new_url(url)
def has_new_url(self):
return len(self.new_urls)!=0
def get_new_url(self):
#list.pop()默认移除列表中最后一个元素对象
new_url=self.new_urls.pop()
self.old_urls.add(new_url)
return new_url
爬取之后就是下载,下载器——html_downloader.py
# -*- coding:utf-8
import requests
class HtmlDownloader(object):
def download(self, url):
if url is None:
return None
html=requests.get(url)
html=html.text.encode(html.encoding).decode("gbk").encode("utf8")
return html
将爬取的数据下载后,需要存储到诸如EXCEL或者数据库。据说Mongo比较适合爬虫,不过我还是先从比较熟悉的MySQL数据库开始了。前后整体用下来暂时还行,不过目前都算小数据量。html_outputer.py代码如下:
# -*- coding:utf-8
import pymysql
import time
class HtmlOutputer(object):
#写入数据库
def output_mysql(self,new_data,house_city):
db = pymysql.connect(host="localhost", user="root", passwd="password", db="database", charset="utf8",cursorclass=pymysql.cursors.DictCursor)
cursor=db.cursor()
# 建表时,还是建议设置下primary key,后来看来还是有15%左右的数据是重复的。一开始忘记设置,所以就在Navicat里处理了。
# sql = """CREATE TABLE fang1 (
# HouseName VARCHAR(255),
# HouseTag VARCHAR(255),
# HouseAddr VARCHAR(255),
# HousePrice VARCHAR(255),
# HouseCity VARCHAR(255),
# CrawDate DATE
# )"""
# cursor.execute(sql)
# db.commit()
for item in new_data:
#笔记# new_data是一个以字典为子元素的列表,item指取其中的字典,item.values指取字典的values,取出后的结果是str类型,为了后面要新增house_city和CrawDate,所以做了下类型转换(str->list)
values = list(item.values())
values.append(house_city)
values.append(time.strftime('%Y-%m-%d', time.localtime(time.time())).encode("utf8"))
print('tv:',tuple(values))
try:
#笔记# %s加引号匹配不出来;VALUES后面要加的params参数移到下面的execute里执行了,试验结果表明params只能传tuple,所以给前面的values参数做了下类型转换(list->tuple)
sql = "INSERT INTO fang1 (HouseName,HouseTag,HouseAddr,HousePrice,HouseCity,CrawDate) VALUES(%s, %s, %s, %s, %s, %s);"
cursor.execute(sql,tuple(values))
db.commit()
except:
print(item["house_name"], "false")
db.rollback() # 发生错误时回滚
db.close()
最后就是爬虫的总调度程序spider_main.py了,管理调度着前面四位小哥的行动。
# -*- coding:utf-8
__author__ ='Starry-Sky-WMG'
import url_manager,html_downloader,html_parser,html_outputer
class SpiderMain(object):
def __init__(self):
self.urls=url_manager.UrlManager()#管理URL
self.downloader=html_downloader.HtmlDownloader()#下载URL内容
self.parser=html_parser.HtmlParser()#解析URL内容
self.outputer=html_outputer.HtmlOutputer()#输出获取到的内容
#获取待爬取城市链接
def CityUrl_crawl(self,root_url):
html_cont=self.downloader.download(root_url)
city_urls=self.parser.cityurlparser(html_cont)
return city_urls
#爬虫主体程序
def crawl(self,city_url):
count=0
self.urls.add_new_url(city_url) #将城市链接放入
while self.urls.has_new_url():
count=count+1
try:
new_url=self.urls.get_new_url()#获取新的链接
html_cont=self.downloader.download(new_url)#下载页面内容
new_urls, new_data ,house_city= self.parser.parse(new_url, html_cont)#解析页面内容
print('new_data:',new_data)
self.urls.add_new_urls(new_urls)
self.outputer.output_mysql(new_data,house_city)#写入mysql
print ("【",house_city,"】的第",count,"个网页【",new_url,"】输出成功------")
except Exception as e:
return e
if __name__=="__main__":
obj_spider=SpiderMain()
#以下第一个root_url,只能爬取主要城市(约占总城市数量的六分之一)
# root_url="http://bunengshuodemimi1"
root_url="http://bunengshuodemimi2"
city_urls=obj_spider.CityUrl_crawl(root_url)
for city_url in city_urls:
obj_spider.crawl(city_url)
本着一颗淳朴的比较善良的心,我还是把该房产网站的URL给隐了,避免大概率殃及该网站及其程序员小哥。
PART TWO:一个可视化的想法
结论:在成为懒人之前很可能需要百转千回兜兜转……
当我爬取完数据,就想着做可视化了,此时又想偷懒了,就百度了一下”python爬虫 echarts“,发现这边果然有位同学[http://www.iteye.com/news/32687]已经展示了python爬虫与echarts的结合案例。看着写的挺好的,但是读到jQuery的地方,我当即眉头一皱。然后笑了笑。掉头离开。进行下一次百度,肯定有人把这个过程封装成Package吧,这帮程序员这么懒对吧,怎么会放弃这么一个表现自己的机会。
果真,像哥伦布发现了印第安人的新大陆,我发现了别人封装的Pyecharts……
Pyecharts的github的内容还挺丰富,开发者还专门做了一个中英文双语版带有动图的WIKI,读起来也很舒服。正想着天下还有这样的好事,喜悦不过片刻,然后就又看到JavaScripts。看来是躲不过了,我想起之前范老师书上有一小节讲了JS,大概也就是类似”十分钟上手XXX“这类文章的篇幅大小,在候机厅里我认真地读完了它。然后谜一般的自信地盖上了书本。截止目前也还没用上。
我想在中国地图上展示不同区域的房价分布情况,然后看了下pyecharts只提供了各城市的地理坐标(下称’location’),但是我爬取的是一个个具体的房产项目的地址,基本上都是细到“道路“这个级别。好在pyecharts支持拓展,需要自己输入其他的location。于是,我通过百度地图开放平台提供的API,注册了个人开发者账号(这样单日能免费解析的坐标数会比较多),解析了3W多个房产项目的具体location,并存入MySQL。
说个题外话,两年多前刚毕业的时候,见产品老大、开发小哥和工位附近的数据分析小哥都在和一个数据库客户端玩耍得很欢乐,有点小好奇,但当时觉得这玩意看起来好像很复杂的样子,想着这辈子应该是老死不相往来了,于是乎乖乖滚回去做产品和运营。
说的客户端就是Navicat MySQL。
好吧,现在看来好像基础的功能项其实还挺简单的……
获取location并存入MySQL的代码如下:
# -*- coding:UTF-8 -*-
import requests
import pymysql
db = pymysql.connect(host="localhost", user="root", passwd="password", db="database", charset="utf8")
#将cursor游标直接设置成dict读取,后面的遍历就不用再转换类型了。
cursor=db.cursor()#cursor=pymysql.cursors.DictCursor
#在百度个人开发者的申请还未通过之前,能识别的坐标数有限,执行到一半就中止了。
#后来申请通过后再操作时,需要剔除一下已经存下来的(我将爬取的房产信息存储在fang1这张表,获得的location存储在fang2这张表里),避免重复解析。
sql="SELECT fang1.HouseAddr FROM fang1 LEFT JOIN fang2 ON fang1.HouseAddr=fang2.addr WHERE addr is NULL OR addr=''"
cursor.execute(sql)
b=cursor.fetchall()
db.commit()
count_e=0
ak='bunengshuodemimi3'
for item in b:
# addr=item["HouseAddr"]
# item1=tuple.__str__(item)
# print('item1:',item1)
url = 'http://api.map.baidu.com/geocoder/v2/?address=' + item[0] + '&output=json&ak=' + ak
resp=requests.get(url=url,timeout = 500).json()
print('resp:',resp)
print('item2:',item[0])
# resp的内容格式大致如此-->>>{'status': 0, 'result': {'location': {'lng': 108.24764375632363, 'lat': 22.808304503694224}, 'precise': 1, 'confidence': 80, 'level': '道路'}}
try:
locat=list(resp['result']['location'].values())
addr_locat = []
addr_locat.append(item[0])
addr_locat.append(str(locat))
addr_locat_list=[]
addr_locat_list.append(tuple(addr_locat))
print('addr_locat_list:',tuple(addr_locat_list))
sql2="""INSERT INTO fang2 (addr,locat) VALUES(%s,%s)"""
cursor.executemany(sql2,tuple(addr_locat_list))
db.commit()
except Exception as e:
count_e += 1
print(e,count_e)
db.close()
接下来就是改写pyecharts已有的地理限定了,引入pyecharts,并且传入存储在MySQL里的location。
数据读来读去的,解析来解析去,数据类型变来变去,很容易报错。这块代码我百度了很多次,也尝试执行了无数次,之前Python基础知识没有掌握牢固。光看书也不行,有些情况书本上也没写。
注:以下可视化的代码是在jupyter notebook里执行的,前面的是用Pycharm编辑器执行的。
# 全国新房房价概览【地图】
# -*- coding:utf-8
from pyecharts import Geo
import pymysql
import warnings
warnings.filterwarnings("ignore")
db = pymysql.connect(host="localhost", user="root", passwd="password", db="database", charset="utf8")#cursorclass=pymysql.cursors.DictCursor)
cursor = db.cursor()
#不加数量限制的话,jupyter notebook会报”IOPub data rate exceeded.“,保险起见,我就先限制解析10000了。
sql=u'SELECT addr,locat FROM fang2 limit 10000;'
cursor.execute(sql)
data_tuple=cursor.fetchall()
# print('data_tuple:',data_tuple)
# tuple -> dict
data_dict1=dict(list(data_tuple))
# data_dict1=json.dumps(dict(data_tuple),ensure_ascii=False)
data_dict2={key:eval(values) for key,values in data_dict1.items()}
# data_dict1数据结构如{'[武安市]武安市洺湖北侧武安一中对面': '[114.2005836920196, 36.696448476792206]'}
# data_dict2数据结构如{'[武安市]武安市洺湖北侧武安一中对面': [114.2005836920196, 36.696448476792206]}
#echarts就是不认data_dict1的数据结构,只能改为data_dict2;而前面一块代码里写入数据库的时候,只能是data_dict1这样的数据结构。所以前面一块代码”addr_locat.append(str(locat))“中会给locat进行str类型转换。
sql2=u"SELECT HouseAddr,CAST(HousePrice AS UNSIGNED) AS price FROM fang1 WHERE HousePrice LIKE '%元/㎡%' limit 10000"#爬取的数据中,存在一定量的比较不规则的房产数据(如,3万起),将这部分数据进行剔除,所以限定正则须满足 '%元/㎡%',剩下约2W多条数据。
cursor.execute(sql2)
data_tuple2=cursor.fetchall()
data2=tuple(data_tuple2)
data2=list(data2)
geo = Geo("全国新房房价概览", "data from xinfang", title_color="#fff",
title_pos="center", width=1200,
height=600, background_color='#404a59')
attr, value = geo.cast(data2)
geo.add("", attr, value, visual_range=[0, 40000], visual_text_color="#fff",symbol_size=8, is_visualmap=True,geo_cities_coords=data_dict2)
geo.render()
geo
# 全国新房价格区间分布【饼图】
from pyecharts import Pie
import pymysql
db = pymysql.connect(host="localhost", user="root", passwd="password", db="database", charset="utf8")#cursorclass=pymysql.cursors.DictCursor)
cursor = db.cursor()
sql="SELECT FANGNUM FROM(SELECT ELT(INTERVAL(a.HousePrice,0,5000,8000,12000,18000,25000,30000,35000,40000,45000),'1/5K(含)以下','2/5K~8K(含)','3/8K~1.2W(含)','4/1.2W~1,8W(含)','5/1.8W~2.5W(含)','6/2.5W~3W(含)','7/3W~3.5W(含)','8/3.5W~4W(含)','9_1/4W~4.5W(含)','9_2/4.5W以上(含)') AS price_e,\
COUNT(HouseAddr) AS FANGNUM \
FROM fang1 AS a \
WHERE HousePrice LIKE '%元/㎡%' GROUP BY price_e \
ORDER BY price_e) b ORDER BY price_e"
cursor.execute(sql)
data_tuple=cursor.fetchall()
data=list(data_tuple)
print('1:',data_tuple)
print('2:',data)
v1 = data
attr = ['5K(含)以下','5K~8K(含)','8K~1.2W(含)','1.2W~1,8W(含)','1.8W~2.5W(含)','2.5W~3W(含)','3W~3.5W(含)','3.5W~4W(含)','4W~4.5W(含)','4.5W以上(含)']
pie = Pie("全国新房房价区间分布", title_pos='center', width=900)
pie.add("A", attr, v1, center=[50, 50], is_random=True, radius=[30, 75], rosetype='area', is_legend_show=False, is_label_show=True)
pie.show_config()
pie.render()
pie
# 全国各城市新房均价分布【柱图】
from pyecharts import Bar
import pymysql
db = pymysql.connect(host="localhost", user="root", passwd="password", db="database", charset="utf8")#cursorclass=pymysql.cursors.DictCursor)
cursor = db.cursor()
sql=u"""SELECT HouseCity,CAST(AVG(HousePrice) AS UNSIGNED) as avg_price
FROM fang1
WHERE HousePrice LIKE '%元/㎡%'
GROUP BY HouseCity
ORDER BY avg_price"""
cursor.execute(sql)
data_tuple=cursor.fetchall()
data=list(tuple(data_tuple))
city_list=[]
avgprice_list=[]
for i in data:
city_list.append(i[0])
avgprice_list.append(i[1])
print(city_list,avgprice_list)
bar = Bar("直方图示例")
bar.add("", city_list, avgprice_list, bar_category_gap=0)
bar.render()
bar
PART THREE:【数据可视化】简单的可视化……
结论:
基于part two的代码,生成相应的效果图。这次是真懒了,不上动图了,来些PNG吧。
以上,一言以蔽之,厦门的房价就是这么贵。
以上,一言以蔽之,然并卵,买得起的还是买得起……
接下来再来看看各城市的新房均价分布情况吧。
澳门竟然排在第一个!?显然不可能,用SQL查了一下数据详情发现单位是“港币/呎”,可是我明明限制了“HousePrice LIKE '%元/㎡%' ”。没懂……
最高是深圳,第一张图太密了,为了满足看众的好奇心,我决定贴出第二张图——均价高于18000的分布图。
均价大于18000的城市还挺少的,不过这个均价的参考意义性肯定不大,纯做case。
看到这图,很多人应该都会当即产生疑问——北京去哪儿了啦?
呃,貌似是因为北京的URL有点与众不同导致运用上面的通用代码没爬取到。
这次,就先写到这吧。