办公自动化:通过字符串相似度算法找出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的也有部分重复。

相关推荐
寄思~2 小时前
Excel 数据匹配工具 -笔记
笔记·python·学习·excel
EasyCVR4 小时前
EasyCVR全栈视频技术:线下零售数字化智能视频监控体系建设实践
音视频·零售
222you7 小时前
智慧社区:居民信息Excel导入数据库
excel
霍格沃兹测试开发学社测试人社区7 小时前
Playwright数据驱动测试:从Excel与JSON获取测试数据指南
excel·playwright
关于不上作者榜就原神启动那件事20 小时前
Java中大量数据Excel导入导出的实现方案
java·开发语言·excel
骆驼爱记录21 小时前
Excel高效粘贴技巧:仅填充可见单元格
自动化·excel·wps·新人首发
说私域1 天前
云零售时代的S2B模式重构:AI智能名片与链动2+1模式的赋能路径
人工智能·重构·零售
SabreWulf20201 天前
Excel打开灰色空白无内容
excel
それども2 天前
Apache POI XSSFWorkbook 和 SXSSFWorkbook 的区别
apache·excel