查看: 363|回复: 0

小白也量化:用Python实时抓取雪球热股榜TOP20(含源码及界面版)

[复制链接]

1488

主题

96

回帖

4万

积分

管理员

积分
49633
发表于 2025-12-20 15:13:57 | 显示全部楼层 |阅读模式

最近有同学问怎么找各平台的热门股票榜单,正好最近我在研究东财实盘跟单与雪球组合跟单。
后面我分几期介绍一下,今天先讲雪球。 d9068b7b70a73cd440d6f588c7d6bfeb.png
行情波动时,我每天也会看雪球的热门榜。它是个不错的风向标,能看出市场在关注什么。
其实你可以用 Python 自己做一个实时更新的监控面板,操作很简单,新手也能搞定。
一、准备工具
需要2个 Python 库:
  • Requests:用来向雪球请求数据。
  • Pandas:用来处理数据。
安装命令:
pip install streamlit requests pandas
ebc13804460dcc169fb8218b88b6b874.png
二、获取数据权限
直接爬雪球数据会被拦截,因为服务器能识别出是脚本访问。解决办法是模拟浏览器行为,主要靠两样东西:
  • 1. 请求头:包含浏览器的基本信息。
  • 2. Cookie:相当于登录凭证,最关键。
具体获取步骤:
  • 1. 浏览器登录雪球账号。
  • 2. 按 F12 打开开发者工具,切换到“网络”(Network)面板。
  • 3. 刷新页面,找到对 xueqiu.com 的请求。
  • 4. 在“请求头”里找到 Cookie 字段,复制其中 u= 和 xq_a_token= 后面的值备用。 be09cc21123d1b2ca37085bcc55ed2cf.png
三、代码实现
主要分三步:
第1步:请求数据
用 requests 库带上请求头和 Cookie,获取热门榜的原始数据。
第2步:处理数据
用 pandas 整理数据,提取股票名称、代码、价格、涨跌幅、热度等字段,并调整格式。
第3步:展示界面
完整代码已整理好,你可以直接复制使用。运行命令:
streamlit run xueqiu.py 870054176e3ec3a5a23aaed36ae7ab2b.png

