办公自动化:通过字符串相似度算法找出Excel 中的重复数据

背景

由于备件编号文件是多个工序人员手动登记录入,而数据库没做重复校验,导致很多库存品的项目内容其实是重复的,会有重复购买,备件冗余等情况。使用python,读取 Excel 中的库存描述数据,通过字符串相似度算法找出每条记录最相似的前 3 条记录,最终将结果整理后重新保存到原 Excel 文件中,再人工确认内容。

一、代码核心功能

  1. 字符串相似度计算 :通过difflib.SequenceMatcherquick_ratio()方法计算两个字符串的相似度,转换为百分比并保留 2 位小数。
  2. Excel 读写与格式化 :读取inventory data.xlsx的 Sheet1 数据,处理完成后将结果写回原文件,同时自动调整 Excel 列宽以适配内容。
  3. 相似记录筛选:对每条记录,遍历后续所有记录计算相似度,筛选出相似度最高的前 3 条,记录其相似度、行号、内容和 SID。
  4. 多进程并行处理 :使用multiprocessing.Pool创建进程池,拆分数据任务进行并行处理,尝试提升大批量数据的处理效率。
  5. 进度可视化 :使用tqdm创建进度条,展示进程处理进度和文件保存前的结果汇总进度。

二、代码

  1. Excel 格式优化saveexcel函数中自动检测日期类型列并转换为字符串,同时计算每列内容的最大长度(包含列名),自动调整 Excel 列宽(+4 预留缓冲),避免导出的 Excel 出现内容被遮挡的问题。

  2. 字符串相似度计算 :封装string_similar函数,利用difflib内置的quick_ratio()方法快速计算相似度,兼顾简洁性和计算效率,适合批量字符串对比场景。

  3. 任务拆分 :动态设置进程池大小:自动适配 CPU 核心数,避免资源浪费。每个子进程仅接收拆分后的子数据(而非完整DataFrame),使用iloc提取视图、values获取数组,提升遍历效率,处理完成后返回子结果DataFrame,最后在主进程汇总,使用numpy.argpartition快速提取前 N 大值,替代完整排序,大幅提升大数据量处理速度。。

  4. 多进程兼容性处理 :添加mp.freeze_support(),解决 Windows 环境下使用multiprocessing打包或运行时可能出现的初始化错误,提升了代码的跨平台兼容性。

    import datetime
    import difflib
    import math
    import multiprocessing as mp
    import numpy as np
    import pandas as pd
    from pandas import read_excel
    from tqdm import tqdm

    ===================== 配置参数(可根据需求修改) =====================

    EXCEL_INPUT_PATH = r"inventory data0.xlsx"
    EXCEL_OUTPUT_PATH = r"inventory data0_result.xlsx" # 输出新文件,避免覆盖原数据
    SHEET_NAME = "Sheet1"
    ROWS_PER_PROCESS = 10 # 每个进程处理的批次大小(比原5条更高效)
    TOP_SIMILAR_COUNT = 3 # 保留最相似的前N条记录
    PROCESS_POOL_SIZE = None # 自动适配CPU核心数,手动设置可改为具体数字(如4、8)

    ===================== 核心工具函数 =====================

    def string_similar(s1, s2):
    """计算两个字符串的相似度(百分比,保留2位小数)"""
    if not isinstance(s1, str) or not isinstance(s2, str):
    return 0.00
    return round(difflib.SequenceMatcher(None, s1, s2).quick_ratio() * 100, 2)

    def saveexcel(data, filename, sheetName, data2='', sheet2=''):
    """优化后的Excel保存函数,自动调整列宽,兼容日期类型"""
    # 复制数据避免修改原数据
    data_copy = data.copy()

    复制代码
     # 日期类型转换为字符串,避免Excel格式异常
     for idx, col in enumerate(data_copy):
         if data_copy[col].dtypes == 'datetime64[ns]':
             data_copy[col] = data_copy[col].astype(str)
    
     # 初始化Excel写入器
     try:
         writer = pd.ExcelWriter(filename, engine='xlsxwriter')
     except Exception as e:
         raise Exception(f"初始化Excel写入器失败:{str(e)}")
    
     # 写入第一个Sheet并调整列宽
     data_copy.to_excel(writer, sheet_name=sheetName, index=None)
     worksheet = writer.sheets[sheetName]
     for idx, col in enumerate(data_copy):
         series = data_copy[col]
         # 计算列宽(内容最大长度 + 列名长度 + 4缓冲)
         max_len = max(
             series.astype(str).map(len).max() if not series.empty else 0,
             len(str(series.name))
         ) + 4
         worksheet.set_column(idx, idx, max_len)
    
     # 写入第二个Sheet(可选)
     if sheet2 != "" and not isinstance(data2, str) and not data2.empty:
         data2_copy = data2.copy()
         for idx, col in enumerate(data2_copy):
             if data2_copy[col].dtypes == 'datetime64[ns]':
                 data2_copy[col] = data2_copy[col].astype(str)
         data2_copy.to_excel(writer, sheet_name=sheet2, index=None)
         worksheet2 = writer.sheets[sheet2]
         for idx, col in enumerate(data2_copy):
             series = data2_copy[col]
             max_len = max(
                 series.astype(str).map(len).max() if not series.empty else 0,
                 len(str(series.name))
             ) + 4
             worksheet2.set_column(idx, idx, max_len)
    
     # 保存文件
     try:
         writer.close()
     except Exception as e:
         raise Exception(f"保存Excel文件失败:{str(e)}")

    ===================== 多进程处理函数 =====================

    def proceed(threadNo, full_data, start, end):
    """
    单个进程的处理逻辑:计算指定区间内每条记录的前N条相似记录
    优化点:避免数据完整拷贝,仅处理指定区间,返回处理后的子DataFrame
    """
    # 修正边界,防止超出数据长度
    end = min(end, len(full_data))
    if start >= end:
    return pd.DataFrame()

    复制代码
     # 提取当前进程需要处理的子数据(视图,非完整拷贝)
     sub_data = full_data.iloc[start:end].copy()
     full_desc = full_data['Long Description'].values
     full_sid = full_data['STOCK # \nSID#'].values
     total_rows = len(full_data)
    
     # 为子数据添加结果列
     for i in range(1, TOP_SIMILAR_COUNT + 1):
         sub_data[f'top{i}-rate'] = 0.00
         sub_data[f'top{i}-cell'] = 0
         sub_data[f'top{i}-data'] = ""
         sub_data[f'top{i}-SID'] = ""
    
     # 遍历当前进程的每条记录
     for sub_idx, orig_idx in enumerate(range(start, end)):
         # 存储当前记录与其他所有记录的相似度
         similarity_list = []
    
         # 遍历所有后续记录(避免重复计算)
         for j in range(orig_idx + 1, total_rows):
             sim_score = string_similar(full_desc[orig_idx], full_desc[j])
             similarity_list.append([j, sim_score])
    
         # 快速提取前N条最高相似度(使用numpy优化,避免完整排序)
         if len(similarity_list) > 0:
             # 转换为numpy数组提升效率
             sim_np = np.array(similarity_list, dtype=object)
             if len(sim_np) >= TOP_SIMILAR_COUNT:
                 # 快速分区提取前N大值,效率高于sorted
                 sim_scores = sim_np[:, 1].astype(float)
                 top_n_indices = np.argpartition(sim_scores, -TOP_SIMILAR_COUNT)[-TOP_SIMILAR_COUNT:]
                 top_n_items = sim_np[top_n_indices]
             else:
                 top_n_items = sim_np
    
             # 对前N条结果排序(从高到低)
             top_n_sorted = sorted(top_n_items, key=lambda x: float(x[1]), reverse=True)
    
             # 填充结果到子数据中
             for rank in range(min(len(top_n_sorted), TOP_SIMILAR_COUNT)):
                 j, score = top_n_sorted[rank]
                 j = int(j)
                 score = float(score)
                 col_prefix = f'top{rank + 1}-'
                 sub_data.iloc[sub_idx, sub_data.columns.get_loc(col_prefix + 'rate')] = score
                 sub_data.iloc[sub_idx, sub_data.columns.get_loc(col_prefix + 'cell')] = j + 2  # 对应Excel行号
                 sub_data.iloc[sub_idx, sub_data.columns.get_loc(col_prefix + 'data')] = full_desc[j]
                 sub_data.iloc[sub_idx, sub_data.columns.get_loc(col_prefix + 'SID')] = full_sid[j]
    
     return sub_data

    ===================== 回调函数(进度条更新) =====================

    def progress_callback(*args):
    """多进程回调函数,用于更新进度条"""
    global pbar
    if pbar is not None:
    pbar.update(1)

    ===================== 主程序入口 =====================

    if name == 'main':
    # 初始化多进程兼容(Windows环境必备)
    mp.freeze_support()
    pbar = None # 全局进度条变量

    复制代码
     try:
         # 1. 读取Excel数据并做合法性校验
         print(f"[{datetime.datetime.now()}] 开始读取Excel文件:{EXCEL_INPUT_PATH}")
         try:
             Rawdata0 = read_excel(
                 EXCEL_INPUT_PATH,
                 sheet_name=SHEET_NAME,
                 header=0,
                 keep_default_na=False
             )
         except FileNotFoundError:
             raise Exception(f"Excel文件未找到,请确认路径正确:{EXCEL_INPUT_PATH}")
         except KeyError:
             raise Exception(f"指定Sheet不存在,请确认Sheet名称:{SHEET_NAME}")
         except Exception as e:
             raise Exception(f"读取Excel失败:{str(e)}")
    
         # 校验必要列是否存在
         required_columns = ["Long Description", "STOCK # \nSID#", "NO"]
         missing_columns = [col for col in required_columns if col not in Rawdata0.columns]
         if missing_columns:
             raise Exception(f"Excel缺失必要列:{', '.join(missing_columns)}")
    
         print(f"[{datetime.datetime.now()}] 数据读取成功,总条数:{len(Rawdata0)}")
    
         # 2. 初始化多进程参数
         total_rows = len(Rawdata0)
         # 计算任务数(按每个进程处理ROWS_PER_PROCESS条数据拆分)
         task_count = math.ceil(total_rows / ROWS_PER_PROCESS)
         # 自动设置进程池大小(不超过CPU核心数和任务数)
         if PROCESS_POOL_SIZE is None:
             cpu_core_count = mp.cpu_count()
             pool_size = min(cpu_core_count, task_count)
         else:
             pool_size = min(PROCESS_POOL_SIZE, task_count)
    
         print(f"[{datetime.datetime.now()}] 初始化进程池,进程数:{pool_size},任务数:{task_count}")
    
         # 3. 启动多进程处理
         pool = mp.Pool(processes=pool_size)
         pbar = tqdm(total=task_count, desc='多进程处理进度')
         job_list = []
    
         for task_id in range(1, task_count + 1):
             start_idx = (task_id - 1) * ROWS_PER_PROCESS
             end_idx = task_id * ROWS_PER_PROCESS
             # 提交异步任务,绑定回调函数更新进度条
             job = pool.apply_async(
                 func=proceed,
                 args=(task_id, Rawdata0, start_idx, end_idx),
                 callback=progress_callback
             )
             job_list.append(job)
    
         # 等待所有进程完成
         pool.close()
         pool.join()
         pbar.close()
         print(f"[{datetime.datetime.now()}] 所有多进程任务处理完成")
    
         # 4. 汇总所有进程的结果
         print(f"[{datetime.datetime.now()}] 开始汇总处理结果")
         result_list = []
         tbar = tqdm(job_list, desc="结果汇总进度")
    
         for job in tbar:
             sub_result = job.get()
             if not sub_result.empty:
                 result_list.append(sub_result)
    
         tbar.close()
    
         # 拼接所有子结果
         if not result_list:
             raise Exception("未获取到任何处理结果,数据可能为空")
    
         final_result = pd.concat(result_list, ignore_index=True)
    
         # 5. 按NO列排序,保持数据原有顺序
         final_result = final_result.sort_values(by="NO", ascending=True, ignore_index=True)
    
         # 6. 保存结果到新Excel文件
         print(f"[{datetime.datetime.now()}] 开始保存结果到Excel:{EXCEL_OUTPUT_PATH}")
         saveexcel(final_result, EXCEL_OUTPUT_PATH, SHEET_NAME)
    
         print(f"[{datetime.datetime.now()}] 全部流程完成!结果已保存至:{EXCEL_OUTPUT_PATH}")
    
     except Exception as e:
         print(f"[{datetime.datetime.now()}] 程序运行出错:{str(e)}")

