python_PyQt5开发股票指定区间K线操作工具_裸K

目录

写在前面:

工具使用演示:

代码:

导入包

横坐标控件、K线控件、带查询下拉列表控件

K线图控件

主界面代码

执行代码


写在前面:

继前面文章提到筛出低位股票后,想逐一查看这些股票今年的K线走势,发现股票软件要查看指定时间范围的K线不是很方便,所以萌生了开发这个工具的想法。另外,考虑到后续可能还会有很多数据在股票软件上不方便查看,就觉得开发个可用的工具十分必要。

工具使用演示:

1 按钮"选择股票日数据所在目录":股票日数据目录。

注:这里的股票日数据来自优矿,所以涉及到日数据字段的操作,参看优矿官网上的数据说明。

2 按钮"选择股票代码和股票名键值对json文件":这里选择放置要查看的股票列表{股票代码:股票名}的json文件

3 第2步选择json文件后,程序会读取股票名信息放入下拉列表中

4 全局区间控制区域:这里设置了时间区间,那么下面K线图就都会显示这个区间,不需要一个个股票设置

5 K线图展示区域

代码:

导入包

复制代码
import os,sys,json
import pandas as pd
from threading import Thread
from typing import Dict,List,Any
from PyQt5 import QtCore,QtWidgets,QtGui
from PyQt5.QtCore import Qt
import pyqtgraph as pg
pg.setConfigOption('background','w')
pg.setConfigOption('foreground','k')

横坐标控件、K线控件、带查询下拉列表控件

复制代码
class RotateAxisItem(pg.AxisItem):
    def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
        p.setRenderHint(p.Antialiasing,False)
        p.setRenderHint(p.TextAntialiasing,True)

        ## draw long line along axis
        pen,p1,p2 = axisSpec
        p.setPen(pen)
        p.drawLine(p1,p2)
        p.translate(0.5,0)  ## resolves some damn pixel ambiguity

        ## draw ticks
        for pen,p1,p2 in tickSpecs:
            p.setPen(pen)
            p.drawLine(p1,p2)

        ## draw all text
        # if self.tickFont is not None:
        #     p.setFont(self.tickFont)
        p.setPen(self.pen())
        for rect,flags,text in textSpecs:
            # this is the important part
            p.save()
            p.translate(rect.x(),rect.y())
            p.rotate(-30)
            p.drawText(int(-rect.width()),int(rect.height()),int(rect.width()),int(rect.height()),flags,text)
            # restoring the painter is *required*!!!
            p.restore()

class CandlestickItem(pg.GraphicsObject):
    def __init__(self, data):
        pg.GraphicsObject.__init__(self)
        self.data = data  ## data must have fields: time, open, close, min, max
        self.generatePicture()

    def generatePicture(self):
        ## pre-computing a QPicture object allows paint() to run much more quickly,
        ## rather than re-drawing the shapes every time.
        self.picture = QtGui.QPicture()
        p = QtGui.QPainter(self.picture)
        p.setPen(pg.mkPen('d'))
        w = (self.data[1][0] - self.data[0][0]) / 3.
        for (t, open, close, min, max) in self.data:
            p.drawLine(QtCore.QPointF(t, min), QtCore.QPointF(t, max))
            if open < close:
                p.setBrush(pg.mkBrush('r'))
            else:
                p.setBrush(pg.mkBrush('g'))
            p.drawRect(QtCore.QRectF(t-w, open, w * 2, close - open))
        p.end()

    def paint(self, p, *args):
        p.drawPicture(0, 0, self.picture)

    def boundingRect(self):
        ## boundingRect _must_ indicate the entire area that will be drawn on
        ## or else we will get artifacts and possibly crashing.
        ## (in this case, QPicture does all the work of computing the bouning rect for us)
        return QtCore.QRectF(self.picture.boundingRect())

