首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >同花顺概念行业板块分析及成分股展示

同花顺概念行业板块分析及成分股展示

作者头像
子晓聊技术
发布2026-04-23 17:59:25
发布2026-04-23 17:59:25
1300
举报
文章被收录于专栏:子晓AI量化子晓AI量化

春节期间有好几个同学问我,怎么获取同花顺概念板块及成分股, 这里写一写。 把之前写的streamlit代码用pyside6重构下。

这个小工具,能够一键获取所有概念板块和行业板块的实时涨跌幅,点击板块即可查看成分股列表,并且涨跌幅用红绿颜色清晰标识,数据一目了然。提供整个代码,你可以自由修改、扩展,打造属于自己的看盘利器!

一、工具功能速览

启动工具后,你会看到一个简洁的双栏界面:

  • 左侧:通过标签页切换“概念板块”和“行业板块”,每个板块名称后都紧跟实时涨跌幅(红色代表上涨,绿色代表下跌),让你快速定位强势板块。
  • 右侧:点击左侧任一板块,右侧立即展示该板块的成分股列表,包含股票代码、名称、最新价和涨跌幅,同样用红绿颜色区分涨跌。

二、代码简单介绍

整个程序分为三大模块:

  • 数据模型(StockTableModel):负责管理股票表格数据及颜色渲染。
  • 工作线程(DataLoader):处理所有网络请求,通过信号与主线程通信。
  • 主窗口(MainWindow):搭建界面,连接信号槽,响应用户交互。

这种设计符合MVC(模型-视图-控制器)模式,如果你想增加新的数据维度(如换手率、市盈率),只需修改模型和对应的数据解析部分即可。

三、如何使用?

环境准备

确保你的Python版本≥3.7,然后安装依赖:

代码语言:javascript
复制
pip install pyside6 pandas requests pywencai
代码语言:javascript
复制
代码语言:javascript
复制
pywencai需要依赖node, 建议node18, 不知道怎么操作的可以翻一翻我之前的文章。

最后这里贴一下完整代码,参考下思路, 具体根据自己的实际情况改造。 备注:如果发现格式有多余的特殊字符,用普通浏览器打开复制应该没问题。 希望我的分享对大家有所帮助。

代码语言:javascript
复制
import sys
import json
import pandas as pd
import requests
import pywencai
from functools import lru_cache
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QTabWidget, QListWidget, QListWidgetItem, QTableView, QSplitter,
    QMessageBox, QStatusBar, QProgressBar, QHeaderView, QLabel
)
from PySide6.QtCore import Qt, QThread, Signal, QObject, QTimer, QAbstractTableModel, QModelIndex, QSize
from PySide6.QtGui import QFont, QColor, QBrush
# -------------------- 数据模型 --------------------
class StockTableModel(QAbstractTableModel):
    """成分股表格模型,涨跌幅列按正负显示颜色"""
    def __init__(self):
        super().__init__()
        self._data = pd.DataFrame(columns=['股票代码', '股票名称', '最新价', '涨跌幅'])
        self._headers = ['股票代码', '股票名称', '最新价', '涨跌幅']
    def update_data(self, df: pd.DataFrame):
        self.beginResetModel()
        self._data = df
        self.endResetModel()
    def rowCount(self, parent=QModelIndex()):
        return self._data.shape[0]
    def columnCount(self, parent=QModelIndex()):
        return self._data.shape[1]
    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        row, col = index.row(), index.column()
        value = self._data.iloc[row, col]
        if role == Qt.DisplayRole:
            if col == 3 and isinstance(value, (int, float)):
                return f"{value:.2f}%"
            return str(value)
        elif role == Qt.ForegroundRole:
            if col == 3:
                try:
                    if isinstance(value, str):
                        val_str = value.replace('%', '').strip()
                        val = float(val_str) if val_str else 0.0
                    else:
                        val = float(value)
                    if val > 0:
                        return QBrush(QColor(255, 80, 80))   # 红色
                    elif val < 0:
                        return QBrush(QColor(0, 150, 0))     # 绿色
                    else:
                        return QBrush(Qt.black)
                except:
                    return QBrush(Qt.gray)
            return QBrush(Qt.black)
        return None
    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self._headers[section]
        return None
