社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

我用Python爬取800只基金数据,发现……

Python之禅 • 3 年前 • 488 次点击  

作者:数据科学家联盟

来源:知乎


以下观点仅供交流讨论,不作为投资建议


牛跑了熊来了,最近基金股票可谓一片绿油油,与其听取别人哪个基金更好,不如自己爬一爬数据一探究竟。整理了python爬数据的方法,希望对大家有用。


01

本文涉及到的知识点

1、python字符串:分割、拼接、中文字符判断;
2、python正则表达式;
3、爬虫requests请求库、xpath获取数据、代理服务器;
4、selenium用法:无头浏览器、元素定位、显式等待、数据获取;
5、python操作mongodb

02

网站分析

代码和数据我们到后面再贴上,先来分析下目标网站,这样有利于我们爬取过程更加清晰

目标网站:开放式基金排行 _ 天天基金网
我们爬取的就是【开放式基金】里的数据:
我们随便点开一个基金,就可以进入其详情页面,不知道你发现没有,该基金详情页面的url就是首页该基金的基金代码和 http://fund.eastmoney.com/ 的一个组合

比如:
040011 --- 华安核心优选混合的url:华安核心优选混合(040011)基金净值_估值_行情走势-天天基金网
005660 --- 嘉实资源精选股票A的url:嘉实资源精选股票A(005660)基金净值_估值_行情走势-天天基金网

ok,好,我们在基金详情页面往下拉就可以找到该基金的股票持仓信息,也就是该基金买了哪些股票:

然后点击 更多 进入该基金持股的详情页,往下拉就会看到,该基金三个季度的股票持仓信息:
这就是目标数据,要爬取的数据

我们先不爬取,再分析这个基金持仓的详情页,这个url也是有规律的,它是用 http://fundf10.eastmoney.com/ccmx_ 和该基金的基金代码组合成的
比如:
005660 ,嘉实资源精选股票A 的持仓详情页面url:嘉实资源精选股票A(005660)基金持仓 _ 基金档案 _ 天天基金网
006921,南方智诚混合 的持仓详情页面url:南方智诚混合(006921)基金持仓 _ 基金档案 _ 天天基金网

因为这些数据是用js动态加载的,如果使用requests爬取的话难度很大,这种情况下一般会使用selenium模拟浏览器行为进行爬取。但是selenium爬取的效率确实比较低

其实我们依旧是可以使用requests进行爬取的,js动态加载是html页面中的js代码执行了一段操作,从服务端自动加载了数据,所以数据在一开始爬取的页面上是看不到的,除非一些特别难爬的数据才需要selenium,因为selenium号称:只要是你看得到的数据就都可以获取。毕竟selenium是模仿人操作浏览器的行为的。这里我们分析js动态加载,然后利用requests来爬取,后面进行二次爬取的时候再用selenium

在首页按F12打开开发者工具,然后再刷新一下
可以看到右边蓝色框里的数据了吧,这是js动态加载之后返回的数据,然后经过加工后呈现在页面上的,其实只要获取这些数据就可以了,不用去爬取首页了

我们再点击 Headers ,这个 Request URL 就是js请求的url了,你可以试试把这个url直接用浏览器回车下,会给你返回一堆的数据;上面分析了基金持仓股票页面url的组成,所以只要需要这些数据里的六位基金代码就可以了

本篇代码中是用python正则进行了六位数字的提取,然后组成的基金持仓股票页面的url;然后再在基金持仓股票页面对该基金持有的股票进行爬取、存储

03

爬取流程

1、首先从首页中请求js动态加载数据时请求的那个url,从中获取六位数字的基金代码

然后 http://fundf10.eastmoney.com/ccmx_ + 基金代码 + .html 组成的基金持仓股票的详情页url

2、针对 基金持仓股票的详情页url 进行爬取,因为也是js动态加载的(加载速度较快),并且需要判断该基金是否有持仓的股票(有的基金没有买股票,也不知道他们干啥了),所以使用selenium来爬取,同时也使用了显式等待的方式来等待数据加载完成;

3、将数据整理,存储到mongodb中

04

代码讲解——数据爬取

这次我们将代码分段放上来,分段说明

需要的库:
import re
from lxml import etree
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import pymongo

准备一些常用的方法:
def is_contain_chinese(check_str):
    """
    判断字符串中是否包含中文
    :param check_str: {str} 需要检测的字符串
    :return: {bool} 包含返回True, 不包含返回False
    """

    for ch in check_str:
        if u'\u4e00' <= ch <= u'\u9fff':
            return True
    return False
