射频测试带界面

复制代码
import sys
import os
import re
import pandas as pd
import numpy as np
from pathlib import Path
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 
    QPushButton, QLabel, QFileDialog, QTextEdit, QProgressBar, 
    QLineEdit, QGroupBox, QFormLayout, QMessageBox, QTabWidget,
    QScrollArea, QFrame, QComboBox
)
from PySide6.QtCore import QThread, Signal, Qt
from PySide6.QtGui import QFont
import matplotlib
matplotlib.use('Agg')  # 使用非GUI后端
import matplotlib.pyplot as plt
from scipy import stats
from openpyxl.drawing.image import Image as OpenpyxlImage
import tempfile


class DataProcessor:
    """数据处理核心类 - 飞机射频耦合测试数据处理"""
    
    def split_column_data_inplace(self, df, column_name):
        """对指定列的数据进行拆分处理,并直接替换原数据"""
        # 预设规则:按列名预期格式
        split_rules = {
            '频段': r'freq_band\s*:\s*([^\s,,\n\r\t\'\"]+)',
            'frequency': r'frequency\s*:\s*([-\d.]+)',
            'rc_gain_a均值': r'rc_gain_a均值:([-\d.]+)',
            'rc_gain_b均值': r'rc_gain_b均值:([-\d.]+)',
            'rc_snr均值': r'rc_snr均值:([-\d.]+)',
            'fc_gain_a均值': r'fc_gain_a均值:([-\d.]+)',
            'fc_gain_b均值': r'fc_gain_b均值:([-\d.]+)',
            'fc_snr均值': r'fc_snr均值:([-\d.]+)',
            'tlv_err均值': r'tlv_err均值:([-\d.]+)',
            'num_err均值': r'num_err均值:([-\d.]+)',
            '错误信息': r'错误信息:(.*)'
        }

        if column_name in df.columns:
            for idx, row in df.iterrows():
                cell_value = str(row[column_name]) if pd.notna(row[column_name]) else ""

                # 首先尝试按照预设规则匹配
                if column_name in split_rules:
                    match = re.search(split_rules[column_name], cell_value)
                    if match:
                        extracted_value = match.group(1)
                        df.at[idx, column_name] = extracted_value
                        continue

                # 如果预设规则不匹配,尝试通用识别
                content_patterns = [
                    (r'freq_band\s*:\s*([^\s,,\n\r\t\'\"]+)', 'freq_band识别'),
                    (r'frequency\s*:\s*([-\d.]+)', 'frequency识别'),
                    (r'rc_gain_a均值:([-\d.]+)', 'rc_gain_a均值识别'),
                    (r'rc_gain_b均值:([-\d.]+)', 'rc_gain_b均值识别'),
                    (r'rc_snr均值:([-\d.]+)', 'rc_snr均值识别'),
                    (r'fc_gain_a均值:([-\d.]+)', 'fc_gain_a均值识别'),
                    (r'fc_gain_b均值:([-\d.]+)', 'fc_gain_b均值识别'),
                    (r'fc_snr均值:([-\d.]+)', 'fc_snr均值识别'),
                    (r'tlv_err均值:([-\d.]+)', 'tlv_err均值识别'),
                    (r'num_err均值:([-\d.]+)', 'num_err均值识别'),
                    (r'错误信息:(.*)', '错误信息识别'),
                ]

                matched = False
                for pattern, desc in content_patterns:
                    match = re.search(pattern, cell_value)
                    if match:
                        extracted_value = match.group(1)

                        # 如果是错误信息,即使在错误的列中也要提取出来
                        if '错误信息' in desc:
                            df.at[idx, column_name] = extracted_value
                        else:
                            df.at[idx, column_name] = extracted_value

                        matched = True
                        break

                if not matched:
                    # 如果内容以"错误信息:"开头但后面没有内容,也应处理
                    if cell_value.startswith('错误信息:'):
                        if len(cell_value) > 5:  # "错误信息:"长度为5
                            extracted_value = cell_value[5:]  # 去除"错误信息:"前缀
                            df.at[idx, column_name] = extracted_value
                        else:
                            # 空的错误信息,设为空字符串
                            df.at[idx, column_name] = ""

        return df

    def remove_duplicates_and_write_to_new_sheet(self, df, output_file_path, 
                                                original_sheet_name='原始处理数据', 
                                                deduplicated_sheet_name='去重后数据', 
                                                filter_2g_sheet='2G数据', 
                                                filter_5g_sheet='5G数据'):
        """将处理完成的数据去重,取最后一次测试数据,并按频段筛选,写入到Excel的不同工作表中"""
        try:
            # 定义可能用于去重的关键列
            key_columns = []
            possible_key_columns = ['飞机SN', 'freq_band', '频段']

            # 检查哪些关键列存在于数据中
            for col in possible_key_columns:
                if col in df.columns:
                    key_columns.append(col)

            # 如果找到了关键列,基于这些列去重;否则基于所有列去重
            if key_columns:
                # 基于关键列去重,保留最后一条记录(最后一次测试数据)
                deduplicated_df = df.drop_duplicates(subset=key_columns, keep='last')
                print(f"基于列 {key_columns} 进行去重,原始数据: {len(df)} 行,去重后: {len(deduplicated_df)} 行")
            else:
                # 如果没有找到关键列,基于所有列进行去重
                deduplicated_df = df.drop_duplicates(keep='last')
                print(f"基于所有列进行去重,原始数据: {len(df)} 行,去重后: {len(deduplicated_df)} 行")

            # 进行频段筛选
            freq_band_col = None
            # 查找频段相关的列
            for col in ['freq_band', '频段', 'frequency']:
                if col in deduplicated_df.columns:
                    freq_band_col = col
                    break

            if freq_band_col:
                # 提取2G和5G数据
                filtered_2g = deduplicated_df[
                    deduplicated_df[freq_band_col].astype(str).str.contains(r'2g|2G|2\.4g|2\.4G', case=False, na=False)
                ]

                filtered_5g = deduplicated_df[
                    deduplicated_df[freq_band_col].astype(str).str.contains(r'5g|5G|5\.8g|5\.8G', case=False, na=False)
                ]

                print(f"2G数据: {len(filtered_2g)} 行")
                print(f"5G数据: {len(filtered_5g)} 行")
            else:
                # 如果没有找到频段列,则创建空的筛选数据
                filtered_2g = pd.DataFrame(columns=deduplicated_df.columns)
                filtered_5g = pd.DataFrame(columns=deduplicated_df.columns)
                print("未找到频段相关列,跳过频段筛选")

            # 将所有数据写入不同的工作表
            with pd.ExcelWriter(output_file_path, engine='openpyxl') as writer:
                # 写入原始处理后的数据
                df.to_excel(writer, sheet_name=original_sheet_name, index=False)
                # 写入去重后的数据
                deduplicated_df.to_excel(writer, sheet_name=deduplicated_sheet_name, index=False)
                # 写入2G频段数据
                filtered_2g.to_excel(writer, sheet_name=filter_2g_sheet, index=False)
                # 写入5G频段数据
                filtered_5g.to_excel(writer, sheet_name=filter_5g_sheet, index=False)

            print(f"数据已保存到 {output_file_path}")
            print(f"- 工作表 '{original_sheet_name}': 包含原始处理后的数据")
            print(f"- 工作表 '{deduplicated_sheet_name}': 包含去重后的数据(最后一次测试)")
            print(f"- 工作表 '{filter_2g_sheet}': 包含2G频段数据")
            print(f"- 工作表 '{filter_5g_sheet}': 包含5G频段数据")

            return deduplicated_df, filtered_2g, filtered_5g

        except Exception as e:
            print(f"去重、筛选和写入过程中发生错误: {str(e)}")
            # 如果出现错误,仍保存原始数据
            df.to_excel(output_file_path, index=False)
            print(f"由于错误,仅保存了原始数据到 {output_file_path}")
            return df, pd.DataFrame(), pd.DataFrame()

    def analyze_pass_rate_by_aircraft_sn(self, df, output_file_path, pass_rate_sheet_name='通过率统计'):
        """
        基于飞机SN和频率分析一次性通过率和复测通过率
        """
        try:
            # 确定飞机SN列
            aircraft_sn_col = None
            possible_sn_cols = ['飞机SN', 'aircraft_sn', 'SN', '遥控器SN', '测试SN', '遥控器SN']
            for col in possible_sn_cols:
                if col in df.columns:
                    aircraft_sn_col = col
                    break

            if not aircraft_sn_col:
                print("警告:未找到飞机SN相关列,跳过通过率分析")
                return pd.DataFrame(), pd.DataFrame()

            # 确定频率列
            freq_col = None
            possible_freq_cols = ['freq_band', '频段', 'frequency']
            for col in possible_freq_cols:
                if col in df.columns:
                    freq_col = col
                    break

            # 确定通过标准列
            pass_col = None
            possible_pass_cols = [col for col in df.columns if any(keyword in col.lower() for keyword in ['pass', 'fail', 'status', 'result', '测试结果', '状态'])]
            if possible_pass_cols:
                pass_col = possible_pass_cols[0]
            else:
                # 查找可能表示通过/失败的列
                numeric_cols = df.select_dtypes(include=['number']).columns.tolist()
                for col in ['rc_snr均值', 'fc_snr均值', 'rc_gain_a均值', 'rc_gain_b均值', 'fc_gain_a均值', 'fc_gain_b均值']:
                    if col in numeric_cols:
                        pass_col = col
                        break

            if not pass_col:
                print("警告:未找到合适的通过/失败判断列,使用默认判断逻辑")

            # 按飞机SN和频率分组
            if freq_col:
                # 按飞机SN和频率分组
                grouped = df.groupby([aircraft_sn_col, freq_col])
            else:
                # 只按飞机SN分组
                grouped = df.groupby([aircraft_sn_col])

            # 初始化统计结果 - 每个SN+频率组合只保留一条记录
            summary_data = {
                '飞机SN': [],
                '频率': [],
                '测试次数': [],
                '最终结果': [],
                '首次测试结果': [],
                '是否复测': [],
                '测试时间': []  # 添加测试时间
            }

            for group_key, group in grouped:
                test_count = len(group)
                first_result = group.iloc[0]  # 第一次测试
                last_result = group.iloc[-1]  # 最后一次测试结果

                # 解析分组键
                if isinstance(group_key, tuple):  # 多列分组
                    if freq_col:
                        aircraft_sn = str(group_key[0])
                        freq_band = str(group_key[1])
                    else:
                        aircraft_sn = str(group_key[0])
                        freq_band = '未知'
                else:  # 单列分组
                    aircraft_sn = str(group_key)
                    freq_band = '未知'

                # 判断通过/失败
                if pass_col and pass_col in group.columns:
                    if group[pass_col].dtype == 'object':  # 文本类型
                        final_value = str(last_result[pass_col]).lower().strip()
                        first_value = str(first_result[pass_col]).lower().strip()

                        final_pass = final_value in ['pass', '通过', 'success', 'ok', '1', 'true', 'yes', 'y']
                        first_pass = first_value in ['pass', '通过', 'success', 'ok', '1', 'true', 'yes', 'y']
                    else:  # 数值类型
                        threshold = 0  # 可根据具体业务逻辑调整
                        final_pass = pd.to_numeric(last_result[pass_col], errors='coerce') >= threshold
                        first_pass = pd.to_numeric(first_result[pass_col], errors='coerce') >= threshold
                else:
                    final_pass = True
                    first_pass = True

                # 添加记录(每个SN+频率组合只保留一条)
                summary_data['飞机SN'].append(aircraft_sn)
                summary_data['频率'].append(freq_band)
                summary_data['测试次数'].append(test_count)  # 实际统计的测试次数
                summary_data['最终结果'].append('PASS' if final_pass else 'FAIL')
                summary_data['首次测试结果'].append('PASS' if first_pass else 'FAIL')
                summary_data['是否复测'].append('是' if test_count > 1 else '否')

                # 如果有时间列,也添加进去
                time_cols = [col for col in group.columns if any(time_keyword in col.lower() for time_keyword in ['time', 'date', '日期', '时间'])]
                if time_cols:
                    summary_data['测试时间'].append(str(group.iloc[-1][time_cols[0]]) if not pd.isna(group.iloc[-1][time_cols[0]]) else '未知')
                else:
                    summary_data['测试时间'].append('未知')

            # 创建汇总DataFrame
            summary_df = pd.DataFrame(summary_data)

            # 计算整体统计
            unique_aircraft_count = summary_df['飞机SN'].nunique()  # 飞机SN数量
            total_tests = len(summary_df)

            # 统计2G和5G测试数量
            if '频率' in summary_df.columns:
                g2_tests = len(summary_df[summary_df['频率'].str.contains('2G|2g', case=False, na=False)])
                g5_tests = len(summary_df[summary_df['频率'].str.contains('5G|5g', case=False, na=False)])
            else:
                g2_tests = 0
                g5_tests = 0

            # 计算一次通过数:所有飞机SN的2G+5G首次测试通过总数
            # 对于每个飞机SN,检查其所有频率的首次测试结果
            sn_freq_first_tests = summary_df.groupby(['飞机SN', '频率']).first().reset_index()

            # 按飞机SN聚合,判断该SN在所有频率下是否首次测试都通过
            sn_first_pass_status = sn_freq_first_tests.groupby('飞机SN').agg({
                '首次测试结果': lambda x: 'PASS' if all(result == 'PASS' for result in x) else 'FAIL'
            }).reset_index()

            # 计算所有频率首次测试都通过的飞机SN数量
            first_pass_total = len(sn_first_pass_status[sn_first_pass_status['首次测试结果'] == 'PASS'])
            first_pass_rate = (first_pass_total / unique_aircraft_count * 100) if unique_aircraft_count > 0 else 0

            # 计算复测情况:统计有复测的飞机SN数量
            # 对每个飞机SN,检查其任一频率是否有复测
            sn_has_retest = summary_df[summary_df['是否复测'] == '是']['飞机SN'].unique()
            retested_count = len(sn_has_retest)
            retest_rate = (retested_count / unique_aircraft_count * 100) if unique_aircraft_count > 0 else 0

            # 修正复测通过数:只有当飞机SN在所有频率(2G和5G)下复测后都通过时才计入
            # 先找出有复测的飞机SN
            retest_sn_list = summary_df[summary_df['是否复测'] == '是']['飞机SN'].unique()

            # 计算每个有复测的飞机SN的最终结果
            retest_sn_results = []
            for sn in retest_sn_list:
                sn_data = summary_df[summary_df['飞机SN'] == sn]
                # 检查该SN的所有频率的最终结果
                all_pass = all(result == 'PASS' for result in sn_data['最终结果'])
                if all_pass:  # 只有该SN的所有频率都通过时才计入复测通过数
                    retest_sn_results.append(sn)

            retest_success_count = len(retest_sn_results)

            # 创建总体统计摘要
            rate_summary = {
                '统计项目': [
                    'SN数量',
                    '2G测试总数量',
                    '5G测试总数量',
                    '总测试数量',
                    '一次通过数(所有SN首次测试均通过的数量)',
                    '一次通过率(首次测试均通过的SN比例)',
                    '复测飞机SN数量(有复测的SN数量)',
                    '复测通过数(复测后均通过的SN数量)',
                    '复测率(有复测的SN比例)'
                ],
                '数值': [
                    unique_aircraft_count,
                    g2_tests,
                    g5_tests,
                    total_tests,
                    first_pass_total,
                    f"{first_pass_rate:.2f}%",
                    retested_count,
                    retest_success_count,
                    f"{retest_rate:.2f}%"
                ]
            }

            rate_summary_df = pd.DataFrame(rate_summary)

            # 将结果写入Excel文件
            import os

            # 检查文件是否存在,如果存在则追加,否则创建
            if os.path.exists(output_file_path):
                with pd.ExcelWriter(output_file_path, mode='a', engine='openpyxl', if_sheet_exists='replace') as writer:
                    summary_df.to_excel(writer, sheet_name='各产品测试详情', index=False)
                    rate_summary_df.to_excel(writer, sheet_name=pass_rate_sheet_name, index=False)
            else:
                with pd.ExcelWriter(output_file_path, engine='openpyxl') as writer:
                    summary_df.to_excel(writer, sheet_name='各产品测试详情', index=False)
                    rate_summary_df.to_excel(writer, sheet_name=pass_rate_sheet_name, index=False)

            print(f"通过率分析完成:")
            print(f"- 飞机SN数量: {unique_aircraft_count}")
            print(f"- 2G测试总数量: {g2_tests}")
            print(f"- 5G测试总数量: {g5_tests}")
            print(f"- 总测试数量: {total_tests}")
            print(f"- 一次通过数(所有飞机SN首次测试均通过的数量): {first_pass_total}")
            print(f"- 一次通过率(首次测试均通过的飞机SN比例): {first_pass_rate:.2f}%")
            print(f"- 复测飞机SN数量: {retested_count}")
            print(f"- 复测通过数(复测后所有频率均通过的飞机SN数量): {retest_success_count}")
            print(f"- 复测率(有复测的飞机SN比例): {retest_rate:.2f}%")
            print(f"- 通过率统计已保存到 {output_file_path} 的 '{pass_rate_sheet_name}' 工作表")

            return summary_df, rate_summary_df

        except Exception as e:
            print(f"通过率分析过程中发生错误: {str(e)}")
            import traceback
            traceback.print_exc()
            return pd.DataFrame(), pd.DataFrame()

    def process_remote_sn_data(self, df):
        """处理遥控器SN列的数据,对以'1910'开头的数据行进行特定列移位"""
        if '遥控器SN' not in df.columns:
            print("警告:未找到'遥控器SN'列")
            return df

        # 复制数据框避免修改原始数据
        processed_df = df.copy()

        # 获取遥控器SN列的所有值
        sn_series = processed_df['遥控器SN']

        # 找出以'1910'开头的行(排除表头)
        mask_1910 = sn_series.astype(str).str.startswith('1910')

        # 获取遥控器SN列的索引位置
        sn_col_index = df.columns.get_loc('遥控器SN') if '遥控器SN' in df.columns else 0

        # 对于以'1910'开头的数据行,将从遥控器SN列开始的所有列向后移动一列
        for idx in processed_df[mask_1910].index:
            # 获取当前行的数据
            row_values = processed_df.loc[idx].values

            # 创建新行数据:从遥控器SN列开始的数据后移,前面的数据保持不变
            new_row_values = []

            # 添加遥控器SN列之前的数据(不改变)
            for i in range(sn_col_index):
                new_row_values.append(row_values[i])

            # 在遥控器SN列位置插入NaN
            new_row_values.append(None)  # 插入None到遥控器SN列位置

            # 将从遥控器SN列开始的原始数据追加到新行
            for i in range(sn_col_index, len(row_values)):
                new_row_values.append(row_values[i])

            # 更新当前行的数据
            for col_idx, col_name in enumerate(processed_df.columns):
                if col_idx < len(new_row_values):
                    processed_df.at[idx, col_name] = new_row_values[col_idx]

            # 如果还有剩余的值,需要添加新列
            if len(new_row_values) > len(processed_df.columns):
                for extra_idx in range(len(processed_df.columns), len(new_row_values)):
                    new_col_name = f"额外列_{extra_idx - len(processed_df.columns) + 1}"
                    if new_col_name not in processed_df.columns:
                        processed_df[new_col_name] = None
                    processed_df.at[idx, new_col_name] = new_row_values[extra_idx]

        # 检查以'R'开头的数据(不需要操作)
        mask_R = sn_series.astype(str).str.startswith('R')
        r_count = len(processed_df[mask_R])
        print(f"发现 {r_count} 行以'R'开头的数据(无需处理)")

        return processed_df

    def find_coupling_test_folder(self, base_path):
        """递归搜索包含"CouplingTest"的文件夹"""
        for root, dirs, files in os.walk(base_path):
            for d in dirs:
                if "CouplingTest" in d:
                    return os.path.join(root, d)
        return None

    def process_folder(self, input_folder, output_folder, progress_callback=None, log_callback=None):
        """处理指定文件夹中的所有Excel文件"""
        # 创建输出文件夹
        os.makedirs(output_folder, exist_ok=True)

        # 获取输入文件夹中所有的Excel文件
        excel_files = [f for f in os.listdir(input_folder) if f.endswith(('.xlsx', '.xls', '.xlsm'))]

        if not excel_files:
            log_callback(f"在 {input_folder} 中未找到Excel文件")
            return

        total_files = len(excel_files)
        
        for idx, excel_file in enumerate(excel_files):
            file_path = os.path.join(input_folder, excel_file)
            log_callback(f"正在处理文件: {excel_file}")

            try:
                # 读取Excel文件
                df = pd.read_excel(file_path, dtype=str)  # 强制将所有数据读取为字符串类型

                # 首先处理遥控器SN列
                df = self.process_remote_sn_data(df)

                # 对所有相关列进行拆分处理
                columns_to_process = [
                    'freq_band',  # 添加频率带宽列
                    'frequency',  # 添加频率列
                    '频段',
                    'rc_gain_a均值', 'rc_gain_b均值', 'rc_snr均值',
                    'fc_gain_a均值', 'fc_gain_b均值', 'fc_snr均值',
                    'tlv_err均值', 'num_err均值', '错误信息'
                ]

                for column_name in columns_to_process:
                    if column_name in df.columns:
                        df = self.split_column_data_inplace(df, column_name)

                # 生成输出文件名(原文件名+拆分数据)
                base_name = os.path.splitext(excel_file)[0]
                ext = os.path.splitext(excel_file)[1]
                output_file_name = f"{base_name}拆分数据{ext}"
                output_file_path = os.path.join(output_folder, output_file_name)

                # 使用修正后的函数,包含去重、频段筛选
                deduplicated_df, filtered_2g, filtered_5g = self.remove_duplicates_and_write_to_new_sheet(
                    df,
                    output_file_path,
                    original_sheet_name='原始处理数据',
                    deduplicated_sheet_name='去重后数据',
                    filter_2g_sheet='2G数据',
                    filter_5g_sheet='5G数据'
                )

                # 添加通过率分析(注意:需要确保有飞机SN列)
                self.analyze_pass_rate_by_aircraft_sn(df, output_file_path)

                # 更新进度
                if progress_callback:
                    progress_callback(int((idx + 1) / total_files * 100))

            except Exception as e:
                log_callback(f"处理文件 {excel_file} 时出错: {str(e)}")