三、运行说明

  1. 确保已安装依赖库:pip install pandas xlsxwriter tqdm numpy
  2. 将原 Excel 文件命名为inventory data0.xlsx,与代码放在同一目录(或修改EXCEL_INPUT_PATH配置绝对路径)
  3. Excel 中必须包含Long DescriptionSTOCK # \nSID#NO三列(与原代码要求一致)
  4. 运行后,结果会保存为inventory data_result.xlsx,不会修改原文件
  5. 大批量数据(如 1000 + 条)可适当调大ROWS_PER_PROCESS(如 20、50),提升处理效率

四、结果确认

10803条中,重复度大于99%的数据有99条,经复核确实是重复建立了。90-99的也有部分重复。

相关推荐
骆驼爱记录15 分钟前
Word表格题注自动设置全攻略
开发语言·c#·自动化·word·excel·wps·新人首发
天荒地老笑话么1 小时前
Vim核心快捷键与运维实战指南
运维·vim·excel
开开心心就好20 小时前
键盘改键工具免安装,自定义键位屏蔽误触
java·网络·windows·随机森林·计算机外设·电脑·excel
fqbqrr1 天前
2601Mfc,自动化excel
自动化·excel·mfc
tlwlmy1 天前
python excel图片批量导出
开发语言·python·excel
TracyDemo2 天前
excel 透视图怎么进行删除透视图
excel
骆驼爱记录2 天前
Excel邮件合并嵌入图片技巧
自动化·word·excel·wps·新人首发
avi91112 天前
Unity Data Excel读取方法+踩坑记;和WPS Excel的一些命令
unity·游戏引擎·excel·wps·data
梦幻通灵2 天前
Excel多个sheet合并透视表实现方案【持续更新】
excel
开开心心就好2 天前
键盘映射工具改键位,绿色版设置后重启生效
网络·windows·tcp/ip·pdf·计算机外设·电脑·excel