【Python爬虫+pyecharts可视化】爬取全国各地房价并在echarts的geo地图上展示

1 篇文章 0 订阅
1 篇文章 0 订阅

导言


          最近回归了可视化,写个文章总结一下经验教训,嘿嘿。不想看分析过程的可以点击目录,直接跳转到代码实现部分。(代码所用模块都是可以用    pip install 模块名    下载的哟)

          先看看最终效果:

目录

项目需求

总体分析

详细分析

代码实现

代码测试

维护更新


项目需求

       获取全国各地的房价,计算出平均值,并用echarts中的geo图表进行展示。

总体分析

  • 爬取数据过程
  1. 利用爬虫获取全国房价。
  2. 将获取的房产信息存储在csv文件中。
  3. 处理csv文件中的数据,筛选出所需字段,并计算出各地房价平均值,最后保存在另一个csv文件中。
  • 可视化过程
  1. 制作echarts中的geo地理图表。
  2. 将数据导入geo图表。
  3. 修饰geo图表。

详细分析

  1. 爬取的是链家网的房价。
  2. 所使用的是Python中的requests模块进行爬取。Python中有很多适用于爬虫的模块,例如urllib.request 。本次我们仅介绍requests模块。
  3. requests模块集成了大量适用于请求相关的函数,感兴趣的可以先自行搜索requests相关文档,后续有空我会上传。本次使用requests中的get()函数获取HTML页面代码。
  4. 利用Python中的BeautifulSoup模块解析爬取的html代码。Python中也有很多解析网页的模块,例如DOM。本次我们仅介绍bs4模块。(bs4是BeautifulSoup4的缩写,代表着它已经发展到了第四代版本。)
    1. 补充:为什么需要解析页面?
      1. 页面原本是纯粹的HTML代码,Python中如果想要操作这些HTML代码(准确说应该是页面中的标记,如div,span),需要将这些HTML代码转换为Python能处理的东西——对象。所以一般会通过某种解析器将获取到的HTML代码转换为HTML对象,便于我们使用Python代码对这些对象进行增删改查的操作。
      2. 具体内容可以百度搜索一下HTML解析器。(目前主要掌握使用解析器的目的即可)。
  5. 利用CSS选择器选择页面中的需要的标记。
  6. 获取标记中的内容,写入保存在csv文件中。
  7. 利用pandas模块读取数据。
  8. 利用pandas处理数据,主要操作是根据自行需求进行数据处理。例如本案例中的需求就是,筛选出所有数据的【省会名称】、【价格】两项,并且对价格进行分组、求平均操作。
  9. 将处理后的数据重新保存在另一个csv文件中备用。
  10. 利用pyecharts模块显示备用csv文件中的数据。

代码实现

一共三个代码文件。会产出两个csv文件和一个html文件。

第一个文件,爬虫文件。下方代码中大部分内容已经添加注释,主要思路是爬取全部页面中的数据,并且保存在csv文件中。如果有需要了解或者可以优化的部分欢迎大家留言(づ ̄3 ̄)づ╭❤~。

# 自动获取全国房价
from bs4 import BeautifulSoup
import requests
import random
import time
import csv
import math

# 链家【新房】链接的入口地址
url = 'https://bj.fang.lianjia.com/'
Agent = [
    'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.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/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/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; QQDownload 732; .NET4.0C; .NET4.0E)',
    'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
]
SumCount = 0  # 所有页面总共的房产数量
flag = True  # 控制值写入一次csv文件头
filename = '全国房产信息.csv'

title = ['省会名称', '楼盘名称', '所在区域', '详细地址', '室厅数量', '建筑面积', '价格']


# 生成response对象
def getResponse(u):
    user_agent = random.choice(Agent)
    headers = {'User-Agent': user_agent}
    r = requests.get(u, headers=headers)
    r.encoding = r.apparent_encoding
    return r