class RemoteDataProcessor:
    """遥控器数据处理核心类 - 遥控器射频耦合测试数据处理"""
    
    def extract_data_from_txt_files(self, folder_path):
        """
        从指定文件夹及其子文件夹中的txt文件中提取数据
        验证路径中是否包含"Coupling"文件夹
        """
        # 检查整个路径中是否包含"Coupling"文件夹
        has_coupling = False
        for root, dirs, files in os.walk(folder_path):
            if "Coupling" in dirs or "coupling" in dirs:  # 不区分大小写
                has_coupling = True
                break

        # 如果路径中没有Coupling文件夹,给出警告但仍允许处理
        if not has_coupling:
            print(f"警告: 未在路径 {folder_path} 中找到 'Coupling' 文件夹")

        # 修改正则表达式,将 device_id 替换为 RC_SN
        pattern = r'(\d{4}-\d{2}-\d{2}-\d{2}:\d{2}:\d{2}),\s*freq_band:([25]G)\s*(NG|PASS)\s*([A-Z0-9]+)\s*rc_gain_a均值:(\d+)\s*rc_gain_b均值:(\d+)\s*rc_snr均值:(\d+)\s*fc_gain_a均值:(\d+)\s*fc_gain_b均值:(\d+)\s*fc_snr均值:(\d+)\s*tlv_err均值:(\d+)\s*num_err均值:(\d+)\s*错误信息:(.*)'

        extracted_data = []

        # 遍历所有文件,不限制是否在Coupling文件夹中
        for root, dirs, files in os.walk(folder_path):
            for filename in files:
                if filename.endswith('.txt'):
                    file_path = os.path.join(root, filename)

                    with open(file_path, 'r', encoding='gbk') as file:
                        for line_num, line in enumerate(file, 1):
                            line = line.strip()
                            if not line:
                                continue

                            match = re.search(pattern, line)
                            if match:
                                date_part = match.group(1).split('-')[0:3]
                                date_str = '-'.join(date_part)

                                data = {
                                    'source_file': file_path,
                                    'line_number': line_num,
                                    'date_part': date_str,
                                    'timestamp': match.group(1),
                                    'freq_band': match.group(2),
                                    'status': match.group(3),
                                    'RC_SN': match.group(4),  # 将 device_id 替换为 RC_SN
                                    'rc_gain_a_mean': int(match.group(5)),
                                    'rc_gain_b_mean': int(match.group(6)),
                                    'rc_snr_mean': int(match.group(7)),
                                    'fc_gain_a_mean': int(match.group(8)),
                                    'fc_gain_b_mean': int(match.group(9)),
                                    'fc_snr_mean': int(match.group(10)),
                                    'tlv_err_mean': int(match.group(11)),
                                    'num_err_mean': int(match.group(12)),
                                    'error_info': match.group(13).strip()
                                }
                                extracted_data.append(data)

        return extracted_data

    def remove_duplicate_sn_by_latest(self, data_list):
        """
        按RC_SN和freq_band去重,保留每个组合的最新测试结果
        """
        # 使用 NumPy 2.x 兼容的排序方法
        sorted_data = sorted(data_list, key=lambda x: (x['RC_SN'], x['freq_band'], x['timestamp']))

        latest_data = {}

        for item in sorted_data:
            key = f"{item['RC_SN']}_{item['freq_band']}"  # 使用 RC_SN 替代 device_id
            latest_data[key] = item

        return list(latest_data.values())

    def process_data_with_statistics(self, data, output_path, status_callback=None):
        """
        处理数据并生成统计信息
        将所有数据输出到一个Excel文件的不同sheet中:
        - 原始数据
        - 去重数据
        - 2G数据
        - 5G数据
        - 详细统计
        - 汇总统计
        """
        if not data:
            return []

        # 按日期分组数据
        date_groups = {}
        for item in data:
            date = item['date_part']
            if date not in date_groups:
                date_groups[date] = []
            date_groups[date].append(item)

        created_files = []

        for date, date_data in date_groups.items():
            date_folder = os.path.join(output_path, date)
            os.makedirs(date_folder, exist_ok=True)

            # 1. 准备原始数据
            raw_df_data = []
            for item in date_data:
                row = item.copy()
                del row['date_part']
                raw_df_data.append(row)
            raw_df = pd.DataFrame(raw_df_data)

            # 2. 准备去重数据
            unique_date_data = self.remove_duplicate_sn_by_latest(date_data)
            unique_df_data = []
            for item in unique_date_data:
                row = item.copy()
                del row['date_part']
                unique_df_data.append(row)
            unique_df = pd.DataFrame(unique_df_data)

            # 3. 准备2G和5G数据
            unique_2g_df = unique_df[unique_df['freq_band'] == '2G'].copy()
            unique_5g_df = unique_df[unique_df['freq_band'] == '5G'].copy()

            # 4. 计算统计信息
            # 按RC_SN分组数据,每个SN的所有测试(2G+5G)
            rc_groups = {}
            for item in date_data:
                rc_sn = item['RC_SN']
                if rc_sn not in rc_groups:
                    rc_groups[rc_sn] = []
                rc_groups[rc_sn].append(item)

            # 构建每个SN的测试记录
            sn_test_records = {}
            for rc_sn, items in rc_groups.items():
                # 按时间排序
                sorted_items = sorted(items, key=lambda x: x['timestamp'])

                # 按频段分组
                freq_data = {}
                for item in sorted_items:
                    freq_band = item['freq_band']
                    if freq_band not in freq_data:
                        freq_data[freq_band] = []
                    freq_data[freq_band].append(item)

                # 为每个频段计算测试次数和最终结果
                sn_test_records[rc_sn] = {}
                for freq_band, freq_items in freq_data.items():
                    sn_test_records[rc_sn][freq_band] = {
                        'test_count': len(freq_items),
                        'latest_result': freq_items[-1]['status'],
                        'all_results': [item['status'] for item in freq_items],
                        'timestamps': [item['timestamp'] for item in freq_items]
                    }

            # 计算统计信息
            daily_stats = []
            total_sn_count = len(sn_test_records)  # 总SN数量

            # 按SN整体统计
            passed_once_count = 0  # 一次通过的SN数量(2G和5G均一次通过)
            retry_sn_count = 0     # 需要复测的SN数量(至少一个频段测试次数>1)
            retry_test_count = 0   # 复测次数(所有需要复测的SN的复测次数之和)
            failed_sn_count = 0    # 最终失败的SN数量
            both_freq_sn_count = 0 # 同时包含2G和5G数据的SN数量

            # 测试总次数 = SN数量 + 2G测试数据总和 + 5G测试数据总和
            total_2g_tests = 0
            total_5g_tests = 0

            for rc_sn, freq_data in sn_test_records.items():
                # 检查该SN是否同时包含2G和5G数据
                has_2g = '2G' in freq_data
                has_5g = '5G' in freq_data
                has_both_freq = has_2g and has_5g

                if has_both_freq:
                    both_freq_sn_count += 1

                # 统计2G和5G测试次数
                if has_2g:
                    total_2g_tests += freq_data['2G']['test_count']
                if has_5g:
                    total_5g_tests += freq_data['5G']['test_count']

                # 检查所有频段是否都通过
                all_freq_passed = all(
                    info['latest_result'] == 'PASS'
                    for info in freq_data.values()
                )

                # 检查所有频段是否都是一次通过(测试次数=1且结果为PASS)
                all_freq_once_passed = all(
                    info['test_count'] == 1 and info['latest_result'] == 'PASS'
                    for info in freq_data.values()
                )

                # 检查是否有频段需要复测
                sn_needs_retry = any(
                    info['test_count'] > 1
                    for info in freq_data.values()
                )

                # 统计该SN的复测次数
                sn_retry_count = sum(
                    max(0, info['test_count'] - 1)  # 每个频段超过1次的部分为复测次数
                    for info in freq_data.values()
                )

                # 判断是否为"一次通过":2G和5G都存在且都是一次通过
                is_once_passed = has_both_freq and all_freq_once_passed

                # 判断是否为"需要复测":至少一个频段测试次数>1
                is_retry_needed = sn_needs_retry

                # 判断是否为"最终失败":至少一个频段最终结果不是PASS
                is_final_failed = not all_freq_passed

                if is_once_passed:
                    passed_once_count += 1

                if is_retry_needed:
                    retry_sn_count += 1  # 需要复测的SN数量
                    retry_test_count += sn_retry_count  # 总复测次数

                if is_final_failed:
                    failed_sn_count += 1

                # 记录每个SN的详细信息
                daily_stats.append({
                    'RC_SN': rc_sn,
                    '2G测试次数': freq_data.get('2G', {}).get('test_count', 0),
                    '2G最终结果': freq_data.get('2G', {}).get('latest_result', 'N/A'),
                    '2G首次结果': freq_data.get('2G', {}).get('all_results', [None])[0] if freq_data.get('2G', {}).get('all_results') else 'N/A',
                    '5G测试次数': freq_data.get('5G', {}).get('test_count', 0),
                    '5G最终结果': freq_data.get('5G', {}).get('latest_result', 'N/A'),
                    '5G首次结果': freq_data.get('5G', {}).get('all_results', [None])[0] if freq_data.get('5G', {}).get('all_results') else 'N/A',
                    '复测次数': sn_retry_count,
                    '是否同时有2G和5G': has_both_freq,
                    '是否一次通过': is_once_passed,
                    '是否最终通过': not is_final_failed,
                    '是否需要复测': is_retry_needed
                })

            # 计算测试总次数 = SN数量 + 2G测试数据总和 + 5G测试数据总和
            total_test_count = total_sn_count + total_2g_tests + total_5g_tests

            # 计算各种通过率
            once_pass_rate = (passed_once_count / both_freq_sn_count * 100) if both_freq_sn_count > 0 else 0
            retry_rate = (retry_sn_count / total_sn_count * 100) if total_sn_count > 0 else 0
            final_pass_rate = ((total_sn_count - failed_sn_count) / total_sn_count * 100) if total_sn_count > 0 else 0

            # 创建统计DataFrame
            stats_df = pd.DataFrame(daily_stats)

            # 创建汇总统计
            summary_data = {
                '统计项目': [
                    '当天测试SN总数',
                    '同时包含2G和5G数据的SN数量',
                    '一次通过SN数量(2G和5G均一次通过)',
                    '一次通过率',
                    '最终通过SN数量',
                    '最终通过率',
                    '失败SN数量',
                    '失败率',
                    '需要复测的SN数量',
                    '复测率',
                    '复测次数',
                    '2G测试数据总和',
                    '5G测试数据总和',
                    '测试总次数(SN+2G+5G测试数据总和)'
                ],
                '数值': [
                    total_sn_count,
                    both_freq_sn_count,
                    passed_once_count,
                    f"{once_pass_rate:.2f}%",
                    total_sn_count - failed_sn_count,
                    f"{final_pass_rate:.2f}%",
                    failed_sn_count,
                    f"{100-final_pass_rate:.2f}%",
                    retry_sn_count,
                    f"{retry_rate:.2f}%",
                    retry_test_count,
                    total_2g_tests,
                    total_5g_tests,
                    total_test_count
                ]
            }
            summary_df = pd.DataFrame(summary_data)

            # 5. 将所有数据写入一个Excel文件的不同sheet中
            output_file = os.path.join(date_folder, f"{date}_数据分析报告.xlsx")

            with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
                # Sheet 1: 汇总统计(放在最前面,方便查看)
                summary_df.to_excel(writer, sheet_name='汇总统计', index=False)

                # Sheet 2: 详细统计
                stats_df.to_excel(writer, sheet_name='详细统计', index=False)

                # Sheet 3: 原始数据
                raw_df.to_excel(writer, sheet_name='原始数据', index=False)

                # Sheet 4: 去重数据
                unique_df.to_excel(writer, sheet_name='去重数据', index=False)

                # Sheet 5: 2G数据
                if not unique_2g_df.empty:
                    unique_2g_df.to_excel(writer, sheet_name='2G数据', index=False)

                # Sheet 6: 5G数据
                if not unique_5g_df.empty:
                    unique_5g_df.to_excel(writer, sheet_name='5G数据', index=False)

            # 通过回调函数发送日志信息到UI
            log_message = f"数据分析报告已保存到 {output_file}"
            if status_callback:
                status_callback(log_message)
            else:
                print(log_message)

            created_files.append(output_file)

        return created_files


