【二】PYTHON爬取全国新房房价与浅析

【一】学PYTHON及爬虫的一些总结

【二】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。


image.png

说个题外话,两年多前刚毕业的时候,见产品老大、开发小哥和工位附近的数据分析小哥都在和一个数据库客户端玩耍得很欢乐,有点小好奇,但当时觉得这玩意看起来好像很复杂的样子,想着这辈子应该是老死不相往来了,于是乎乖乖滚回去做产品和运营。
说的客户端就是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吧。

全国新房房价概览.png

福建省新房房价概览.png

以上,一言以蔽之,厦门的房价就是这么贵。

image.png

以上,一言以蔽之,然并卵,买得起的还是买得起……

接下来再来看看各城市的新房均价分布情况吧。
澳门竟然排在第一个!?显然不可能,用SQL查了一下数据详情发现单位是“港币/呎”,可是我明明限制了“HousePrice LIKE '%元/㎡%' ”。没懂……
最高是深圳,第一张图太密了,为了满足看众的好奇心,我决定贴出第二张图——均价高于18000的分布图。

aom
image.png

均价大于18000的城市还挺少的,不过这个均价的参考意义性肯定不大,纯做case。
看到这图,很多人应该都会当即产生疑问——北京去哪儿了啦?
呃,貌似是因为北京的URL有点与众不同导致运用上面的通用代码没爬取到。

这次,就先写到这吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容