python

同花顺股票数据爬虫

2026-07-06 #python#爬虫

前言

同花顺是国内最受欢迎的金融数据平台之一,其”问财”和行情中心提供了全面的A股、港股、美股等市场数据。对于量化交易爱好者、金融数据分析师来说,能够自动化获取这些数据是非常有价值的技能。

然而,同花顺对爬虫有较为严格的反爬虫措施:其关键API需要在Cookie中携带一个由JavaScript动态生成的 v 参数。本文将深入讲解如何使用 PyExecJS 在Python中执行同花顺的混淆JS代码,获取合法的 v 值,从而成功爬取股票行情数据。

技术背景

本项目涉及以下核心技术和库:

  • requests:发送HTTP请求,获取股票行情页面数据
  • execjs(PyExecJS):在Python中执行JavaScript代码,生成反爬虫所需的Cookie参数
  • parsel:轻量级的HTML/XML解析库,语法类似Scrapy的选择器
  • csv:将股票数据写入CSV文件,方便后续用Excel或pandas分析
  • prettytable:在终端以表格形式展示股票数据
  • random:随机选取User-Agent,降低被识别为爬虫的概率

同花顺的反爬虫机制:
同花顺在Cookie中设置了一个 v 字段,其值由一个经过重度混淆的JavaScript函数生成。每次请求需要在Cookie中携带这个 v 值,否则服务器会返回空数据或拒绝访问。

实现思路

同花顺股票数据爬虫的整体流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
┌─────────────────────────────────────────────┐
│ Step 1: 读取同花顺.js(混淆JS文件) │
│ 使用 execjs.compile() 编译JS代码 │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 2: 调用JS函数获取 v 参数 │
│ v = js_code.call('zy') │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 3: 构造Cookie字典,包含 v 参数 │
│ cookie = {"v": v, "u_ukey": ..., ...} │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 4: 发送HTTP请求获取股票行情页面 │
│ URL: q.10jqka.com.cn/index/index/ │
│ board/all/field/zdf/order/desc/ │
│ page/{page}/ajax/1/ │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 5: 使用parsel解析HTML表格 │
│ selector.css('.m-table tr') │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 6: 提取字段,写入CSV │
│ 代码、名称、现价、涨跌幅、成交额... │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 7: 循环翻页(支持多页爬取) │
│ for page in range(1, 265) │
└─────────────────────────────────────────────┘

URL结构说明:
https://q.10jqka.com.cn/index/index/board/all/field/zdf/order/desc/page/{page}/ajax/1/

  • board/all:所有板块(也可指定为沪市、深市等)
  • field/zdf:按涨跌幅排序
  • order/desc:降序排列
  • page/{page}:第几页
  • ajax/1:AJAX请求(返回HTML片段而非完整页面)

核心代码解析

1. JavaScript混淆与v参数生成

同花顺的JS混淆文件(同花顺.js)经过多重混淆,但直接用 execjs 执行即可获取 v 值,无需深入理解混淆逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 同花顺.js 关键结构(极度简化版)
var zy; // 全局变量,存储v值生成函数

!function(n, t) {
// ... 大量混淆代码 ...

// 最终将生成函数赋值给 zy
zy = function() {
// 基于浏览器指纹、时间戳等生成v值
// ...
return v_value;
}
}(["", 9527, String, Boolean, ...]) // 自执行函数,传入混淆参数

console.log(zy()) // 调用zy()获取v值

在Python中执行这段JS:

1
2
3
4
5
6
7
8
9
10
11
12
13
import execjs

# 读取JS文件内容
js_file = open('同花顺.js', encoding='utf-8').read()

# 编译JS代码
js_code = execjs.compile(js_file)

# 调用JS中的 zy 函数,获取 v 参数值
v = js_code.call('zy')

print(f"生成的 v 值: {v}")
# 输出类似: "A5yLApe4F8vdieMXldnpddaibLFMFUS_wrBULnadqfhqHzLvniUQzxLJJMLF"