class AircraftProcessingThread(QThread):
    """飞机数据处理线程"""
    progress_signal = Signal(int)
    log_signal = Signal(str)
    finished_signal = Signal()

    def __init__(self, processor, input_folder, output_folder):
        super().__init__()
        self.processor = processor
        self.input_folder = input_folder
        self.output_folder = output_folder

    def run(self):
        try:
            # 自动搜索CouplingTest文件夹
            coupling_folder = self.processor.find_coupling_test_folder(self.input_folder)
            if coupling_folder:
                self.log_signal.emit(f"找到CouplingTest文件夹: {coupling_folder}")
                actual_input_folder = coupling_folder
            else:
                self.log_signal.emit(f"未找到包含'CouplingTest'的文件夹,使用指定的输入文件夹: {self.input_folder}")
                actual_input_folder = self.input_folder

            self.processor.process_folder(
                actual_input_folder, 
                self.output_folder,
                progress_callback=self.progress_signal.emit,
                log_callback=self.log_signal.emit
            )
            self.log_signal.emit("处理完成!")
        except Exception as e:
            self.log_signal.emit(f"处理过程中发生错误: {str(e)}")
        finally:
            self.finished_signal.emit()


class RemoteProcessingThread(QThread):
    """遥控器数据处理线程"""
    progress_signal = Signal(int)
    log_signal = Signal(str)
    finished_signal = Signal(list)

    def __init__(self, processor, input_folder, output_folder):
        super().__init__()
        self.processor = processor
        self.input_folder = input_folder
        self.output_folder = output_folder

    def run(self):
        try:
            # 提取数据
            self.log_signal.emit("正在读取txt文件并提取数据...")
            extracted_data = self.processor.extract_data_from_txt_files(self.input_folder)

            if not extracted_data:
                self.log_signal.emit("未找到匹配的数据")
                self.finished_signal.emit([])
                return

            # 处理数据 - 包含通过率统计
            self.log_signal.emit("正在处理数据并计算通过率...")
            created_files = self.processor.process_data_with_statistics(
                extracted_data,
                self.output_folder,
                status_callback=lambda msg: self.log_signal.emit(msg)
            )

            self.log_signal.emit(f"处理完成!共创建了 {len(created_files)} 个文件")
            self.finished_signal.emit(created_files)
        except Exception as e:
            self.log_signal.emit(f"处理过程中发生错误: {str(e)}")
            self.finished_signal.emit([])