class ExtendedComboBox(QtWidgets.QComboBox):
    def __init__(self, parent=None):
        super(ExtendedComboBox, self).__init__(parent)

        self.setFocusPolicy(Qt.StrongFocus)
        self.setEditable(True)

        # add a filter model to filter matching items
        self.pFilterModel = QtCore.QSortFilterProxyModel(self)
        self.pFilterModel.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.pFilterModel.setSourceModel(self.model())

        # add a completer, which uses the filter model
        self.completer = QtWidgets.QCompleter(self.pFilterModel, self)
        # always show all (filtered) completions
        self.completer.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
        self.setCompleter(self.completer)

        # connect signals
        self.lineEdit().textEdited.connect(self.pFilterModel.setFilterFixedString)
        self.completer.activated.connect(self.on_completer_activated)

    # on selection of an item from the completer, select the corresponding item from combobox
    def on_completer_activated(self, text):
        if text:
            index = self.findText(text)
            self.setCurrentIndex(index)
            self.activated[str].emit(self.itemText(index))

    # on model change, update the models of the filter and completer as well
    def setModel(self, model):
        super(ExtendedComboBox, self).setModel(model)
        self.pFilterModel.setSourceModel(model)
        self.completer.setModel(self.pFilterModel)

    # on model column change, update the model column of the filter and completer as well
    def setModelColumn(self, column):
        self.completer.setCompletionColumn(column)
        self.pFilterModel.setFilterKeyColumn(column)
        super(ExtendedComboBox, self).setModelColumn(column)

K线图控件

复制代码
class K_Widget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.init_data()
        self.init_ui()
        pass
    def init_data(self):
        self.whole_header = None
        self.whole_df = None
        self.whole_pd_header = None
        self.whole_xTick = None

        self.current_df = None
        self.current_data = None
        self.dur_len = 20
        pass
    def init_ui(self):
        self.duration_label = QtWidgets.QLabel('左边界~右边界')

        self.left_label = QtWidgets.QLabel('左边:')
        self.left_slider = QtWidgets.QSlider(Qt.Horizontal)
        self.left_slider.valueChanged.connect(self.left_slider_valueChanged)
        self.right_slider = QtWidgets.QSlider(Qt.Horizontal)
        self.right_slider.valueChanged.connect(self.right_slider_valueChanged)
        self.right_label = QtWidgets.QLabel(':右边')

        check_btn = QtWidgets.QPushButton('确定')
        check_btn.clicked.connect(self.check_btn_clicked)

        layout_top = QtWidgets.QHBoxLayout()
        layout_top.addWidget(self.duration_label)
        layout_top.addWidget(self.left_label)
        layout_top.addWidget(self.left_slider)
        layout_top.addWidget(self.right_slider)
        layout_top.addWidget(self.right_label)
        layout_top.addWidget(check_btn)

        xax = RotateAxisItem(orientation='bottom')
        xax.setHeight(h=50)
        self.pw = pg.PlotWidget(axisItems={'bottom': xax})
        self.pw.setMouseEnabled(x=True, y=False)
        # self.pw.enableAutoRange(x=False,y=True)
        self.pw.setAutoVisible(x=False, y=True)

        layout = QtWidgets.QVBoxLayout()
        layout.addLayout(layout_top)
        layout.addWidget(self.pw)
        self.setLayout(layout)
        pass

    def first_setData(self,data:Dict):
        whole_header = data['whole_header']
        whole_df = data['whole_df']
        whole_pd_header = data['whole_pd_header']

        whole_xTick = whole_df['xTick'].values.tolist()

        self.whole_header = whole_header
        self.whole_df = whole_df
        self.whole_pd_header = whole_pd_header
        self.whole_xTick = whole_xTick

        self.left_slider.setMinimum(0)
        self.left_slider.setMaximum(len(whole_xTick)-1)
        self.right_slider.setMinimum(0)
        self.right_slider.setMaximum(len(whole_xTick)-1)

        self.left_slider.setValue(0)
        self.right_slider.setValue(len(whole_xTick)-1)
        self.left_label.setText(f"左边:{whole_xTick[0]}")
        self.right_label.setText(f"{whole_xTick[-1]}:右边")

        self.current_df = self.whole_df.copy()
        self.caculate_and_show_data()
        pass

    def caculate_and_show_data(self):
        df = self.current_df.copy()
        df.reset_index(inplace=True)
        df['x'] = [i for i in range(len(df))]
        xTick = df['xTick'].values.tolist()
        xTick00 = []
        dur_num = int(len(xTick)/self.dur_len)
        if dur_num > 2:
            for i in range(0,len(xTick),dur_num):
                xTick00.append((i,xTick[i]))
            pass
        else:
            for i,item in enumerate(xTick):
                xTick00.append((i,item))
            pass
        candle_data = []
        for i,row in df.iterrows():
            candle_data.append((row['x'],row['open'],row['close'],row['lowest'],row['highest']))
        self.current_data = df.loc[:,self.whole_pd_header].values.tolist()
        y_min = df['lowest'].min()
        y_max = df['highest'].max()

        # 开始配置显示内容
        self.pw.clear()

        self.duration_label.setText(f"{xTick[0]}~{xTick[-1]}")

        xax = self.pw.getAxis('bottom')
        xax.setTicks([xTick00])

        self.vb = self.pw.getViewBox()
        self.vb.setLimits(yMin=y_min,yMax=y_max)

        candle_fixed_target = CandlestickItem(candle_data)
        self.pw.addItem(candle_fixed_target)

        self.vLine = pg.InfiniteLine(angle=90,movable=False)
        self.hLine = pg.InfiniteLine(angle=0,movable=False)
        self.label = pg.TextItem()

        self.pw.addItem(self.vLine,ignoreBounds=True)
        self.pw.addItem(self.hLine,ignoreBounds=True)
        self.pw.addItem(self.label,ignoreBounds=True)

        self.proxy = pg.SignalProxy(self.pw.scene().sigMouseMoved,rateLimit=60,slot=self.mouseMoved)
        self.pw.enableAutoRange()
        self.pw.setAutoVisible()
        pass

    def mouseMoved(self,evt):
        pos = evt[0]
        if self.pw.sceneBoundingRect().contains(pos):
            mousePoint = self.vb.mapSceneToView(pos)
            index = int(mousePoint.x())
            if index>=0 and index<len(self.current_data):
                target_data = self.current_data[index]
                html_str = ''
                for i,item in enumerate(self.whole_header):
                    html_str += f"<br/>{item}:{target_data[i]}"
                self.label.setHtml(html_str)
                self.label.setPos(mousePoint.x(),mousePoint.y())
                pass
            self.vLine.setPos(mousePoint.x())
            self.hLine.setPos(mousePoint.y())
            pass
        pass

    def left_slider_valueChanged(self):
        left_value = self.left_slider.value()
        self.left_label.setText(f"左边:{self.whole_xTick[left_value]}")
        pass
    def right_slider_valueChanged(self):
        right_value = self.right_slider.value()
        self.right_label.setText(f"{self.whole_xTick[right_value]}:右边")
        pass
    def check_btn_clicked(self):
        left_value = self.left_slider.value()
        right_value = self.right_slider.value()

        if right_value <= left_value:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '左边界不能大于右边界',
                QtWidgets.QMessageBox.Yes
            )
            return
        self.current_df = self.whole_df.iloc[left_value:right_value].copy()
        self.caculate_and_show_data()
        pass
    pass