#selenium通过class name判断元素是否存在,用于判断基金持仓股票详情页中该基金是否有持仓股票;
def is_element(driver,element_class):
    try:
        WebDriverWait(driver,2).until(EC.presence_of_element_located((By.CLASS_NAME,element_class)))
    except:
        return False
    else:
        return True
#requests请求url的方法,处理后返回text文本
def get_one_page(url):
    headers = {
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',
    }
    proxies = {
        "http""http://XXX.XXX.XXX.XXX:XXXX"
    }
 
    response = requests.get(url,headers=headers,proxies=proxies)
    response.encoding = 'utf-8'
    if response.status_code == 200:
        return response.text
    else:
        print("请求状态码 != 200,url错误.")
        return None
#该方法直接将首页的数据请求、返回、处理,组成持仓信息url和股票名字并存储到数组中;
def page_url():
    stock_url = []      #定义一个数组,存储基金持仓股票详情页面的url
    stock_name = []     #定义一个数组,存储基金的名称
    url = "http://fund.eastmoney.com/data/rankhandler.aspx?op=ph&dt=kf&ft=all&rs=&gs=0&sc=zzf&st=desc&sd=2018-11-26&ed=2019-11-26&qdii=&tabSubtype=,,,,,&pi=1&pn=10000&dx=1&v=0.234190661250681"
    result_text = get_one_page(url)
    # print(result_text.replace('\"',','))    #将"替换为,
    # print(result_text.replace('\"',',').split(','))    #以,为分割
    # print(re.findall(r"\d{6}",result_text))     #输出股票的6位代码返回数组;
    for i in result_text.replace('\"',',').split(','):  #将"替换为,再以,进行分割,遍历筛选出含有中文的字符(股票的名字)
        result_chinese = is_contain_chinese(i)
        if result_chinese == True:
            stock_name.append(i)
    for numbers in re.findall(r"\d{6}",result_text):
        stock_url.append("http://fundf10.eastmoney.com/ccmx_%s.html" % (numbers))    #将拼接后的url存入列表;
    return stock_url,stock_name
#selenium请求[基金持仓股票详情页面url]的方法,爬取基金的持仓股票名称;
def hold_a_position(url):
    driver.get(url)  # 请求基金持仓的信息
    element_result = is_element(driver, "tol")  # 是否存在这个元素,用于判断是否有持仓信息;
    if element_result == True:  # 如果有持仓信息则爬取;
        wait = WebDriverWait(driver, 3)  # 设置一个等待时间
        input = wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'tol')))  # 等待这个class的出现;
        ccmx_page = driver.page_source  # 获取页面的源码
        ccmx_xpath = etree.HTML(ccmx_page)  # 转换成成 xpath 格式
        ccmx_result = ccmx_xpath.xpath("//div[@class='txt_cont']//div[@id='cctable']//div[@class='box'][1]//td[3]//text()")
        return ccmx_result
    else:   #如果没有持仓信息,则返回null字符;
        return "null"

注意 page_url() 方法,里面的url就是上面分析js动态加载数据时请求的url,需要注意的是该url后面的参数,pi是第几页,pn是每页多少条数据

我这里pi=1,pn=10000,意思就是第一页,显示10000条数据(实际数据肯定没这么多,首页才5000+),就一次性的显示出所有的数据了;

程序开始:
if __name__ == '__main__':
    # 创建连接mongodb数据库
    client = pymongo.MongoClient(host='XXX.XXX.XXX.XXX', port=XXXXX)  # 连接mongodb,host是ip,port是端口
    db = client.db_spider  # 使用(创建)数据库
    db.authenticate("用户名""密码")  # mongodb的用户名、密码连接;
    collection = db.tb_stock  # 使用(创建)一个集合(表)
 
    stock_url, stock_name = page_url()     #获取首页数据,返回基金url的数组和基金名称的数组;
 
    #浏览器动作
    chrome_options = Options()
    chrome_options.add_argument('--headless')
    driver = webdriver.Chrome(options=chrome_options)    #初始化浏览器,无浏览器界面的;
 
    if len(stock_url) == len(stock_name):       #判断获取的基金url和基金名称数量是否一致
        for i in range(len(stock_url)):
            return_result = hold_a_position(stock_url[i])  # 遍历持仓信息,返回持仓股票的名称---数组
            dic_data = {
                'fund_url':stock_url[i],
                'fund_name':stock_name[i],
                'stock_name':return_result
            }        #dic_data 为组成的字典数据,为存储到mongodb中做准备;
            print(dic_data)
            collection.insert_one(dic_data)     #将dic_data插入mongodb数据库
    else:
        print("基金url和基金name数组数量不一致,退出。")
        exit()
 
    driver.close()    #关闭浏览器
 
    #查询:过滤出非null的数据
    find_stock = collection.find({'stock_name': {'$ne''null'}})  # 查询 stock_name 不等于 null 的数据(排除那些没有持仓股票的基金机构);
    for i in find_stock:
        print(i)   