class MainWindow(QMainWindow):
    """主窗口"""
    
    def __init__(self):
        super().__init__()
        self.setWindowTitle("射频数据处理器")
        self.setGeometry(100, 100, 800, 600)
        
        # 创建数据处理器实例
        self.aircraft_processor = DataProcessor()
        self.remote_processor = RemoteDataProcessor()
        
        # 创建中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 创建布局
        layout = QVBoxLayout(central_widget)
        
        # 创建标签页
        tab_widget = QTabWidget()
        layout.addWidget(tab_widget)
        
        # 飞机数据处理标签页
        self.aircraft_tab = QWidget()
        self.setup_aircraft_tab()
        tab_widget.addTab(self.aircraft_tab, "飞机数据处理")
        
        # 遥控器数据处理标签页
        self.remote_tab = QWidget()
        self.setup_remote_tab()
        tab_widget.addTab(self.remote_tab, "遥控器数据处理")
        
        # 关于标签页
        about_tab = QWidget()
        about_layout = QVBoxLayout(about_tab)
        about_label = QLabel(
            "<h2>射频数据处理器</h2>"
            "<p>本工具用于处理射频测试数据,支持:</p>"
            "<ul>"
            "<li>飞机射频耦合测试数据处理</li>"
            "<li>遥控器射频耦合测试数据处理</li>"
            "<li>数据拆分和格式化</li>"
            "<li>去重和频段筛选</li>"
            "<li>通过率统计分析</li>"
            "</ul>"
            "<p>开发者: 射频数据处理团队</p>"
        )
        about_label.setWordWrap(True)
        about_layout.addWidget(about_label)
        tab_widget.addTab(about_tab, "关于")
    
    def setup_aircraft_tab(self):
        """设置飞机数据处理标签页"""
        layout = QVBoxLayout(self.aircraft_tab)
        
        # 输入文件夹选择
        input_group = QGroupBox("输入设置")
        input_layout = QFormLayout(input_group)
        
        input_hbox = QHBoxLayout()
        self.aircraft_input_folder_line = QLineEdit()
        self.aircraft_input_folder_btn = QPushButton("浏览")
        self.aircraft_input_folder_btn.clicked.connect(self.browse_aircraft_input_folder)
        input_hbox.addWidget(self.aircraft_input_folder_line)
        input_hbox.addWidget(self.aircraft_input_folder_btn)
        input_layout.addRow("输入文件夹:", input_hbox)
        
        # 说明文字
        info_label = QLabel("注意:程序会自动搜索当前文件夹下的CouplingTest文件夹进行处理")
        info_label.setStyleSheet("color: blue; font-size: 12px; margin: 5px;")
        info_label.setWordWrap(True)
        input_layout.addRow("", info_label)
        
        layout.addWidget(input_group)
        
        # 输出文件夹选择
        output_group = QGroupBox("输出设置")
        output_layout = QFormLayout(output_group)
        
        output_hbox = QHBoxLayout()
        self.aircraft_output_folder_line = QLineEdit()
        self.aircraft_output_folder_btn = QPushButton("浏览")
        self.aircraft_output_folder_btn.clicked.connect(self.browse_aircraft_output_folder)
        output_hbox.addWidget(self.aircraft_output_folder_line)
        output_hbox.addWidget(self.aircraft_output_folder_btn)
        output_layout.addRow("输出文件夹:", output_hbox)
        
        layout.addWidget(output_group)
        
        # 控制按钮
        button_layout = QHBoxLayout()
        self.aircraft_process_btn = QPushButton("开始处理")
        self.aircraft_process_btn.clicked.connect(self.start_aircraft_processing)
        self.aircraft_process_btn.setEnabled(False)  # 初始禁用
        button_layout.addWidget(self.aircraft_process_btn)
        layout.addLayout(button_layout)
        
        # 进度条
        self.aircraft_progress_bar = QProgressBar()
        self.aircraft_progress_bar.setRange(0, 100)
        self.aircraft_progress_bar.setValue(0)
        layout.addWidget(self.aircraft_progress_bar)
        
        # 日志区域
        log_group = QGroupBox("处理日志")
        log_layout = QVBoxLayout(log_group)
        self.aircraft_log_text = QTextEdit()
        self.aircraft_log_text.setMaximumHeight(200)
        log_layout.addWidget(self.aircraft_log_text)
        layout.addWidget(log_group)
    
    def setup_remote_tab(self):
        """设置遥控器数据处理标签页"""
        layout = QVBoxLayout(self.remote_tab)
        
        # 数据类型说明
        info_label = QLabel("注意:请确保输入文件夹路径包含 'Coupling' 文件夹,这是遥控器射频耦合测试数据所在的位置")
        info_label.setStyleSheet("color: blue; font-size: 12px; margin: 5px;")
        info_label.setWordWrap(True)
        layout.addWidget(info_label)
        
        # 输入文件夹选择
        input_group = QGroupBox("输入文件夹 (应包含 Coupling 子文件夹)")
        input_layout = QHBoxLayout()

        self.remote_input_label = QLabel("未选择")
        self.remote_input_button = QPushButton("选择输入文件夹")
        self.remote_input_button.clicked.connect(self.select_remote_input_folder)

        input_layout.addWidget(self.remote_input_label)
        input_layout.addWidget(self.remote_input_button)
        input_group.setLayout(input_layout)
        layout.addWidget(input_group)

        # 输出文件夹选择
        output_group = QGroupBox("输出文件夹")
        output_layout = QHBoxLayout()

        self.remote_output_label = QLabel("未选择")
        self.remote_output_button = QPushButton("选择输出文件夹")
        self.remote_output_button.clicked.connect(self.select_remote_output_folder)

        output_layout.addWidget(self.remote_output_label)
        output_layout.addWidget(self.remote_output_button)
        output_group.setLayout(output_layout)
        layout.addWidget(output_group)

        # 处理按钮
        self.remote_process_button = QPushButton("开始处理遥控器耦合数据")
        self.remote_process_button.clicked.connect(self.start_remote_processing)
        self.remote_process_button.setEnabled(False)
        layout.addWidget(self.remote_process_button)

        # 进度条
        self.remote_progress_bar = QProgressBar()
        self.remote_progress_bar.setVisible(False)
        layout.addWidget(self.remote_progress_bar)

        # 状态显示
        self.remote_status_text = QTextEdit()
        self.remote_status_text.setMaximumHeight(150)
        layout.addWidget(self.remote_status_text)

        # 结果显示
        self.remote_result_text = QTextEdit()
        self.remote_result_text.setReadOnly(True)
        layout.addWidget(self.remote_result_text)
    
    def browse_aircraft_input_folder(self):
        """浏览飞机数据输入文件夹"""
        folder = QFileDialog.getExistingDirectory(self, "选择输入文件夹")
        if folder:
            self.aircraft_input_folder_line.setText(folder)
            # 启用处理按钮
            self.check_aircraft_enable_process_btn()
    
    def browse_aircraft_output_folder(self):
        """浏览飞机数据输出文件夹"""
        folder = QFileDialog.getExistingDirectory(self, "选择输出文件夹")
        if folder:
            self.aircraft_output_folder_line.setText(folder)
            # 启用处理按钮
            self.check_aircraft_enable_process_btn()
    
    def check_aircraft_enable_process_btn(self):
        """检查是否启用飞机数据处理按钮"""
        input_path = self.aircraft_input_folder_line.text()
        output_path = self.aircraft_output_folder_line.text()
        self.aircraft_process_btn.setEnabled(bool(input_path and output_path))
    
    def start_aircraft_processing(self):
        """开始处理飞机数据"""
        input_path = self.aircraft_input_folder_line.text()
        output_path = self.aircraft_output_folder_line.text()
        
        if not input_path or not output_path:
            QMessageBox.warning(self, "警告", "请输入文件夹和输出文件夹")
            return
        
        # 禁用按钮
        self.aircraft_process_btn.setEnabled(False)
        
        # 创建处理线程
        self.aircraft_thread = AircraftProcessingThread(self.aircraft_processor, input_path, output_path)
        self.aircraft_thread.progress_signal.connect(self.update_aircraft_progress)
        self.aircraft_thread.log_signal.connect(self.log_aircraft_message)
        self.aircraft_thread.finished_signal.connect(self.aircraft_processing_finished)
        self.aircraft_thread.start()
    
    def update_aircraft_progress(self, value):
        """更新飞机数据处理进度条"""
        self.aircraft_progress_bar.setValue(value)
    
    def log_aircraft_message(self, message):
        """添加飞机数据处理日志消息"""
        self.aircraft_log_text.append(message)
    
    def aircraft_processing_finished(self):
        """飞机数据处理完成"""
        self.aircraft_process_btn.setEnabled(True)
        self.aircraft_progress_bar.setValue(0)
    
    def select_remote_input_folder(self):
        """选择遥控器数据输入文件夹"""
        folder = QFileDialog.getExistingDirectory(self, "选择包含 Coupling 文件夹的输入路径")
        if folder:
            # 检查是否包含Coupling文件夹
            has_coupling = False
            for root, dirs, files in os.walk(folder):
                if any("Coupling" in d or "coupling" in d for d in dirs):
                    has_coupling = True
                    break

            # 显示警告信息但允许继续
            if not has_coupling:
                QMessageBox.warning(
                    self,
                    "警告",
                    f"选定的文件夹 '{folder}' 中未发现 'Coupling' 子文件夹。\n\n"
                    "此工具专门用于处理遥控器射频耦合测试数据,通常存储在 'Coupling' 文件夹中。\n\n"
                    "如果数据在其他文件夹中,仍可继续处理。"
                )

            self.remote_input_label.setText(folder)
            self.check_remote_ready_to_process()

    def select_remote_output_folder(self):
        """选择遥控器数据输出文件夹"""
        folder = QFileDialog.getExistingDirectory(self, "选择输出文件夹")
        if folder:
            self.remote_output_label.setText(folder)
            self.check_remote_ready_to_process()

    def check_remote_ready_to_process(self):
        """检查是否准备好处理遥控器数据"""
        if self.remote_input_label.text() != "未选择" and self.remote_output_label.text() != "未选择":
            self.remote_process_button.setEnabled(True)
        else:
            self.remote_process_button.setEnabled(False)

    def start_remote_processing(self):
        """开始处理遥控器数据"""
        input_folder = self.remote_input_label.text()
        output_folder = self.remote_output_label.text()

        if not os.path.exists(input_folder):
            QMessageBox.warning(self, "警告", "输入文件夹不存在!")
            return

        if not os.path.exists(output_folder):
            QMessageBox.warning(self, "警告", "输出文件夹不存在!")
            return

        # 验证是否包含Coupling文件夹
        has_coupling = False
        for root, dirs, files in os.walk(input_folder):
            if "Coupling" in dirs:
                has_coupling = True
                break

        if not has_coupling:
            reply = QMessageBox.question(
                self,
                "确认",
                "选定的输入文件夹中未发现 'Coupling' 子文件夹。\n\n"
                "此工具专门用于处理遥控器射频耦合测试数据,通常存储在 'Coupling' 文件夹中。\n\n"
                "是否仍要继续处理?",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No
            )
            if reply == QMessageBox.No:
                return

        self.remote_process_button.setEnabled(False)
        self.remote_progress_bar.setVisible(True)
        self.remote_status_text.append("开始处理遥控器射频耦合测试数据...")

        self.remote_thread = RemoteProcessingThread(self.remote_processor, input_folder, output_folder)
        self.remote_thread.progress_signal.connect(self.update_remote_progress)
        self.remote_thread.log_signal.connect(self.update_remote_status)
        self.remote_thread.finished_signal.connect(self.remote_processing_finished)
        self.remote_thread.start()

    def update_remote_progress(self, value):
        """更新遥控器数据处理进度"""
        self.remote_progress_bar.setValue(value)

    def update_remote_status(self, message):
        """更新遥控器数据处理状态"""
        self.remote_status_text.append(message)

    def remote_processing_finished(self, created_files):
        """遥控器数据处理完成"""
        self.remote_progress_bar.setVisible(False)
        self.remote_process_button.setEnabled(True)

        if created_files:
            result_text = f"处理完成!共创建了 {len(created_files)} 个文件:\n"
            for file_path in created_files:
                result_text += f"- {file_path}\n"
            self.remote_result_text.setPlainText(result_text)
        else:
            self.remote_result_text.setPlainText("处理完成,但没有创建任何文件。")