主界面代码

复制代码
class KMainWidget(QtWidgets.QWidget):
    signal_excute = QtCore.pyqtSignal(object)
    def __init__(self):
        super().__init__()

        self.thread_caculate: Thread = None

        self.init_data()
        self.init_ui()
        self.register_event()
        self.progress_init()
        self.global_duration_checkbox_clicked()
        pass
    def init_data(self):
        self.please_select_str: str = '-- 请选择 --'
        self.current_stock_name: str = None
        self.ticker_name_map: Dict[str,str] = {}
        self.name_ticker_map: Dict[str,str] = {}
        self.stockname_list: List[str] = []
        self.target_column_list: List[str] = ['xTick','open','close','highest','lowest']
        pass
    def init_ui(self):
        self.setWindowTitle('股票指定区间K线工具')

        self.caculate_progress = QtWidgets.QProgressBar()
        self.caculate_status_label = QtWidgets.QLabel()

        layout_progress = QtWidgets.QHBoxLayout()
        layout_progress.addWidget(self.caculate_progress)
        layout_progress.addWidget(self.caculate_status_label)

        choice_stock_dir_btn = QtWidgets.QPushButton('选择股票日数据所在目录')
        choice_stock_dir_btn.clicked.connect(self.choice_stock_dir_btn_clicked)
        self.stock_dir_lineedit = QtWidgets.QLineEdit()
        choice_stockname_map_file_btn = QtWidgets.QPushButton('选择股票代码和股票名键值对json文件')
        choice_stockname_map_file_btn.clicked.connect(self.choice_stockname_map_file_btn_clicked)
        self.stockname_json_lineedit = QtWidgets.QLineEdit()

        layout_one = QtWidgets.QFormLayout()
        layout_one.addRow(choice_stock_dir_btn,self.stock_dir_lineedit)
        layout_one.addRow(choice_stockname_map_file_btn,self.stockname_json_lineedit)

        tip_label = QtWidgets.QLabel('股票列表')
        self.stock_combox = ExtendedComboBox()
        self.stock_combox.addItem(self.please_select_str)
        self.stock_combox.currentTextChanged.connect(self.stock_combox_currentTextChanged)

        self.global_duration_checkbox = QtWidgets.QCheckBox('全局区间')
        self.global_duration_checkbox.clicked.connect(self.global_duration_checkbox_clicked)
        self.left_point = QtWidgets.QDateEdit()
        self.left_point.setDisplayFormat('yyyy-MM-dd')
        self.left_point.setCalendarPopup(True)
        self.right_point = QtWidgets.QDateEdit()
        self.right_point.setDisplayFormat('yyyy-MM-dd')
        self.right_point.setCalendarPopup(True)
        self.global_duration_btn = QtWidgets.QPushButton('确定')
        self.global_duration_btn.clicked.connect(self.global_duration_btn_clicked)

        layout_two = QtWidgets.QHBoxLayout()
        layout_two.addWidget(tip_label)
        layout_two.addWidget(self.stock_combox)
        layout_two.addStretch(1)
        layout_two.addWidget(self.global_duration_checkbox)
        layout_two.addWidget(self.left_point)
        layout_two.addWidget(self.right_point)
        layout_two.addWidget(self.global_duration_btn)

        layout_up = QtWidgets.QVBoxLayout()
        layout_up.addLayout(layout_one)
        layout_up.addLayout(layout_two)

        groupbox = QtWidgets.QGroupBox('全局设置',self)
        groupbox.setLayout(layout_up)

        self.title_label = QtWidgets.QLabel('股票简称')
        self.title_label.setAlignment(QtCore.Qt.AlignCenter)
        self.title_label.setStyleSheet('QLabel{font-size:16px;font-weight:bold}')

        self.k_widget = K_Widget()

        layout_three = QtWidgets.QVBoxLayout()
        layout_three.addWidget(self.title_label)
        layout_three.addWidget(self.k_widget)

        up_btn = QtWidgets.QPushButton('上一个')
        up_btn.clicked.connect(self.up_btn_clicked)
        down_btn = QtWidgets.QPushButton('下一个')
        down_btn.clicked.connect(self.down_btn_clicked)

        layout_four = QtWidgets.QVBoxLayout()
        layout_four.addWidget(up_btn)
        layout_four.addWidget(down_btn)

        layout_five = QtWidgets.QHBoxLayout()
        layout_five.addLayout(layout_three,11)
        layout_five.addLayout(layout_four,1)

        layout = QtWidgets.QVBoxLayout()
        layout.addLayout(layout_progress)
        layout.addWidget(groupbox)
        layout.addLayout(layout_five)
        self.setLayout(layout)
        pass
    def register_event(self):
        self.signal_excute.connect(self.process_excute_event)
        pass
    def process_excute_event(self,data:Dict):
        mark_str = data['mark_str']
        status = data['status']
        if mark_str == 'show_stock_candle':
            if status == 'error':
                self.thread_caculate = None
                self.progress_finished()
                QtWidgets.QMessageBox.information(
                    self,
                    '提示',
                    data['data'],
                    QtWidgets.QMessageBox.Yes
                )
                return
            else:
                self.title_label.setText(self.current_stock_name)
                self.k_widget.first_setData(data['data'])

                self.thread_caculate = None
                self.progress_finished()
                pass
        pass
    def choice_stock_dir_btn_clicked(self):
        path = QtWidgets.QFileDialog.getExistingDirectory(
            self,
            '打开股票日数据所在目录',
            '.'
        )
        if not path:
            return
        self.stock_dir_lineedit.setText(path)
        pass
    def choice_stockname_map_file_btn_clicked(self):
        path,_ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            '选择股票代码与名称键值对json文件',
            '.',
            'JOSN(*.json)'
        )
        if not path:
            return
        self.stockname_json_lineedit.setText(path)
        with open(path,'r',encoding='utf-8') as fr:
            self.ticker_name_map = json.load(fr)
        self.name_ticker_map = {}
        for key,val in self.ticker_name_map.items():
            self.name_ticker_map[val] = key
        self.stockname_list = list(self.name_ticker_map.keys())
        self.stock_combox.clear()
        self.stock_combox.addItem(self.please_select_str)
        self.stock_combox.addItems(self.stockname_list)
        pass
    def stock_combox_currentTextChanged(self,txt:str):
        cur_txt = self.stock_combox.currentText()
        if not cur_txt \
                or cur_txt==self.current_stock_name \
                or cur_txt==self.please_select_str:
            return
        self.current_stock_name = cur_txt
        self.pre_caculate_data()
        pass
    def global_duration_checkbox_clicked(self):
        if self.global_duration_checkbox.isChecked():
            # 勾选,则表示要遵从全局区间
            self.left_point.setDisabled(False)
            self.right_point.setDisabled(False)
            self.global_duration_btn.setDisabled(False)
            pass
        else:
            # 没有勾选,表示不设全局区间
            self.left_point.setDisabled(True)
            self.right_point.setDisabled(True)
            self.global_duration_btn.setDisabled(True)
            pass
        pass
    def global_duration_btn_clicked(self):
        if self.current_stock_name:
            self.pre_caculate_data()
        pass
    def up_btn_clicked(self):
        if not self.current_stock_name:
            self.current_stock_name = self.stockname_list[-1]
        else:
            try:
                cur_index = self.stockname_list.index(self.current_stock_name)
                if cur_index == 0:
                    cur_index = len(self.stockname_list)-1
                else:
                    cur_index = cur_index -1
                self.current_stock_name = self.stockname_list[cur_index]
            except:
                QtWidgets.QMessageBox.information(
                    self,
                    '提示',
                    f'{self.current_stock_name}在股票列表中不存在',
                    QtWidgets.QMessageBox.Yes
                )
                return
        self.pre_caculate_data()
        pass
    def down_btn_clicked(self):
        if not self.current_stock_name:
            self.current_stock_name = self.stockname_list[0]
        else:
            try:
                cur_index = self.stockname_list.index(self.current_stock_name)
                if cur_index >= len(self.stockname_list) - 1:
                    cur_index = 0
                else:
                    cur_index = cur_index + 1
                self.current_stock_name = self.stockname_list[cur_index]
            except:
                QtWidgets.QMessageBox.information(
                    self,
                    '提示',
                    f'{self.current_stock_name}在股票列表中不存在',
                    QtWidgets.QMessageBox.Yes
                )
                return
        self.pre_caculate_data()
        pass
    def pre_caculate_data(self):
        # 股票日数据目录是否已经指定
        daily_dir = self.stock_dir_lineedit.text()
        daily_dir = daily_dir.strip()
        if not daily_dir:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '请选择股票日数据目录',
                QtWidgets.QMessageBox.Yes
            )
            return
        if not self.ticker_name_map:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '请选择股票代码股票名键值对json文件',
                QtWidgets.QMessageBox.Yes
            )
            return
        daily_file_path = daily_dir + os.path.sep + self.name_ticker_map[self.current_stock_name] + '.csv'
        if not os.path.exists(daily_file_path):
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                f'不存在{self.current_stock_name}的日数据文件',
                QtWidgets.QMessageBox.Yes
            )
            return

        # 判断是否有全局设置
        if self.global_duration_checkbox.isChecked():
            global_dur_yeah = True
            left_point = self.left_point.date().toString('yyyy-MM-dd')
            right_point = self.right_point.date().toString('yyyy-MM-dd')
            global_dur_list = [left_point,right_point]
            pass
        else:
            global_dur_yeah = False
            global_dur_list = []
            pass
        mark_str = 'show_stock_candle'
        pre_map = {
            'global_dur_yeah':global_dur_yeah,
            'global_dur_list':global_dur_list,
            'daily_file_path':daily_file_path
        }
        self.start_caculate_thread(mark_str,pre_map)
        self.progress_busy()
        pass

    def start_caculate_thread(self,mark_str:str,data:Dict[str,Any]):
        if self.thread_caculate:
            QtWidgets.QMessageBox.information(
                self,
                '提示',
                '线程正在执行任务,请稍后。。。',
                QtWidgets.QMessageBox.Yes
            )
            return
        self.thread_caculate = Thread(
            target=self.running_caculate_thread,
            args=(
                mark_str, data,
            )
        )
        self.thread_caculate.start()
        self.progress_busy()
        pass
    def running_caculate_thread(self,mark_str:str,data:Dict[str,Any]):
        if mark_str == 'show_stock_candle':
            global_dur_yeah = data['global_dur_yeah']
            global_dur_list = data['global_dur_list']
            daily_file_path = data['daily_file_path']

            df = pd.read_csv(daily_file_path,encoding='utf-8')
            # 去除停牌的数据
            df = df.loc[df['openPrice']>0].copy()
            # 计算前复权数据
            df['open'] = df['openPrice']*df['accumAdjFactor']
            df['close'] = df['closePrice']*df['accumAdjFactor']
            df['highest'] = df['highestPrice']*df['accumAdjFactor']
            df['lowest'] = df['lowestPrice']*df['accumAdjFactor']
            df['xTick'] = df['tradeDate']

            if global_dur_yeah:
                df['o_date'] = pd.to_datetime(df['tradeDate'])
                df = df.loc[(df['o_date']>=global_dur_list[0]) & (df['o_date']<=global_dur_list[1])].copy()
                if len(df)<2:
                    res_map = {
                        'mark_str': mark_str,
                        'status': 'error',
                        'data': '全局区间对应的时间段没有数据'
                    }
                    self.signal_excute.emit(res_map)
                    return
            res_data = {
                'whole_df':df,
                'whole_header':['日期','开盘','收盘','最高','最低'],
                'whole_pd_header':self.target_column_list
            }
            res_map = {
                'mark_str': mark_str,
                'status': 'success',
                'data': res_data
            }
            self.signal_excute.emit(res_map)
            pass
        pass
    def progress_init(self) -> None:
        self.caculate_progress.setValue(0)
        self.caculate_status_label.setText('无任务')
    def progress_busy(self) -> None:
        self.caculate_progress.setRange(0, 0)
        self.caculate_status_label.setText('正在执行')
    def progress_finished(self) -> None:
        self.caculate_progress.setRange(0, 100)
        self.caculate_progress.setValue(100)
        self.caculate_status_label.setText('执行完毕')
        pass
    pass