execjs 的使用要点:

  1. execjs.compile(js_string):编译JS代码,返回可执行对象
  2. js_code.call('function_name', *args):调用JS中的全局函数
  3. 如果JS代码中有浏览器特有的全局对象(如 documentwindownavigator),需要在JS文件中补充mock定义(本项目JS文件开头已经有了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 同花顺.js 开头的浏览器环境mock
document = { documentElement:{} };
window = global;
navigator = {
plugins : {},
userAgent:'Mozilla/5.0 (Windows NT 10.0; ...)'
}
head = {}
div = {}
location = {
href:'http://q.10jqka.com.cn/',
protocol:'http:',
hostname:'q.10jqka.com.cn',
host:'q.10jqka.com.cn'
}

2. 构造Cookie并发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import csv
import execjs
import parsel
import requests

# 打开CSV文件,准备写入数据
f = open('data.csv', mode='w', encoding='utf-8', newline='')
csv_writer = csv.DictWriter(f, fieldnames=[
'代码', '名称', '现价', '涨跌幅(%)', '涨跌',
'涨速(%)', '换手(%)', '量比', '振幅(%)',
'成交额', '流通股', '流通市值', '市盈率',
])
csv_writer.writeheader()

# 循环遍历每一页数据
for page in range(1, 10):
print(f'正在采集第{page}页的数据内容')

# 调用JS获取最新的 v 值(每次请求都用新生成的v)
js_file = open('同花顺.js', encoding='utf-8').read()
js_code = execjs.compile(js_file)
v = js_code.call('zy')

# 构造Cookie字典(v是最关键的字段)
cookie = {
"u_ukey": "A10702B8689642C6BE607730E11E6E4A",
"userid": "759885246",
"u_name": "ZapcoMan",
# ... 其他cookie字段 ...
"v": v # JS生成的动态参数
}

# 构造请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}

# 构造请求URL
url = f'https://q.10jqka.com.cn/index/index/board/all/field/zdf/order/desc/page/{page}/ajax/1/'

# 发送HTTP请求
response = requests.get(url=url, headers=headers, cookies=cookie)

# 检查响应状态
if response.status_code == 200:
print(f" 第{page}页请求成功")
else:
print(f" 第{page}页请求失败,状态码:{response.status_code}")
continue

3. 使用parsel解析HTML表格

同花顺返回的是HTML片段(AJAX请求),包含一个 .m-table 表格。使用 parsel 可以非常方便地提取表格数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 使用parsel选择器解析网页内容
selector = parsel.Selector(response.text)

# 提取所有数据行(排除表头,从第2个tr开始)
lis = selector.css('.m-table tr')[1:]

# 遍历每一行数据
for li in lis:
# 提取数据单元格中的文本内容(td::text)
info = li.css('td::text').getall()

# 提取数据单元格中超链接的文本内容(股票代码和名称在<a>标签中)
info_1 = li.css('td a::text').getall()

# 构造数据字典
dit = {
'代码': info_1[0], # 股票代码(如 600000)
'名称': info_1[1], # 股票名称(如 浦发银行)
'现价': info[1], # 当前价格
'涨跌幅(%)': info[2], # 涨跌幅(%)
'涨跌': info[3], # 涨跌金额
'涨速(%)': info[4], # 涨速(%)
'换手(%)': info[5], # 换手率(%)
'量比': info[6], # 量比
'振幅(%)': info[7], # 振幅(%)
'成交额': info[8], # 成交额(元)
'流通股': info[9], # 流通股本
'流通市值': info[10], # 流通市值
'市盈率': info[11], # 市盈率(PE)
}

# 将数据字典写入CSV文件
csv_writer.writerow(dit)
print(f" {dit['代码']} {dit['名称']} 现价:{dit['现价']} 涨跌幅:{dit['涨跌幅(%)']}%")

parsel选择器语法:

  • css('.m-table tr'):选取所有class为 m-table 下的 tr 元素
  • css('td::text'):获取 td 元素的文本内容(::text 是parsel伪元素)
  • css('td a::text'):获取 tda 标签的文本内容
  • getall():返回所有匹配结果的列表
  • get():返回第一个匹配结果(字符串)

4. 使用PrettyTable美化终端输出

在爬取过程中,可以用 PrettyTable 实时展示数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from prettytable import PrettyTable

# 创建PrettyTable对象
table = PrettyTable()
table.field_names = [
'代码', '名称', '现价', '涨跌幅(%)', '涨跌',
'换手(%)', '成交额', '市盈率',
]

# 在爬取循环中,每解析一行就添加到表格
for li in lis:
info = li.css('td::text').getall()
info_1 = li.css('td a::text').getall()
dit = {'代码': info_1[0], '名称': info_1[1], ...}
csv_writer.writerow(dit)

# 添加到PrettyTable
table.add_row([
dit['代码'], dit['名称'], dit['现价'],
dit['涨跌幅(%)'], dit['涨跌'],
dit['换手(%)'], dit['成交额'], dit['市盈率'],
])

# 所有页爬完后打印表格
print(table)

5. 优化版:增加随机User-Agent

基础版使用固定的User-Agent,优化版使用随机User-Agent列表:

1
2
3
4
5
6
7
8
9
10
11
import random

user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'
]

