文章摘要

主力资金动向是判断市场热点和板块轮动的核心指标,而同花顺的行业资金数据因其更新及时、覆盖全面,成为很多投资者的首选参考。但同花顺网页版采用了JS 动态加密的反爬机制,直接请求会返回空数据或被封禁 IP。

本文将详细拆解同花顺的核心反爬逻辑,通过py_mini_racer 执行 JS 代码破解hexin-v加密参数,实现全行业主力资金数据的自动爬取,并返回标准的 Pandas DataFrame 格式。文章包含完整的技术原理分析、修正后的可运行代码、效果展示和避坑指南,帮助你轻松获取实时主力资金数据。


1. 技术原理:同花顺反爬机制拆解

同花顺网页版的核心反爬手段是动态请求头加密,具体逻辑如下:

  1. 每次访问数据接口时,请求头中必须携带一个名为hexin-v的加密参数
  2. 该参数由服务器返回的ths.js文件中的v()函数动态生成,有效期约为几分钟
  3. 如果参数缺失或错误,服务器会直接拒绝请求或返回无效数据

我们的破解思路非常清晰:

  • 下载同花顺官方的ths.js加密文件
  • 使用 Python 的py_mini_racer库执行 JS 代码,调用v()函数生成合法的hexin-v参数
  • 携带该参数发起请求,即可正常获取数据

注:本文使用 akshare 内置的get_ths_js函数自动获取最新的ths.js文件,无需手动下载,大大降低了维护成本。


2. 前置环境准备

首先安装所需的 Python 依赖库:

pip install pandas requests beautifulsoup4 py_mini_racer akshare

各库作用说明:

  • pandas:数据处理与格式化,返回标准 DataFrame
  • requests:发起 HTTP 请求获取网页数据
  • beautifulsoup4:解析 HTML 页面,提取总页数
  • py_mini_racer:轻量级 JS 执行引擎,用于运行加密函数
  • akshare:提供最新的同花顺加密 JS 文件和进度条工具

3. 完整可运行代码

说明:以下代码已修正原文档中的所有语法错误和拼写错误,可直接复制运行。

from io import StringIO
import pandas as pd
import requests
from bs4 import BeautifulSoup
import py_mini_racer
from akshare.utils.tqdm import get_tqdm
from akshare.datasets import get_ths_js

# 设置Pandas显示选项,完整展示所有数据
pd.set_option('display.unicode.ambiguous_as_wide', True)
pd.set_option('display.unicode.east_asian_width', True)
pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
pd.set_option('display.expand_frame_repr', False)
pd.set_option('display.max_colwidth', 100)


def _get_file_content_ths(file: str = "ths.js") -> str:
    """
    获取同花顺加密JS文件的内容
    :param file: JS文件名,默认ths.js
    :return: JS文件内容字符串
    """
    setting_file_path = get_ths_js(file)
    with open(setting_file_path, encoding="utf-8") as f:
        file_data = f.read()
    return file_data


def get_industry_fund_flow() -> pd.DataFrame:
    """
    获取同花顺全行业主力资金流向数据
    :return: 包含行业资金数据的DataFrame
    """
    # 初始化JS执行环境
    js_code = py_mini_racer.MiniRacer()
    js_content = _get_file_content_ths("ths.js")
    js_code.eval(js_content)
    
    # 生成加密参数hexin-v
    v_code = js_code.call("v")
    
    # 第一次请求获取总页数
    headers = {
        "Accept": "text/html,*/*;q=0.01",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
        "hexin-v": v_code,
        "Host": "data.10jqka.com.cn",
        "Pragma": "no-cache",
        "Referer": "http://data.10jqka.com.cn/funds/hyzj1/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
    }
    
    base_url = "http://data.10jqka.com.cn/funds/hyzj1/field/tradezdf/order/desc/page/{}/"
    first_url = base_url.format(1)
    
    try:
        r = requests.get(first_url, headers=headers, timeout=10)
        r.raise_for_status()
    except Exception as e:
        print(f"请求失败: {e}")
        return pd.DataFrame()
    
    # 解析总页数
    soup = BeautifulSoup(r.text, features="lxml")
    page_info = soup.find(name="span", attrs={"class": "page_info"})
    if not page_info:
        print("未获取到总页数信息")
        return pd.DataFrame()
    
    page_num = int(page_info.text.split("/")[1])
    print(f"共发现 {page_num} 页数据,开始爬取...")
    
    big_df = pd.DataFrame()
    tqdm = get_tqdm()
    
    # 循环爬取所有页面
    for page in tqdm(range(1, page_num + 1), leave=False):
        # 每页重新生成加密参数,避免过期
        js_code = py_mini_racer.MiniRacer()
        js_content = _get_file_content_ths("ths.js")
        js_code.eval(js_content)
        v_code = js_code.call("v")
        
        headers["hexin-v"] = v_code
        current_url = base_url.format(page)
        
        try:
            r = requests.get(current_url, headers=headers, timeout=10)
            r.raise_for_status()
            temp_df = pd.read_html(StringIO(r.text))[0]
            big_df = pd.concat(objs=[big_df, temp_df], ignore_index=True)
        except Exception as e:
            print(f"第 {page} 页爬取失败: {e}")
            continue
    
    # 数据清洗:删除空列和无用列
    big_df = big_df.dropna(axis=1, how="all")
    if "序号" in big_df.columns:
        big_df = big_df.drop(columns=["序号"])
    
    print(f"爬取完成,共获取 {len(big_df)} 个行业的资金数据")
    return big_df