# 获取所有城市链接    存储在<li class="clear">  标签中 所有后代元素 <a href="//bd.fang.lianjia.com" title="保定房产网">保定</a>
# 返回 包含所有{'城市名称':'链接'}的字典
def getHref(u):
    city_name_href_dict = {}
    r = getResponse(u)
    if r.status_code == 200:
        html = r.text
        tagList = getTag(html, 'li.clear a')
        city_href_list = getAttributeFromTag(tagList, 'href')
        city_name_list = getAttributeFromTag(tagList, 'title')

        # 利用遍历,将城市名称和链接,添加至字典中
        for i in range(0, len(city_name_list)):
            key = city_name_list[i]
            href = city_href_list[i]
            value = "http:" + href + '/loupan/'
            city_name_href_dict[key] = value
        return city_name_href_dict
    else:
        print('获取所有城市链接时失败')
        return str(r.status_code)


# 获取最大页码    参数 :地址
def getMaxPageNum(u):
    r = getResponse(u)
    tag = getTag(r.text, 'div.page-box')
    # tag[0]['data-total-count']    tag[0]获取包含总房产数量的div对象, ['data-total-count']获取其身上的总房产数量属性值
    allPage = int(tag[0]['data-total-count'])
    maxPageNum = math.ceil(allPage / 10)
    return maxPageNum


# 获取页面标记组
def getTag(html, element):
    soup = BeautifulSoup(html, 'html.parser')
    tagList = soup.select(element)
    return tagList


# 获取一页内城市信息
def getCityInfo(city_TagList, p):
    global SumCount
    AllList = []  # 存储所有的list
    pageInsideCount = 0  # 一个页面内几条数据

    for tag in city_TagList:
        city_tag_list = []
        # 信息可能不存在
        try:
            # 获取信息
            provinceName = p
            cityName = tag.select('div.resblock-name a')
            cityLocation = tag.select('div.resblock-location span')
            cityAddress = tag.select('div.resblock-location a')
            cityRoom = tag.select('a.resblock-room span')  # 几室几厅
            cityArea = tag.select('div.resblock-area span')  # 建筑面积
            cityPrice = tag.select('div.resblock-price span.number')

            # 向列表中添加房产信息
            city_tag_list.append(provinceName)  # 省会名称
            city_tag_list.append(cityName[0].text)  # 依次添加每一个内容到list中
            city_tag_list.append(cityLocation[0].text)
            city_tag_list.append(cityAddress[0].text)
            city_tag_list.append(cityRoom[0].text)
            city_tag_list.append(cityArea[0].text)
            city_tag_list.append(cityPrice[0].text)
        except AttributeError as e:
            # 没有text属性
            print('没有获取到房产信息对象')
        except IndexError as e1:
            # 没有获取到某个标记,而导致[0]操作时,下标越界
            # 信息可能不存在,判断不存在则添加空串避免出错。
            if 0 == len(cityName):
                cityName = ['null']
            if 0 == len(cityLocation):
                cityLocation = ['null']
            if 0 == len(cityAddress):
                cityAddress = ['null']
            if 0 == len(cityRoom):
                cityRoom = ['null']
            if 0 == len(cityArea):
                cityArea = ['null']
            if 0 == len(cityPrice):
                cityPrice = ['null']
        AllList.append(city_tag_list)  # 将list放入AllList中  形成二维数组,一会便于写入csv
        pageInsideCount = pageInsideCount + 1

    SumCount = SumCount + pageInsideCount

    print("本页共" + str(pageInsideCount) + "条数据")
    return AllList


# 从标记组中获取某标签某属性包含的连接  存储到字典中返回
def getAttributeFromTag(tagList, attr):
    attr_list = []
    for tag in tagList:
        value = tag[attr]
        # value = "http:" + href + '/loupan/'        # 需要/loupan资源才能访问到房产信息首页页面
        attr_list.append(value)
    return attr_list