# -------------------- 工作线程 --------------------
class DataLoader(QObject):
    concept_blocks_ready = Signal(pd.DataFrame)
    industry_blocks_ready = Signal(pd.DataFrame)
    stock_list_ready = Signal(pd.DataFrame, str)
    error_occurred = Signal(str)
    def __init__(self):
        super().__init__()
        self._cache_concept = None
        self._cache_industry = None
    @lru_cache(maxsize=128)
    def _fetch_concept_blocks(self):
        return pywencai.get(query="概念板块,涨跌幅排序", query_type="zhishu", sort_order='desc', loop=True)
    @lru_cache(maxsize=128)
    def _fetch_industry_blocks(self):
        return pywencai.get(query="行业板块,涨跌幅排序", query_type="zhishu", sort_order='desc', loop=True)
    def load_concept_blocks(self):
        try:
            df = self._fetch_concept_blocks()
            self.concept_blocks_ready.emit(df)
        except Exception as e:
            self.error_occurred.emit(f"获取概念板块失败: {str(e)}")
    def load_industry_blocks(self):
        try:
            df = self._fetch_industry_blocks()
            self.industry_blocks_ready.emit(df)
        except Exception as e:
            self.error_occurred.emit(f"获取行业板块失败: {str(e)}")
    def load_stock_list(self, block_code: str):
        try:
            url = f"https://d.10jqka.com.cn/v2/blockrank/{block_code}/199112/d1000.js"
            headers = {
                'Referer': 'http://q.10jqka.com.cn/',
                'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                               'AppleWebKit/537.36 (KHTML, like Gecko) '
                               'Chrome/119.0.0.0 Safari/537.36')
            }
            resp = requests.get(url, headers=headers, timeout=10)
            if resp.status_code != 200:
                self.error_occurred.emit(f"请求失败,状态码:{resp.status_code}")
                return
            json_str = resp.text.split('(', 1)[1].rsplit(')', 1)[0]
            data = json.loads(json_str)
            items = data.get('items', [])
            if not items:
                self.stock_list_ready.emit(pd.DataFrame(), block_code)
                return
            rows = []
            for s in items:
                rows.append([
                    s.get('5', '').zfill(6),
                    s.get('55', ''),
                    s.get('8', ''),
                    s.get('199112', 0)
                ])
            df = pd.DataFrame(rows, columns=['股票代码', '股票名称', '最新价', '涨跌幅'])
            self.stock_list_ready.emit(df, block_code)
        except Exception as e:
            self.error_occurred.emit(f"获取成分股失败: {str(e)}")