def main():
    app = QApplication(sys.argv)
    
    # 设置应用样式
    app.setStyle('Fusion')
    
    window = MainWindow()
    window.show()
    
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
相关推荐
不脱发的程序猿2 小时前
使用Python高效对比多个相似的CAN DBC数据
python·单片机·嵌入式硬件·嵌入式
winfredzhang2 小时前
构建自动化 Node.js 项目管理工具:从文件夹监控到一键联动运行
chrome·python·sqlite·node.js·端口·运行js
AI_56782 小时前
Airflow“3分钟上手”教程:用Python定义定时数据清洗任务
开发语言·人工智能·python
Aurora-Borealis.2 小时前
Day 38 GPU训练和call方法
python
Ulyanov2 小时前
PyVista三维战场仿真实战
开发语言·python·tkinter·pyvista·gui开发
深蓝电商API2 小时前
Scrapy爬虫部署到Scrapyd服务端详解
爬虫·python·scrapy
无垠的广袤2 小时前
【工业树莓派 CM0 NANO 单板计算机】YOLO26 部署方案
linux·python·opencv·yolo·树莓派·目标识别
STLearner2 小时前
AAAI 2026 | 时间序列(Time Series) 论文总结[下] (分类,异常检测,基础模型,表示学习,生成)
大数据·论文阅读·人工智能·python·深度学习·机器学习·数据挖掘
科研鬼才(bushi3 小时前
项目文件夹规范
python