# 获取各个城市房产信息
def getBuildingInfo(c_dict):
    # 文件名
    global filename
    key_list, value_list = [], []
    # 获取所有城市名
    for key in c_dict:
        key_list.append(key)
        value_list.append(c_dict[key])
    # c_list全部城市首页链接
    for city_url in value_list:
        # 获取当前城市最大页面数字
        maxPageNum = getMaxPageNum(city_url)
        # 遍历当前城市所有页面
        for pageNum in range(1, maxPageNum + 1):
            # 拼接所有页面地址
            r_city_url = city_url + '/pg' + str(pageNum)
            # city_response 每个城市对应的响应对象
            city_response = getResponse(r_city_url)
            if 200 == city_response.status_code:
                html = city_response.text
                #这里去掉了选择其中的.has-results 部分,因为有的页面中li的类名仅为下方内容,并不包含.has-results 部分
                building_tagList = getTag(html,
                                          'ul.resblock-list-wrapper li.resblock-list.post_ulog_exposure_scroll')  # 每一个城市房产列表 所有的li
                # value_list.index() 获取指定元素下标   这里是获取下标之后,再获取key_list中对应下标的城市名称
                province = key_list[value_list.index(city_url)].split('房')[
                    0]  # key_list[value_list.index(city_url)] 内容为  xx房产网
                # 获取到所有li后获取其中的房屋信息    building_info_list是二维列表
                building_info_list = getCityInfo(building_tagList, province)  # 当前页面所有房屋信息的二维列表
            saveData(filename, building_info_list)
            print("获取【" + province + "】的第【" + str(pageNum) + "】页已经完成...")
            print('睡一秒...')
            time.sleep(1)
            print('继续!')
        print("【" + province + "】所有页已经完成...")
        print('睡一秒...')
        time.sleep(1)
        print('下一座城市!')
    print("全国共【" + str(SumCount) + "】条数据")
    # return building_info_list


# 保存数据
def saveData(fname, b_info_list):
    global title, flag
    with open(fname, 'a',newline='',encoding='utf-8') as f:
        csvFile = csv.writer(f)
        if flag:
            csvFile.writerow(title)
            flag = False
        csvFile.writerows(b_info_list)  # 写多行  也就是二维数组的时候用writerows()  一维数组用writerow()
    print('数据写入完成!')


# 获取所有城市链接
cityLinkList = getHref(url)
# 获取各个城市房产信息  二维列表
getBuildingInfo(cityLinkList)

第二个文件,处理csv中数据的文件。这是还是强调一下,处理数据使用的是pandas模块,这个模块比较“沉重”,可以选择使用csv模块处理数据。pandas模块有些函数也不是很好用。(我才不会说是我不会用。)
另外,这个文件没有固定的内容,需要根据自己的需求去修改。比如,可能会对数据【去重】【排序】【分组】【求和】【求平均】等等,所以在处理数据这一块儿需要自己花点时间学习。

# 将全国房价清洗为各省市平均值
import csv
import pandas as pd
import numpy


# 读取csv文件
def getCsvFile(csvName):
    dataFrame = pd.read_csv(csvName + '.csv')
    return dataFrame


# 处理数据
def processData(df):
    df_list = []
    df = df.drop_duplicates("楼盘名称")
    row_indexs = df[df['价格'] == '价格待定'].index.tolist()
    df = df.drop(axis=0, labels=row_indexs)
    # 分组
    # 是个生成器
    group = df['价格'].groupby(df['省会名称'])
    for g in group:
        result_list = []
        # g[1].tolist()  --  ['22000', nan, nan, nan, nan, nan, nan, '17000']  每个省会对应的房价列表
        l = g[1].tolist()
        # 高效去除nan
        while numpy.nan in l:
            l.remove(numpy.nan)
        # 把所有str转换为int
        l = [int(x) for x in l]
        # 求平均
        avg_l = float('%.2f' % numpy.mean(l))
  
        # 制作列表作为返回值使用
        # g[0]  --  省会名称
        result_list.append(g[0])
        result_list.append(avg_l)
        df_list.append(result_list)
    return df_list


# 存储处理后的csv文件
def saveCsvFile(csvData, csvName):
    title = ['省会名称', '价格']
    r_csvName = csvName + 'v1.csv'
    with open(r_csvName, 'w', newline='') as f:
        csvFile = csv.writer(f)
        csvFile.writerow(title)
        for i in csvData:
            csvFile.writerow(i)


csvFilename = '全国房产信息'
csv_df = getCsvFile(csvFilename)

result_df = processData(csv_df)
saveCsvFile(result_df, csvFilename)

第三个文件,将数据可视化处理的文件。啊最喜欢的文件来了,有了它我们的数据就会变得很直观、漂亮了。但是之前玩的是js的echarts,此文件使用的是pyecharts。它是python为了便捷学习、操作echarts专门制作的模块,可以实现部分主要的echarts功能。但是说句实在话,习惯了前端后端分开,把原本前端的东西放在后端,有点整的不会了。。api很多,每一个都可以自己试着玩一玩,下方代码中我自己试着玩了一些,剩下的欢迎大家自己测试。

