zoukankan      html  css  js  c++  java
  • 爬虫入门到放弃系列06:爬虫实战基金

    前言

    爬虫的基本知识已经告一段落,这次就找个网站实战一波。但是为什么选择了基金?这还要从我的故事讲起。

    我是一名韭零后,小白一枚,随大流入基市一载,佛系持有,盈亏持平。看到年前白酒红胜火,遂小投一笔,未曾想开市之后绿如蓝,赚的本韭菜空喜欢,一周梦回解放前。

    还记得那天的天台的风很凉,低头往下看车来车往,有点恐高。想点一支烟烘托一下气氛,才想起我不会抽烟。悲伤之际,突然想起一位名人曾说过:"只要你不跑,你就不是韭菜"。于是转身回家,坐在电脑前写下了这篇文章。

    准备

    1. 明确爬取目标
      爬取各个板块基金数据
    2. 寻找数据网站:天天基金网(fund.eastmoney.com)

    天天基金网

    1. 确定网站入口:在首页上点击 投资工具 -> 主题基金 进入主题页面,选择 主题索引,如下图:

    主题分类

    1. 确定爬取内容点击主题下的主题索引下的 白酒 进入白酒列表。

    点击招商中证白酒,进入详情页面。

    根据自己的需求,从页面上的内容确定要爬取的字段。这里要爬取的字段除了图中红框部分,还有基金名称、基金编码、所属主题字段。

    1. 明确页面跳转关系:主题页面 -> 列表 -> 详情页,一共三层

    网站分析

    第一层:请求网站入口

    F12或者右键选择检查,使用开发者工具找到基金分类的html元素。

    右键html元素,复制xpath,当然你可以自己写。

    开发代码获取分类列表:
    第一部分代码
    如图,按理说使用我自己写的xpath和拷贝的xpath,都可以获取到分类的html元素,但结果结果却为空。带着疑问,去查看返回的网页内容。

    请求内容

    如图,爬虫请求返回的网页和从浏览器上看到的网页元素不一样,行业分类内容没了!!刚接触爬虫的可能还在疑问为什么,开发过爬虫的已经开始抢答了:

    嗯,什么是动态加载? 这里我就用我自己的理解说一下。

    动态加载

    我们用浏览器访问一个网页的时候,后台返回给浏览器html网页、js、css等文件。浏览器内核(也称渲染引擎)在加载网页的同时,也会执行html中的js渲染网页,然后将渲染后的网页展示在浏览器上,即浏览器上的网页内容是:原始HTML + 浏览器js渲染的结果。

    js将数据渲染到网页的过程方式就是动态加载。那么,数据从哪来?

    你输入url请求网站时,其实js中定义的方法也偷偷地帮你发起了请求。最常见的是网页上有一数据展示的部分,当我们点击下一页时,页面没有进行跳转,只有展示数据部分刷新,这个就是ajax实现的局部刷新功能,也是最常见的动态加载之一。讲讲大致原理。

    前端开发者在js中对下一页按钮添加了点击监听事件。点击按钮时,进入相应js函数,在函数中使用ajax对后台url进行请求,返回json或者其他格式的数据,然后选中数据展示区的html元素,清除其中已有的数据,插入新获取的数据,就实现了数据刷新而不需要网页跳转的功能,也称为异步请求、局部刷新。当然很多网站在网页加载时,就使用ajax来获取数据进行渲染。

    但是爬虫程序他没有渲染引擎啊,无法执行js,所以只能呆呆地获取后台返回的原始html。我们在浏览器中看到的网页源码,才是没有经过js渲染的网页,也是我们爬虫最终获取的网页内容。

    原始网页

    如图,网页源码中也没有分类元素。至此,我们可以得出结论:开发者工具看到的是js渲染后的html,网页源码是原始的html

    这时候你应该有所考虑:我们解析网页是为了什么?获取数据!但网页中没有数据,所以我们就不需要请求这个网页的url了。我们只要找到js获取数据的url,直接请求这个url,数据不直接就有了么

    正常情况下,如何应对动态加载

    找接口的url

    在我看来,使用动态加载网页获取数据比普通网页简单的多,使用加密参数的除外。我们可以直接从接口获取json或者其他文本格式的数据,而不需要解析网页。我们的爬虫开发也直接从面向网页变成了面向数据。我们首先要做的就是找接口的url。

    如何找到接口url?

    1. 打开开发者工具,刷新页面,搜索关键字

    根据返回数据中的关键字搜索,如图,我们根据"白酒"找到了对应的响应内容。这里先看看返回的内容,这里记住BKCodeBkname两个字段。

    1. 查看url,构造参数

    我们来查看此响应的请求。如图,我们找到了url,并且有两个请求参数。

    根据请求和响应来看,这个是一个JSONP的请求。这类请求的规律是:url中的callback由一个方法名+时间戳组成,_参数也是一个时间戳;响应内容格式为callback(json)。如果用兴趣可以去了解一下JSONP,如果单纯获取数据只要了解他的规律即可。

    第二层:解析列表页

    1. 我们点击进入"电子信息"的基金列表页,如图

    2. 按照分类页面请求的方法,你会发现这个也是一个jsonp接口返回的数据,同样,来寻找接口url。

    这里主要关注FCODE字段。从列表页发现,一页是十个基金,需要翻页,所以在响应数据中末尾有TotalCount字段,用这个可以来计算一共有多少页。

    1. 查看请求参数

    这里的tp字段就是BKCode,pageIndex传入当前请求的页数。

    第三层:解析详情页

    进入一个基金详情页,你会发现这个页面就是传统的静态页面,使用css或者xpath直接解析即可。通过url你会发现,从列表页是通过Fcode字段来跳转到每个基金的详情页。

    程序开发

    从上面的分析来看,分类页和列表页是动态加载,返回内容是类似于json的jsonp文本,我们可以去掉多余的部分,直接用json解析。详情页是静态页面,用xpath即可。

    代码开发

    import requests
    import time
    import datetime
    import json
    import pymysql
    from lxml.html import etree
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15'
        , 'Referer': 'http://fund.eastmoney.com'
    }
    
    # 初始化数据库连接
    connection = pymysql.connect(host='47.102.***.**', user='root', password='root', database='scrapy', port=3306, charset='utf8')
    cursor = connection.cursor()
    
    # 程序入口, 解析基金分类
    def start_requests():
        timestamp = int(time.time() * 1000)
        callback = 'jQuery18306789193760800711_' + str(timestamp)
        start_url = f'http://fundtest.eastmoney.com/dataapi1015/ztjj//GetBKListByBKType?callback={callback}&_={timestamp}'
        response = requests.get(start_url, headers=headers)
        # 将分类返回的数据掐头去尾,格式化成json
        result = response.text.replace(callback, '')
        result = result[1: result.rfind(')')]
        data = json.loads(result)
        # 遍历行业分类数据,获取名称和代号
        for item in data['Data']['hy'] :
            time.sleep(3)
            code = item['BKCode']
            category = item['BKName']
            print(code, category)
            parseFundList(code, category)
        # 遍历概念分类数据
        for item in data['Data']['gn']:
            time.sleep(3)
            code = item['BKCode']
            category = item['BKName']
            print(code, category)
            parseFundList(code, category)
    
    # 解析每个分类下的基金列表
    def parseFundList(code, category):
        timestamp = int(time.time() * 1000)
        callback = 'jQuery1830316287740290561061_' + str(timestamp)
        index = 1
        url = f'http://fundtest.eastmoney.com/dataapi1015/ztjj/GetBKRelTopicFund?callback={callback}&sort=SON_1N&sorttype=DESC&pageindex={index}&pagesize=10&tp={code}&isbuy=1&_={timestamp}'
        response = requests.get(url, headers=headers)
        result = response.text.replace(callback, '')
        result = result[1: result.rfind(')')]
        data = json.loads(result)
        totalCount = data['TotalCount']
        # 先根据totalCount计算出总页数
        pages = int(int(totalCount) / 10) + 1
        # 解析出每页基金的FCode
        for index in range(1, pages + 1):
            timestamp = int(time.time() * 1000)
            callback = 'jQuery1830316287740290561061_' + str(timestamp)
            url = f'http://fundtest.eastmoney.com/dataapi1015/ztjj/GetBKRelTopicFund?callback={callback}&sort=SON_1N&sorttype=DESC&pageindex={index}&pagesize=10&tp={code}&isbuy=1&_={timestamp}'
            response = requests.get(url, headers=headers)
            result = response.text.replace(callback, '')
            result = result[1: result.rfind(')')]
            data = json.loads(result)
            for item in data['Data']:
                time.sleep(3)
                fundCode = item['FCODE']
                fundName = item['SHORTNAME']
                parse_info(fundCode, fundName, category)
    
    
    def parse_info(fundCode, fundName, category):
        url = f'http://fund.eastmoney.com/{fundCode}.html'
        response = requests.get(url, headers=headers)
        content = response.text.encode('ISO-8859-1').decode('UTF-8')
        html = etree.HTML(content)
        worth = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[2]/dd[1]/span[1]/text()')
        if worth:
            worth = worth[0]
        else:
            worth = 0
        scope = html.xpath('//div[@class="infoOfFund"]/table/tr[1]/td[2]/text()')[0].replace(':', '')
        manager = html.xpath('//div[@class="infoOfFund"]/table/tr[1]/td[3]/a/text()')[0]
        create_time = html.xpath('//div[@class="infoOfFund"]/table/tr[2]/td[1]/text()')[0].replace(':', '')
        company = html.xpath('//div[@class="infoOfFund"]/table/tr[2]/td[2]/a/text()')[0]
        level = html.xpath('//div[@class="infoOfFund"]/table/tr[2]/td[3]/div/text()')
        if level:
            level = level[0]
        else:
            level = '暂无评级'
        month_1 = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[1]/dd[2]/span[2]/text()')
        month_3 = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[2]/dd[2]/span[2]/text()')
        month_6 = html.xpath('//*[@id="body"]/div[11]/div/div/div[3]/div[1]/div[1]/dl[3]/dd[2]/span[2]/text()')
        if month_1:
            month_1 = month_1[0]
        else:
            month_1 = ''
    
        if month_3:
            month_3 = month_3[0]
        else:
            month_3 = ''
    
        if month_6:
            month_6 = month_6[0]
        else:
            month_6 = ''
        print(fundName, fundCode, category, worth, scope, manager, create_time, company, level, month_1, month_3, month_6, sep='|')
        # 存储到mysql
        today = datetime.date.today()
        sql = f"insert into fund_info values('{today}', '{fundName}', '{fundCode}', '{category}', '{worth}', '{scope}', '{manager}', '{create_time}', '{company}', '{level}', '{month_1}', '{month_3}', '{month_6}')"
        cursor.execute(sql)
        connection.commit()
    # 开始爬取
    start_requests()
    

    声明: 以上代码仅限于学习使用,不得使用该程序对网站恶意请求造成破坏,否则后果自负。

    程序如上,在解析动态加载的数据的时候明显比解析网页显简单,因为数据字段规范,根本不用考虑字段缺失的问题,而解析网页就会有各种各样的情况出现。

    其次,程序还有很多可以优化的部分。例如

    1. 可以将冗余代码重构成一个方法,这里为了直观都是逐行写的。
    2. 可以针对详情页不同结构多设置几种解析方式。
    3. 对详情页每个字段进行if为空的判断,然后设置缺省值,我这里只判断了三四个字段。

    数据库建表

    CREATE TABLE `fund_info` (
      `op_time` varchar(20) DEFAULT NULL,
      `fundName` varchar(20) DEFAULT NULL,
      `fundCode` varchar(20) DEFAULT NULL,
      `category` varchar(20) DEFAULT NULL,
      `worth` varchar(20) DEFAULT NULL,
      `scope` varchar(20) DEFAULT NULL,
      `manager` varchar(20) DEFAULT NULL,
      `create_time` varchar(20) DEFAULT NULL,
      `company` varchar(20) DEFAULT NULL,
      `level` varchar(20) DEFAULT NULL,
      `month_1` varchar(20) DEFAULT NULL,
      `month_3` varchar(20) DEFAULT NULL,
      `month_6` varchar(20) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    

    运行结果

    控制台输出:

    数据库查询:

    结语

    3月6日确定题目开始着手写,写完已经是3月14日。也深刻体会到开发容易描述不易。本篇文章从分析网站、到开发爬虫、存储数据,以及穿插了部分动态加载的知识,全方面的讲述了一个爬虫开发的全过程,希望对你有所启示。期待下一次相遇。



    写的都是日常工作中的亲身实践,置身自己的角度从0写到1,保证能够真正让大家看懂。

    文章会在公众号 [入门到放弃之路] 首发,期待你的关注。

    感谢每一份关注

  • 相关阅读:
    javaweb毕业设计
    Maven入门----MyEclipse创建maven项目(二)
    Maven入门----介绍及环境搭建(一)
    SpringMvc入门五----文件上传
    SpringMvc入门四----rest风格Url
    SpringMvc入门三----控制器
    SpringMvc入门二----HelloWorld
    SpringMvc入门一----介绍
    分析setup/hold电气特性从D触发器内部结构角度
    33. Search in Rotated Sorted Array
  • 原文地址:https://www.cnblogs.com/seven0007/p/scrapy06.html
Copyright © 2011-2022 走看看