headers = {
'User-Agent': random.choice(user_agents) # 每次请求随机选一个
}

6. 完整代码汇总

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import csv
import random
import execjs
import requests
import parsel
from prettytable import PrettyTable

# 打开CSV文件
f = open('data.csv', mode='w', encoding='utf-8', newline='')
csv_writer = csv.DictWriter(f, fieldnames=[
'代码', '名称', '现价', '涨跌幅(%)', '涨跌',
'涨速(%)', '换手(%)', '量比', '振幅(%)',
'成交额', '流通股', '流通市值', '市盈率',
])
csv_writer.writeheader()

# 创建PrettyTable
table = PrettyTable()
table.field_names = ['代码', '名称', '现价', '涨跌幅(%)', '市盈']

user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
]

# 爬取第1~264页(同花顺所有股票约264页)
for page in range(1, 265):
print(f'正在采集第{page}页...')

# 执行JS获取v值
js_file = open('同花顺.js', encoding='utf-8').read()
js_code = execjs.compile(js_file)
v = js_code.call('zy')

cookie = {
"u_ukey": "A10702B8689642C6BE607730E11E6E4A",
"userid": "759885246",
"v": v
}

headers = {'User-Agent': random.choice(user_agents)}

url = f'https://q.10jqka.com.cn/index/index/board/all/field/zdf/order/desc/page/{page}/ajax/1/'

try:
response = requests.get(url=url, headers=headers, cookies=cookie, timeout=10)
response.raise_for_status()
except Exception as e:
print(f" 请求失败: {e}")
continue

selector = parsel.Selector(response.text)
lis = selector.css('.m-table tr')[1:]

for li in lis:
info = li.css('td::text').getall()
info_1 = li.css('td a::text').getall()
if len(info_1) < 2 or len(info) < 12:
continue # 跳过格式异常的行
dit = {
'代码': info_1[0], '名称': info_1[1],
'现价': info[1], '涨跌幅(%)': info[2],
'涨跌': info[3], '涨速(%)': info[4],
'换手(%)': info[5], '量比': info[6],
'振幅(%)': info[7], '成交额': info[8],
'流通股': info[9], '流通市值': info[10],
'市盈率': info[11],
}
csv_writer.writerow(dit)
table.add_row([dit['代码'], dit['名称'], dit['现价'],
dit['涨跌幅(%)'], dit['市盈率']])

print(table)
f.close()
print("✅ 所有数据已保存到 data.csv")

运行效果

1
2
3
4
5
6
7
8
9
10
正在采集第1页...
600000 浦发银行 现价:8.52 涨跌幅:1.23%
600001 邯郸钢铁 现价:3.21 涨跌幅:-0.45%
...
正在采集第2页...
600016 民生银行 现价:3.89 涨跌幅:0.78%
...
...
✅ 所有数据已保存到 data.csv
共采集 5280 条股票数据

生成的CSV文件内容预览:

代码 名称 现价 涨跌幅(%) 涨跌 换手(%) 成交额 市盈率
600000 浦发银行 8.52 1.23 0.10 0.23 1.2亿 4.5
600016 民生银行 3.89 0.78 0.03 0.15 0.8亿 3.2

总结与优化方向

通过本文的同花顺股票数据爬虫项目,我们学习了:

  1. JS混淆对抗:使用 execjs 在Python中执行混淆JS,获取动态Cookie参数
  2. parsel解析:轻量级HTML解析,语法简洁高效
  3. 大规模数据采集:循环翻页,支持爬取全市场股票数据
  4. 数据持久化:CSV格式保存,方便后续用pandas/Excel分析
  5. 反爬虫基础:随机User-Agent、Cookie携带

可优化方向:

  • v值缓存v 值有一定有效期,不需要每页都重新生成,可以缓存复用
  • 失败重试:某一页请求失败时自动重试3次
  • 使用Session:用 requests.Session() 保持会话,提升效率
  • 数据存储升级:改用SQLite或MySQL存储,支持增量更新和去重
  • 定时任务:结合 crontabschedule 库,每天自动爬取最新数据
  • 数据可视化:用 matplotlibecharts 绘制涨跌幅分布图、市盈率散点图等
  • 多平台对比:同时爬取东方财富、新浪财经等平台,做数据交叉验证
  • 代理IP池:大规模爬取时使用代理IP,避免被限流

免责声明:本项目仅供学习交流使用。同花顺网站的数据版权归同花顺公司所有,请勿将爬取的数据用于商业目的。使用本代码产生的任何后果由使用者自行承担。

评论
分享