还有,在这里顺便向各路大神请教个问题,如何利用pyecharts中实现 legend(图例)的单击事件。我想实现点击各个城市名称跳转到对应城市地图的操作。🙏

from pyecharts import options as opts
from pyecharts.charts import Geo
import pandas as pd
from pyecharts.options import TextStyleOpts

csvFilename = '全国房产信息v1.csv'
province_list = []
price_list = []
df = pd.read_csv(csvFilename)
province_list = df['省会名称'].tolist()
price_list = df['价格'].tolist()
# 生成全国各省市平均房价显示图

c = (
    Geo(
        # 设置生成的div及页面属性
        init_opts=opts.InitOpts(
        width='1700px',
        height='750px',
        page_title='全国各省市平均房价',
    ))
        .add_schema(maptype="china")
        .add_coordinate('保亭',109.70259,18.63905)
        .add_coordinate('乐东',109.17361,18.74986)
        .add_coordinate('陵水',110.0372,18.50596)
        .add("城市名", [list(z) for z in zip(province_list, price_list)])
        .set_series_opts(
        label_opts=opts.LabelOpts(
            is_show=True,
            formatter='{b}',

        )
    )
        .set_global_opts(
        visualmap_opts=opts.VisualMapOpts(
            is_piecewise=True,
            min_=0,
            max_=40000,
            range_size=10000
        ),
        title_opts=opts.TitleOpts(
            title="全国各省市平均房价",
            # title_link='http://www.baidu.com',  # 点击标题跳转链接
            # title_target=,    #链接对应新窗口的打开方式  _blank _self
            # subtitle=,
            # subtitle_link=,
            # subtitle_target=,
            # item_gap=10,    #主副标题之间的间距。
            # title_textstyle_opts={
            #     'color':'#dd23fb',
            #
            # },       #标题样式  是个字典
            # subtitle_textstyle_opts={
            #
            # },       #副标题样式  是个字典

        ),
        legend_opts=opts.LegendOpts(
            legend_icon='circle',

        ),
        tooltip_opts=opts.TooltipOpts(
            is_show=True,
            trigger='item',
            axis_pointer_type='cross',
            is_always_show_content=False,
            # position=['10%','10%'],
            # formatter='{b0}: {c0}<br />{b1}: {c1}'
            textstyle_opts= TextStyleOpts()

        ),



    )
        .render("全国各省市平均房价.html")
)

代码测试

首先,第一个文件中,在获取城市信息的时候可能会出现问题。

tag.select(element)  element是我们获取目标数据的各项选择器,但是这个选择器有时会获取不到数据,比如,有的房产信息没有书写几室几厅,有的没有书写房价,这样就会导致数据获取不到而报错,所以,才有了如下的try except代码。

 

其次,第二个文件中,pandas处理数据过程中涉及到了分组、求平均的操作。(pandas分组方式比较简单,形式很多,可以百度自行学习。)根据以往数据库的经验,数据都是先分组,再聚合,所以这里也先将数据进行group()分组操作,然后求平均mean()。这个mean()函数有很多坑。我搜索了很多资料,他们的求平均代码是没有问题的,不过只适用于他们的数据,不适用于我的数据。查看之后发现csv文件中获取到的【房价】信息有的值是‘价格待定’,由于‘价格待定’是string类型的数据,且无法转换为int或float类型,这导致了'价格待定'这样的数据无法参与mean()函数的运算。,所以专门使用drop()函数将这些数据删除。重要的来了就算删除了这些数据,仍然无法使用mean()函数。这一点困惑了我好久。最后无奈放弃了,使用了下图所示的方式。(谁会用pandas分组后的mean()求平均务必赐教,要哭了😢。)

图片中的主要思路是:删除价格待定的行,然后将数据分组,按照图中df['价格'].groupby(['省会名称'])来分组的话,它的含义是:根据【省会名称】分组,显示【价格】。这样产生的结果对象group将数据存储在了list中。因为list中的索引0对应着【省会名称】,索引1对应着【价格】。所以最后对索引1的数据进行处理。