执行代码

复制代码
if __name__ == '__main__':
    QtCore.QCoreApplication.setAttribute(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
    app = QtWidgets.QApplication(sys.argv)
    main_window = KMainWidget()
    main_window.showMaximized()
    app.exec()
    pass
相关推荐
copyer_xyf1 分钟前
LangChain 调用 LLM
后端·python·agent
copyer_xyf10 分钟前
Prompt 组织管理
后端·python·agent
shimly1234561 小时前
python3 uvicorn 是啥?
python
CTA量化套保2 小时前
期货量化程序 time.sleep 卡死:天勤单线程与 deadline 替代
python·区块链
GIS数据转换器2 小时前
城市排水生命线安全运行监测平台深度解析
java·运维·人工智能·python·安全·数据挖掘·无人机
贤哥哥yyds2 小时前
GBK转UTF\-8编码自动转换工具 使用文档
python
数量技术宅2 小时前
2026量化前沿:从Reddit热帖到Python实战,如何用赫斯特指数(Hurst)狙击虚假突破?
开发语言·python
华如锦3 小时前
面了很多 Java转AI Agent方向,一些面试题总结
java·开发语言·人工智能·python·ai
戴西软件3 小时前
戴西 DLM 许可授权管理系统:破解无网络环境下工业软件授权难题,助力制造企业降本增效
网络·人工智能·python·深度学习·程序人生·算法·制造
Dxy12393102163 小时前
Python线程锁:为什么多线程会“打架“,以及怎么解决
开发语言·前端·python