好,至此,爬取数据的代码交代完毕,运行后坐等即可

该项目单进程运行,所以爬取速度略慢,同时也受网速影响,后期会继续改进成多线程。

05

代码讲解——数据处理

上面已经把数据爬取并存储到数据库中,这里对数据进行处理,将其变成可用的;
首先说明思路:

1、我们需要知道这些基金所有持仓的股票的综合数据,也包括基金持仓中有重复的股票

2、需要知道哪些股票重复了,有多少个重复的,重复了多少次
这样,重复数最多的那只股票就肯定是最好的了,因为这证明有很多的基金都购买了这支股票

具体看代码,注释说的已经很清楚了:
import pymongo
 
#一、数据库:连接库、使用集合、创建文档;#
client = pymongo.MongoClient(host='XXX.XXX.XXX.XXX',port=XXXXX)  #连接mongodb数据库
 
db = client.db_spider       #使用(创建)数据库
db.authenticate("用户名","密码")      #认证用户名、密码
 
collection = db.tb_stock    #使用(创建)一个集合(表),里面已经存储着上面程序爬取的数据了;
tb_result = db.tb_data      #使用(创建)一个集合(表),用于存储最后处理完毕的数据;
 
#查询 stock_name 不等于 null 的数据,即:排除那些没有持仓股票的基金;
find_stock = collection.find({'stock_name':{'$ne':'null'}})
 
#二、处理数据,将所有的股票数组累加成一个数组---list_stock_all #
list_stock_all = []     #定义一个数组,存储所有的股票名称,包括重复的;
for i in find_stock:
    print(i['stock_name'])    #输出基金的持仓股票(类型为数组)
    list_stock_all = list_stock_all + i['stock_name']   #综合所有的股票数组为一个数组;
print("股票总数:" + str(len(list_stock_all)))
 
#三、处理数据,股票去重#
list_stock_repetition = []  #定义一个数组,存放去重之后的股票
for n in list_stock_all:
    if n not in list_stock_repetition:        #如果不存在
        list_stock_repetition.append(n)        #则添加进该数组,去重;
print("去重后的股票数量:" + str(len(list_stock_repetition)))
 
#四、综合二、三中的得出的两个数组进行数据筛选#
for u in list_stock_repetition:        #遍历去重后股票的数组
    if list_stock_all.count(u) > 10:   #在未去重股票的数组中查找股票的重复数,如果重复数大于10
        #将数据组成字典,用于存储到mongodb中;
        data_stock = {
            "name":u,
            "numbers":list_stock_all.count(u)
        }
        insert_result = tb_result.insert_one(data_stock)    #存储至mongodb中
        print("股票名称:" + u + " , 重复数:" + str(list_stock_all.count(u)))

这样,就将数据稍微处理了一下存入了 tb_data 的集合中

下面只披露部分处理的数据:
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62c9'), 'name''水晶光电''numbers'61}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62ca'), 'name''老百姓''numbers'77}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62cb'), 'name''北方华创''numbers'52}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62cc'), 'name''金风科技''numbers'84}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62cd'), 'name''天顺风能''numbers'39}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62ce'), 'name''石大胜华''numbers'13}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62cf'), 'name''国投电力''numbers'55}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62d0'), 'name''中国石化''numbers'99}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62d1'), 'name''中国石油''numbers'54}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62d2'), 'name''中国平安''numbers'1517}
{'_id': ObjectId('5e0b5ecc7479db5ac2ec62d3'), 'name''贵州茅台''numbers'1573}
{'_id': ObjectId( '5e0b5ecc7479db5ac2ec62d4'), 'name''招商银行''numbers'910}

该数据还未做排序,排名不分先后

上面展示的数据中:
中国石化 的numbers是54,说明在5000+家的基金中有54家买了中国石化的股票

招商银行的numbers为910,说明在5000+家的基金中有910家基金买了招商银行的股票,已经相当高了

06

总结

数据的获取主要采用了爬虫的基本方法,使用的是 requests 库。而数据的解析和保存主要运用的是正则表达式、xpath解析库以及 pandas 数据处理库

对于一个基金的分析远远不止于这些数据(例如持仓分布,基金经理信息等),这里只是做个引子,毕竟自己用数据来选基金比听别人推荐买什么基金要好上一些,希望能给大家一个思路
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/110186
 
488 次点击