下图中还用到了numpy.mean()函数,这个函数也是用于求平均的,但是并不适用与group对象,适用于list。

最后,第三个文件中,add()函数的第二个参数,需要的是二维数组的数据结构,也就是[['北京',50000],['上海',40000]]这样的数据。

我们可以通过for z in zip()来制作二位数组。

例如:for z in zip(list_1,list_2)   可以将list_1 和list_2的数据搅拌在一起。for 一个赞破:

list_1 = [1, 2, 3, 4]
list_2 = ['a', 'b', 'c']

for z in zip(list_1, list_2):
    print(z)

print([list(z) for z in zip(['裕华', '长安', '桥西', '新华'], [17723, 19428, 18575, 15245])])

输出:

(1, 'a')
(2, 'b')
(3, 'c')

[['裕华', 17723], ['长安', 19428], ['桥西', 18575], ['新华', 15245]]


可以看到for z in zip()  本身是将两个list中的数据,按照顺序,一个一个将对应索引的数据,重新存储了一个tuple中。

然后我们可以再通过list()函数,将这个tuple快速转换为list。

补充测试:

在运行第三个文件生成HTML页面时,可能会报错:  显示某某地点不在。这是因为echarts中的geo图表内并未包含所有的城市坐标数据,没有的需要我们手动添加。


Traceback (most recent call last):
 
pyecharts.exceptions.NonexistentCoordinatesException: 当前地点: ('保亭', 12388.89) 坐标不存在, 错误原因: cannot unpack non-iterable NoneType object

Process finished with exit code 1

添加这个城市的坐标即可。通过add_coordinate('城市名称',x地理坐标,y地理坐标)   。查看某城市坐标:https://jingweidu.bmcx.com/

c = (
    Geo(
        # 设置生成的div及页面属性
        init_opts=opts.InitOpts(
            width='1700px',
            height='750px',
            page_title='全国各省市平均房价',
        ))
        .add_schema(maptype="china")
        .add_coordinate('保亭', 109.70259, 18.63905)
        .add_coordinate('乐东', 109.17361, 18.74986)
        .add_coordinate('陵水', 110.0372, 18.50596)

维护更新

  1. 尝试利用pyecharts实现legend的点击事件,实现页面跳转。
  2. 还有,就是上传部分api文档了吧,大家也可以自行收集。

    好了,这次先更新到这了,如果你看到了这里那你一定很优秀,(づ ̄ 3 ̄)づ

 

4月24日更新:

  • requests模块api文档已经上传。
  • 第一个文件中添加了睡眠时间,使爬虫不易被检测到,更稳定,但是爬取时间更长。
  • 第一个文件中修改了每个页面总的li类选择器。  如下所示,原本爬取的是ul.resblock-list-wrapper li.resblock-list.post_ulog_exposure_scroll.has_result   但是部分页面,例如北京的18,19,20这些页面,他们的li是没有.has_result这部分的,所以将.has_result部分去掉,达到了获取全部北京页面房产信息的目的。
# 每一个城市房产列表 所有的li  修改前
building_tagList = getTag(html,'ul.resblock-list-wrapper li.resblock-list.post_ulog_exposure_scroll.has_result')  
               
# 每一个城市房产列表 所有的li  修改后
building_tagList = getTag(html,'ul.resblock-list-wrapper li.resblock-list.post_ulog_exposure_scroll')  
               

 

  • 第一个文件中修改了向csv文件中写入内容的指定字符集。
    • 不同系统中写入csv的数据,会产生字符集问题。
      • with open() as f:     以这种方式向csv文件中写入数据时,会采用系统默认的字符集。我们可以通过open()函数的encoding属性来指定字符集。
      •  with open(fname, 'a',newline='',encoding='utf-8') as f:
                csvFile = csv.writer(f)
                if flag:
                    csvFile.writerow(title)
                    flag = False
                csvFile.writerows(b_info_list)  # 写多行  也就是二维数组的时候用writerows()  一维数组用writerow()
            print('数据写入完成!')

         

  • MAC OS系统默认字符集为utf-8。Windows 系统默认字符集为GBK。(由于我们使用的都是中文版的Windows,所以才使用了GBK字符集。)

 

 

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值