# -------------------- 主窗口 --------------------
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("同花顺板块分析")
        self.setMinimumSize(1100, 650)
        self._setup_ui()
        self._setup_loader()
        self._load_blocks()
    def _setup_ui(self):
        central = QWidget()
        self.setCentralWidget(central)
        main_layout = QHBoxLayout(central)
        main_layout.setContentsMargins(5, 5, 5, 5)
        splitter = QSplitter(Qt.Horizontal)
        main_layout.addWidget(splitter)
        left_widget = QWidget()
        left_layout = QVBoxLayout(left_widget)
        left_layout.setContentsMargins(0, 0, 0, 0)
        self.tab_widget = QTabWidget()
        left_layout.addWidget(self.tab_widget)
        # 概念板块列表
        self.concept_list = QListWidget()
        self.concept_list.setAlternatingRowColors(True)
        self.concept_list.itemClicked.connect(self._on_block_selected)
        self.tab_widget.addTab(self.concept_list, "概念板块")
        # 行业板块列表
        self.industry_list = QListWidget()
        self.industry_list.setAlternatingRowColors(True)
        self.industry_list.itemClicked.connect(self._on_block_selected)
        self.tab_widget.addTab(self.industry_list, "行业板块")
        splitter.addWidget(left_widget)
        # 右侧表格
        self.table_view = QTableView()
        self.table_model = StockTableModel()
        self.table_view.setModel(self.table_model)
        self.table_view.setAlternatingRowColors(True)
        self.table_view.horizontalHeader().setStretchLastSection(True)
        self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
        splitter.addWidget(self.table_view)
        splitter.setSizes([350, 750])
        self._progress = QProgressBar()
        self._progress.setVisible(False)
        self.statusBar().addPermanentWidget(self._progress)
        self.setStyleSheet("""
            QListWidget::item { border-bottom: 1px solid #e0e0e0; }
            QListWidget::item:selected { background-color: #c0e0ff; }
            QTabWidget::pane { border: 1px solid #cccccc; }
            QHeaderView::section { background-color: #f0f0f0; padding: 4px; }
        """)
    def _setup_loader(self):
        self._loader = DataLoader()
        self._thread = QThread()
        self._loader.moveToThread(self._thread)
        self._thread.start()
        self._loader.concept_blocks_ready.connect(self._on_concept_blocks_loaded)
        self._loader.industry_blocks_ready.connect(self._on_industry_blocks_loaded)
        self._loader.stock_list_ready.connect(self._on_stock_list_loaded)
        self._loader.error_occurred.connect(self._show_error)
    def _load_blocks(self):
        self._progress.setVisible(True)
        self._progress.setRange(0, 0)
        QTimer.singleShot(100, self._loader.load_concept_blocks)
        QTimer.singleShot(200, self._loader.load_industry_blocks)
    def _on_concept_blocks_loaded(self, df):
        self._populate_list_widget(self.concept_list, df)
        self._check_all_loaded()
    def _on_industry_blocks_loaded(self, df):
        self._populate_list_widget(self.industry_list, df)
        self._check_all_loaded()
    def _get_change_column(self, df: pd.DataFrame) -> str:
        possible_names = ['涨跌幅', '涨跌幅(%)', '涨幅', '涨幅%', '涨跌幅度']
        for col in df.columns:
            if col in possible_names:
                return col
        for col in df.columns:
            if '涨跌幅' in col or '涨幅' in col:
                return col
        return None
    def _format_change(self, value):
        if pd.isna(value):
            return '--'
        try:
            if isinstance(value, str):
                value = value.replace('%', '').strip()
            val = float(value)
            return f"{val:+.2f}%"
        except:
            return str(value)
    def _get_change_color_html(self, change_str: str) -> str:
        try:
            if change_str.endswith('%'):
                num_str = change_str[:-1].strip()
            else:
                num_str = change_str
            if num_str.startswith('+'):
                num_str = num_str[1:]
            val = float(num_str)
            if val > 0:
                return 'red'
            elif val < 0:
                return 'green'
            else:
                return 'black'
        except:
            return 'gray'
    def _populate_list_widget(self, list_widget: QListWidget, df: pd.DataFrame):
        """填充左侧列表,使用QLabel实现颜色区分,并增加项高度"""
        list_widget.clear()
        if '指数简称' not in df.columns or 'code' not in df.columns:
            self._show_error("数据格式错误:缺少指数简称或code列")
            return
        change_col = self._get_change_column(df)
        if change_col is None:
            self._show_error("未找到涨跌幅列,将显示默认值")
            changes = ['--'] * len(df)
        else:
            changes = df[change_col].values
        for idx, row in df.iterrows():
            name = row['指数简称']
            code = str(row['code'])
            change_val = changes[idx]
            change_str = self._format_change(change_val)
            color = self._get_change_color_html(change_str)
            # 创建列表项
            item = QListWidgetItem(list_widget)
            item.setData(Qt.UserRole, code)
            # 创建QLabel显示HTML,增加内边距以提高项高度
            label = QLabel()
            html = f"""
            <div style="display: flex; justify-content: space-between; font-family: Arial; width: 100%;">
                <span style="font-weight: normal;">{name} <span style="color: #666;">({code})</span></span>
                <span style="font-weight: bold; color: {color};">{change_str}</span>
            </div>
            """
            label.setText(html)
            label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
            # 增加内边距,使项更高
            label.setStyleSheet("padding: 8px 4px; background-color: transparent;")
            label.setAutoFillBackground(False)
            # 让label根据内容和内边距调整大小
            label.adjustSize()
            # 设置项的大小提示,使其高度适应label
            item.setSizeHint(label.sizeHint())
            # 将label设置为item的widget
            list_widget.setItemWidget(item, label)
    def _check_all_loaded(self):
        if self.concept_list.count() > 0 and self.industry_list.count() > 0:
            self._progress.setVisible(False)
    def _on_block_selected(self, item: QListWidgetItem):
        block_code = item.data(Qt.UserRole)
        self.statusBar().showMessage("正在加载成分股...")
        self._progress.setVisible(True)
        self._progress.setRange(0, 0)
        QTimer.singleShot(0, lambda: self._loader.load_stock_list(block_code))
    def _on_stock_list_loaded(self, df: pd.DataFrame, block_code: str):
        self._progress.setVisible(False)
        self.statusBar().showMessage("就绪", 3000)
        if df.empty:
            QMessageBox.information(self, "提示", "该板块暂无成分股数据")
            self.table_model.update_data(pd.DataFrame())
        else:
            self.table_model.update_data(df)
            self.table_view.resizeColumnsToContents()
    def _show_error(self, msg: str):
        self._progress.setVisible(False)
        self.statusBar().showMessage("错误", 3000)
        QMessageBox.critical(self, "错误", msg)
    def closeEvent(self, event):
        self._thread.quit()
        self._thread.wait()
        event.accept()
# -------------------- 启动应用 --------------------
def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
if __name__ == "__main__":
    main()
代码语言:javascript
复制
如果我的分享对你有所帮助, 不吝啬给个点赞呗
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-02-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 子晓聊技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、工具功能速览
  • 三、如何使用?
    • 环境准备
    • 最后这里贴一下完整代码,参考下思路, 具体根据自己的实际情况改造。 备注:如果发现格式有多余的特殊字符,用普通浏览器打开复制应该没问题。 希望我的分享对大家有所帮助。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档