if __name__ == "__main__":
    # 调用函数获取数据
    fund_flow_df = get_industry_fund_flow()
    
    # 打印前15条数据
    if not fund_flow_df.empty:
        print("\n" + "="*120)
        print("同花顺全行业主力资金流向数据(前15条)")
        print("="*120)
        print(fund_flow_df.head(15))

4. 运行效果与字段说明

4.1 运行输出示例

共发现 18 页数据,开始爬取...
爬取完成,共获取 106 个行业的资金数据

========================================================================================================================
同花顺全行业主力资金流向数据(前15条)
========================================================================================================================
          行业  行业指数涨跌幅  流入资金(亿)  流出资金(亿)  净额(亿)  公司家数  领涨股  领涨股涨跌幅  当前价(元)
0      能源金属        4.65%     120.45     116.31     4.15      13  博迁新材      10.00%     65.44
1      工业金属        2.66%     250.00     228.76    21.24      58  盛屯矿业      10.02%     10.32
2        小金属        2.66%     123.82     113.10    10.73      26  锡业股份       9.98%     23.14
3        半导体        2.44%     921.94     743.91   178.03     163  江波龙       20.00%     99.87
4      军工电子        2.33%      71.26      54.34    16.92      62  高凌信息       13.90%     25.57
5      军工装备        2.30%     176.89     138.22    38.67      81  北摩高科       10.02%     29.21
6          电池        2.02%     468.60     452.12    16.48     101  派能科技       19.99%    174.24
7  其他电源设备        1.82%      84.94      90.19    -5.25      32  新雷能       20.00%     22.44
8    互联网电商        1.81%      17.83      17.20     0.64      19  星徽股份       20.00%      7.74
9      汽车整车        1.52%     135.88     109.91    25.97      23  众泰汽车        8.18%      3.57
10     医疗服务        1.51%      81.62      59.55    22.07      53  药明康德        6.42%    112.03
11     光伏设备        1.50%     221.57     202.22    19.35      75  固德威       12.68%     64.69
12     农化制品        1.32%      52.98      43.43     9.55      60  蓝丰生化       10.05%     10.40
13       贵金属        1.25%      62.41      57.27     5.14      11  中金黄金        3.83%     21.93
14   金属新材料        1.24%      41.85      35.88     5.97      32  铂科新材       12.44%     84.32

4.2 核心字段说明

字段名 说明
行业 同花顺行业分类名称
行业指数涨跌幅 该行业指数当日涨跌幅
流入资金 (亿) 当日主力资金流入总额(单位:亿元)
流出资金 (亿) 当日主力资金流出总额(单位:亿元)
净额 (亿) 当日主力资金净流入净额(流入 - 流出)
公司家数 该行业包含的上市公司数量
领涨股 当日该行业涨幅最大的股票
领涨股涨跌幅 领涨股当日涨跌幅
当前价 (元) 领涨股当日收盘价

5. 常见问题与避坑指南

5.1 代码运行提示 "未获取到总页数信息"

  • 原因:hexin-v参数过期或 JS 文件更新
  • 解决方案:
    1. 更新 akshare 到最新版本:pip install akshare --upgrade
    2. 检查网络连接,确保能正常访问同花顺网站
    3. 适当增加请求超时时间

5.2 爬取过程中部分页面失败

  • 原因:请求频率过高导致 IP 被临时封禁
  • 解决方案:
    1. 在循环中添加延时:time.sleep(1)
    2. 使用代理 IP 池轮换请求
    3. 降低并发请求数量

5.3 返回数据为空或字段不完整

  • 原因:同花顺更新了页面结构或接口地址
  • 解决方案:
    1. 检查base_url是否正确
    2. 用浏览器开发者工具查看最新的接口返回格式
    3. 调整pd.read_html的解析参数

5.4 JS 执行报错

  • 原因:ths.js文件内容发生变化,原加密函数失效
  • 解决方案:
    1. 更新 akshare 获取最新的 JS 文件
    2. 手动从同花顺官网下载最新的ths.js文件替换

6. 风险提示

  1. 合规性风险:本文提供的代码仅供个人学习和研究使用,不得用于商业用途。请严格遵守《网络安全法》和目标网站的robots.txt协议,不得恶意爬取或滥用数据。
  2. 反爬风险:同花顺会不定期更新反爬机制,包括加密算法、请求频率限制和 IP 封禁策略。代码可能随时失效,需根据实际情况调整。
  3. 数据准确性风险:爬取的数据仅供参考,不保证其准确性、完整性和时效性。投资决策请以官方渠道发布的数据为准。
  4. 投资风险:主力资金流向只是投资参考指标之一,不能作为唯一的投资依据。股市有风险,投资需谨慎。
  5. 代码使用风险:本文提供的代码已尽可能测试,但不保证无任何缺陷。使用者自行承担因代码问题导致的所有风险和损失。

7. 总结

本文通过 JS 逆向技术,成功破解了同花顺的hexin-v加密参数,实现了全行业主力资金数据的自动爬取。该方法具有以下优势:

  • 无需登录账号,无需购买付费接口
  • 数据更新及时,与网页版完全同步
  • 返回标准 DataFrame 格式,方便后续数据分析和量化策略开发
  • 代码结构清晰,易于维护和扩展

你可以基于此代码,进一步开发资金流向量化策略、板块轮动监控工具或实时预警系统。如果需要获取个股主力资金、北向资金或龙虎榜数据,也可以采用类似的方法进行破解。

更多推荐