这样就能在本地浏览器看到一个实时更新的雪球热股监控面板了。
下期再介绍其他平台的数据获取方法。
界面版及PY源码阅读原文或通过网盘获取:雪球热门榜单.zip
链接: https://pan.baidu.com/s/1gaIzc5_XZ8H6WzIcxL4Dvg?pwd=zj41 提取码: zj41

  1. import os
  2. import threading
  3. import time
  4. import tkinter as tk
  5. from tkinter import ttk, messagebox
  6. import webbrowser
  7. import csv
  8. from datetime import datetime
  9. from typing import Any, Dict, List, Optional

  10. from xueqiu_client import (
  11.     XueqiuClient,
  12.     XueqiuCookies,
  13.     XueqiuAuthError,
  14.     extract_required_cookies,
  15.     load_config,
  16.     save_config,
  17. )


  18. APP_TITLE = '雪球热门榜单--仓鼠量化'
  19. CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'xueqiu_config.json')

  20. HOT_STOCK_TYPE_ID = 20


  21. COLUMNS = [
  22.     ('name', '名称', 180),
  23.     ('symbol', '代码', 110),
  24.     ('current', '现价', 90),
  25.     ('percent', '涨跌幅(%)', 90),
  26.     ('chg', '涨跌额', 90),
  27.     ('exchange', '交易所', 80),
  28.     ('value', '热度值', 110),
  29.     ('rank_change', '排名变化', 80),
  30. ]


  31. class XueqiuHotStockGUI:
  32.     def __init__(self, root: tk.Tk):
  33.         self.root = root
  34.         self.root.title(APP_TITLE)
  35.         self.root.geometry('1200x800')

  36.         self._auto_refresh = False
  37.         self._auto_after_id: Optional[str] = None
  38.         self._fetch_lock = threading.Lock()
  39.         self._last_items: List[Dict[str, Any]] = []

  40.         self._build_ui()
  41.         self._load_config_into_ui()

  42.         self._set_status('就绪')

  43.     def _build_ui(self) -> None:
  44.         top = tk.Frame(self.root)
  45.         top.pack(fill=tk.X, padx=10, pady=10)

  46.         title = tk.Label(top, text=APP_TITLE, font=('微软雅黑', 18, 'bold'))
  47.         title.pack(side=tk.LEFT)

  48.         self.status_var = tk.StringVar(value='')
  49.         status = tk.Label(top, textvariable=self.status_var, anchor=tk.E)
  50.         status.pack(side=tk.RIGHT, fill=tk.X, expand=True)

  51.         body = tk.Frame(self.root)
  52.         body.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))

  53.         self._build_left_panel(body)
  54.         self._build_table(body)

  55.     def _build_left_panel(self, parent: tk.Frame) -> None:
  56.         left = tk.Frame(parent, width=380)
  57.         left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
  58.         left.pack_propagate(False)

  59.         cookie_frame = tk.LabelFrame(left, text='Cookie设置', padx=10, pady=10)
  60.         cookie_frame.pack(fill=tk.X, pady=(0, 10))

  61.         tk.Label(cookie_frame, text='Cookie整段(可选):').pack(anchor=tk.W)
  62.         self.cookie_text = tk.Text(cookie_frame, height=5, width=40)
  63.         self.cookie_text.pack(fill=tk.X, pady=(5, 10))

  64.         row1 = tk.Frame(cookie_frame)
  65.         row1.pack(fill=tk.X)
  66.         tk.Label(row1, text='u:').pack(side=tk.LEFT)
  67.         self.u_var = tk.StringVar(value='')
  68.         tk.Entry(row1, textvariable=self.u_var).pack(side=tk.RIGHT, fill=tk.X, expand=True)

  69.         row2 = tk.Frame(cookie_frame)
  70.         row2.pack(fill=tk.X, pady=(6, 0))
  71.         tk.Label(row2, text='xq_a_token:').pack(side=tk.LEFT)
  72.         self.token_var = tk.StringVar(value='')
  73.         tk.Entry(row2, textvariable=self.token_var).pack(side=tk.RIGHT, fill=tk.X, expand=True)

  74.         btn_row = tk.Frame(cookie_frame)
  75.         btn_row.pack(fill=tk.X, pady=(10, 0))

  76.         tk.Button(btn_row, text='从整段Cookie解析', command=self.on_parse_cookie).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 6))
  77.         tk.Button(btn_row, text='保存配置', command=self.on_save_config).pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(6, 0))

  78.         action_frame = tk.LabelFrame(left, text='获取与刷新', padx=10, pady=10)
  79.         action_frame.pack(fill=tk.X, pady=(0, 10))

  80.         interval_row = tk.Frame(action_frame)
  81.         interval_row.pack(fill=tk.X, pady=(10, 0))
  82.         tk.Label(interval_row, text='自动刷新间隔(秒):').pack(side=tk.LEFT)
  83.         self.interval_var = tk.StringVar(value='30')
  84.         ttk.Spinbox(interval_row, from_=10, to=600, textvariable=self.interval_var, width=8).pack(side=tk.RIGHT)

  85.         btn_row2 = tk.Frame(action_frame)
  86.         btn_row2.pack(fill=tk.X, pady=(10, 0))

  87.         tk.Button(btn_row2, text='测试Cookie', command=self.on_test_cookie).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 6))
  88.         tk.Button(btn_row2, text='手动刷新', command=self.on_refresh).pack(side=tk.RIGHT, fill=tk.X, expand=True, padx=(6, 0))

  89.         btn_row3 = tk.Frame(action_frame)
  90.         btn_row3.pack(fill=tk.X, pady=(10, 0))
  91.         self.auto_btn = tk.Button(btn_row3, text='开始自动刷新', command=self.on_toggle_auto)
  92.         self.auto_btn.pack(fill=tk.X)

  93.         export_frame = tk.LabelFrame(left, text='导出', padx=10, pady=10)
  94.         export_frame.pack(fill=tk.X)
  95.         tk.Button(export_frame, text='导出CSV', command=self.on_export_csv).pack(fill=tk.X)

  96.     def _build_table(self, parent: tk.Frame) -> None:
  97.         right = tk.Frame(parent)
  98.         right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

  99.         table_frame = tk.Frame(right)
  100.         table_frame.pack(fill=tk.BOTH, expand=True)

  101.         self.tree = ttk.Treeview(table_frame, columns=[c[0] for c in COLUMNS], show='headings')
  102.         vsb = ttk.Scrollbar(table_frame, orient='vertical', command=self.tree.yview)
  103.         hsb = ttk.Scrollbar(table_frame, orient='horizontal', command=self.tree.xview)
  104.         self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)

  105.         self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
  106.         vsb.pack(side=tk.RIGHT, fill=tk.Y)
  107.         hsb.pack(side=tk.BOTTOM, fill=tk.X)

  108.         for key, title, width in COLUMNS:
  109.             self.tree.heading(key, text=title)
  110.             self.tree.column(key, width=width, anchor=tk.W)

  111.         self.tree.bind('<Double-1>', self.on_open_selected)

  112.         menu = tk.Menu(self.root, tearoff=0)
  113.         menu.add_command(label='打开雪球链接', command=self.on_open_selected)
  114.         menu.add_command(label='复制代码', command=self.on_copy_symbol)
  115.         self._context_menu = menu

  116.         self.tree.bind('<Button-3>', self._show_context_menu)

  117.     def _show_context_menu(self, event) -> None:
  118.         item_id = self.tree.identify_row(event.y)
  119.         if item_id:
  120.             self.tree.selection_set(item_id)
  121.             self._context_menu.tk_popup(event.x_root, event.y_root)

  122.     def _set_status(self, msg: str) -> None:
  123.         ts = datetime.now().strftime('%H:%M:%S')
  124.         self.status_var.set(f'{ts} - {msg}')

  125.     def _get_client(self) -> XueqiuClient:
  126.         u = (self.u_var.get() or '').strip()
  127.         token = (self.token_var.get() or '').strip()
  128.         if not u or not token:
  129.             raise ValueError('请先填写 u 和 xq_a_token,或粘贴整段Cookie后解析')
  130.         return XueqiuClient(XueqiuCookies(u=u, xq_a_token=token))

  131.     def on_parse_cookie(self) -> None:
  132.         cookie_string = self.cookie_text.get('1.0', tk.END).strip()
  133.         u, token = extract_required_cookies(cookie_string)
  134.         if u:
  135.             self.u_var.set(u)
  136.         if token:
  137.             self.token_var.set(token)

  138.         if u and token:
  139.             self._set_status('已从整段Cookie解析成功')
  140.         else:
  141.             messagebox.showwarning('提示', '未能解析到 u 或 xq_a_token,请确认Cookie内容')

  142.     def _load_config_into_ui(self) -> None:
  143.         cfg = load_config(CONFIG_FILE)
  144.         if not isinstance(cfg, dict):
  145.             return

  146.         self.u_var.set(str(cfg.get('u', '') or ''))
  147.         self.token_var.set(str(cfg.get('xq_a_token', '') or ''))
  148.         self.interval_var.set(str(cfg.get('interval_seconds', '30') or '30'))

  149.     def on_save_config(self) -> None:
  150.         data = {
  151.             'u': (self.u_var.get() or '').strip(),
  152.             'xq_a_token': (self.token_var.get() or '').strip(),
  153.             'interval_seconds': int((self.interval_var.get() or '30').strip() or '30'),
  154.         }
  155.         save_config(CONFIG_FILE, data)
  156.         self._set_status('配置已保存')

  157.     def on_test_cookie(self) -> None:
  158.         def worker():
  159.             try:
  160.                 client = self._get_client()
  161.                 start = time.time()
  162.                 items = client.get_hot_stocks(type_id=HOT_STOCK_TYPE_ID, page=1, size=10)
  163.                 cost = int((time.time() - start) * 1000)
  164.                 self.root.after(0, lambda: self._set_status(f'Cookie有效,获取到 {len(items)} 条 (耗时 {cost}ms)'))
  165.             except XueqiuAuthError as e:
  166.                 self.root.after(0, lambda: self._on_cookie_expired(str(e)))
  167.             except Exception as e:
  168.                 self.root.after(0, lambda: messagebox.showerror('错误', str(e)))

  169.         threading.Thread(target=worker, daemon=True).start()

  170.     def _on_cookie_expired(self, msg: str) -> None:
  171.         self._set_status('Cookie疑似失效')
  172.         messagebox.showwarning('Cookie可能已失效', msg)

  173.     def on_refresh(self) -> None:
  174.         if not self._fetch_lock.acquire(blocking=False):
  175.             return

  176.         self._set_status('正在获取数据...')

  177.         def worker():
  178.             try:
  179.                 client = self._get_client()
  180.                 start = time.time()
  181.                 items = client.get_hot_stocks(type_id=HOT_STOCK_TYPE_ID, page=1, size=100)
  182.                 cost = int((time.time() - start) * 1000)
  183.                 self.root.after(0, lambda: self._update_table(items, cost))
  184.             except XueqiuAuthError as e:
  185.                 self.root.after(0, lambda: self._on_cookie_expired(str(e)))
  186.             except Exception as e:
  187.                 self.root.after(0, lambda: messagebox.showerror('错误', str(e)))
  188.                 self.root.after(0, lambda: self._set_status('获取失败'))
  189.             finally:
  190.                 self._fetch_lock.release()

  191.         threading.Thread(target=worker, daemon=True).start()

  192.     def _update_table(self, items: List[Dict[str, Any]], cost_ms: int) -> None:
  193.         self._last_items = items
  194.         for iid in self.tree.get_children():
  195.             self.tree.delete(iid)

  196.         for item in items:
  197.             values = []
  198.             for key, _title, _w in COLUMNS:
  199.                 v = item.get(key)
  200.                 if v is None:
  201.                     values.append('')
  202.                 else:
  203.                     values.append(str(v))
  204.             self.tree.insert('', tk.END, values=values)

  205.         self._set_status(f'已更新 {len(items)} 条 (耗时 {cost_ms}ms)')

  206.     def on_toggle_auto(self) -> None:
  207.         if not self._auto_refresh:
  208.             self._auto_refresh = True
  209.             self.auto_btn.config(text='停止自动刷新')
  210.             self._schedule_next_refresh(0)
  211.             self._set_status('自动刷新已启动')
  212.         else:
  213.             self._auto_refresh = False
  214.             self.auto_btn.config(text='开始自动刷新')
  215.             if self._auto_after_id:
  216.                 self.root.after_cancel(self._auto_after_id)
  217.                 self._auto_after_id = None
  218.             self._set_status('自动刷新已停止')

  219.     def _schedule_next_refresh(self, delay_ms: int) -> None:
  220.         if not self._auto_refresh:
  221.             return
  222.         self._auto_after_id = self.root.after(delay_ms, self._auto_tick)

  223.     def _auto_tick(self) -> None:
  224.         if not self._auto_refresh:
  225.             return

  226.         try:
  227.             interval = int((self.interval_var.get() or '30').strip() or '30')
  228.         except Exception:
  229.             interval = 30

  230.         self.on_refresh()
  231.         self._schedule_next_refresh(interval * 1000)

  232.     def _get_selected_symbol(self) -> Optional[str]:
  233.         sel = self.tree.selection()
  234.         if not sel:
  235.             return None
  236.         values = self.tree.item(sel[0], 'values')
  237.         if not values:
  238.             return None
  239.         symbol_idx = next((i for i, c in enumerate(COLUMNS) if c[0] == 'symbol'), None)
  240.         if symbol_idx is None:
  241.             return None
  242.         if symbol_idx >= len(values):
  243.             return None
  244.         return str(values[symbol_idx])

  245.     def on_open_selected(self, _event=None) -> None:
  246.         symbol = self._get_selected_symbol()
  247.         if not symbol:
  248.             return
  249.         url = f'https://xueqiu.com/S/{symbol}'
  250.         webbrowser.open(url)

  251.     def on_copy_symbol(self) -> None:
  252.         symbol = self._get_selected_symbol()
  253.         if not symbol:
  254.             return
  255.         self.root.clipboard_clear()
  256.         self.root.clipboard_append(symbol)
  257.         self.root.update()
  258.         self._set_status('已复制代码到剪贴板')

  259.     def on_export_csv(self) -> None:
  260.         if not self._last_items:
  261.             messagebox.showinfo('提示', '没有数据可导出,请先获取数据')
  262.             return

  263.         from tkinter import filedialog

  264.         filename = filedialog.asksaveasfilename(defaultextension='.csv', filetypes=[('CSV文件', '*.csv')])
  265.         if not filename:
  266.             return

  267.         try:
  268.             with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
  269.                 writer = csv.writer(f)
  270.                 writer.writerow([c[1] for c in COLUMNS] + ['链接'])
  271.                 for item in self._last_items:
  272.                     row = [item.get(c[0], '') for c in COLUMNS]
  273.                     symbol = item.get('symbol', '')
  274.                     row.append(f'https://xueqiu.com/S/{symbol}' if symbol else '')
  275.                     writer.writerow(row)
  276.             self._set_status(f'已导出: {filename}')
  277.         except Exception as e:
  278.             messagebox.showerror('错误', f'导出失败: {e}')


  279. def main() -> None:
  280.     root = tk.Tk()
  281.     app = XueqiuHotStockGUI(root)
  282.     root.mainloop()


  283. if __name__ == '__main__':
  284.     main()
复制代码



雪球PY.txt

13.33 KB, 下载次数: 81

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

指标评测

股指标

建议反馈

常见问题

股指标评测

商务合作

新闻媒体

量化投资研究社

联系我们

微信:ZBPC88

备用微信:cqcangshu

邮箱:1099750285@qq.com

关注微信公众号

QQ|手机版|小黑屋|股指标网 ( 渝ICP备2024026571号-1 )

GMT+8, 2026-2-4 10:35 , Processed in 0.133748 second(s), 31 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.