吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8339|回复: 88
收起左侧

[Python 原创] 自动匹配提取Excel数据工具,这几天高考志愿填报帮了我的大忙。

  [复制链接]
风子是我 发表于 2025-7-2 14:54
本帖最后由 风子是我 于 2025-8-1 22:03 编辑

应楼下多位坛友的要求,我补上成品,百度网盘的:
通过网盘分享的文件:02_数据提取工具.rar等2个文件(原网盘失效,20250801更新网盘
链接: https://pan.baidu.com/s/1ZVUARkVn-09_t0C2ZtDRzg 提取码: 52pj
-先上成品运行截图
1.png
2.png

作用:比较两个excel文件,提取“数据文件2”中的数据到“数据文件1”
两个数据文件的行顺序和列顺序可以不同,会自动识别,但是两个数据文件的需要提取数据的列名必须存在且相同。
上全部代码:
[Python] 纯文本查看 复制代码
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import openpyxl
from openpyxl.utils import get_column_letter

class ExcelDataExtractorApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Excel数据提取工具")
        self.root.geometry("800x700")
        
        # 初始化变量
        self.file1_path = tk.StringVar()
        self.file2_path = tk.StringVar()
        self.compare_headers = []
        self.extract_headers = []
        
        # 创建界面
        self.create_widgets()
        
    def create_widgets(self):
        # 文件选择部分
        file_frame = ttk.LabelFrame(self.root, text="文件选择", padding=10)
        file_frame.pack(fill="x", padx=10, pady=5)
        
        ttk.Label(file_frame, text="数据文件1:").grid(row=0, column=0, sticky="w")
        ttk.Entry(file_frame, textvariable=self.file1_path, width=50).grid(row=0, column=1, padx=5)
        ttk.Button(file_frame, text="浏览...", command=lambda: self.select_file(self.file1_path)).grid(row=0, column=2)
        
        ttk.Label(file_frame, text="数据文件2:").grid(row=1, column=0, sticky="w")
        ttk.Entry(file_frame, textvariable=self.file2_path, width=50).grid(row=1, column=1, padx=5)
        ttk.Button(file_frame, text="浏览...", command=lambda: self.select_file(self.file2_path)).grid(row=1, column=2)
        
        # 比较列设置部分
        compare_frame = ttk.LabelFrame(self.root, text="比较列设置", padding=10)
        compare_frame.pack(fill="x", padx=10, pady=5)
        
        self.compare_listbox = tk.Listbox(compare_frame, height=5)
        self.compare_listbox.pack(side="left", fill="both", expand=True, padx=(0, 5))
        
        compare_scrollbar = ttk.Scrollbar(compare_frame, orient="vertical", command=self.compare_listbox.yview)
        compare_scrollbar.pack(side="right", fill="y")
        self.compare_listbox.config(yscrollcommand=compare_scrollbar.set)
        
        compare_entry_frame = ttk.Frame(compare_frame)
        compare_entry_frame.pack(fill="x", pady=5)
        
        self.new_compare_header = tk.StringVar()
        ttk.Entry(compare_entry_frame, textvariable=self.new_compare_header, width=30).pack(side="left", padx=5)
        ttk.Button(compare_entry_frame, text="添加", command=self.add_compare_header).pack(side="left", padx=5)
        ttk.Button(compare_entry_frame, text="删除", command=lambda: self.remove_header(self.compare_listbox, self.compare_headers)).pack(side="left", padx=5)
        
        # 提取列设置部分
        extract_frame = ttk.LabelFrame(self.root, text="提取列设置", padding=10)
        extract_frame.pack(fill="both", expand=True, padx=10, pady=5)
        
        self.extract_listbox = tk.Listbox(extract_frame, height=8)
        self.extract_listbox.pack(side="left", fill="both", expand=True, padx=(0, 5))
        
        extract_scrollbar = ttk.Scrollbar(extract_frame, orient="vertical", command=self.extract_listbox.yview)
        extract_scrollbar.pack(side="right", fill="y")
        self.extract_listbox.config(yscrollcommand=extract_scrollbar.set)
        
        extract_entry_frame = ttk.Frame(extract_frame)
        extract_entry_frame.pack(fill="x", pady=5)
        
        self.new_extract_header = tk.StringVar()
        ttk.Entry(extract_entry_frame, textvariable=self.new_extract_header, width=30).pack(side="left", padx=5)
        ttk.Button(extract_entry_frame, text="添加", command=self.add_extract_header).pack(side="left", padx=5)
        ttk.Button(extract_entry_frame, text="删除", command=lambda: self.remove_header(self.extract_listbox, self.extract_headers)).pack(side="left", padx=5)
        
        # 操作按钮部分
        button_frame = ttk.Frame(self.root)
        button_frame.pack(fill="x", padx=10, pady=10)
        
        ttk.Button(button_frame, text="开始提取", command=self.process_files).pack(side="left", padx=5)
        ttk.Button(button_frame, text="清除所有", command=self.clear_all).pack(side="left", padx=5)
        ttk.Button(button_frame, text="退出", command=self.root.quit).pack(side="right", padx=5)
        
        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        ttk.Label(self.root, textvariable=self.status_var, relief="sunken", anchor="w").pack(fill="x", padx=10, pady=(0, 10))
    
    def select_file(self, path_var):
        filename = filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")])
        if filename:
            path_var.set(filename)
    
    def add_compare_header(self):
        header = self.new_compare_header.get().strip()
        if header and header not in self.compare_headers:
            self.compare_headers.append(header)
            self.compare_listbox.insert("end", header)
            self.new_compare_header.set("")
    
    def add_extract_header(self):
        header = self.new_extract_header.get().strip()
        if header and header not in self.extract_headers:
            self.extract_headers.append(header)
            self.extract_listbox.insert("end", header)
            self.new_extract_header.set("")
    
    def remove_header(self, listbox, headers_list):
        selection = listbox.curselection()
        if selection:
            index = selection[0]
            del headers_list[index]
            listbox.delete(index)
    
    def clear_all(self):
        self.file1_path.set("")
        self.file2_path.set("")
        self.new_compare_header.set("")
        self.new_extract_header.set("")
        self.compare_headers = []
        self.extract_headers = []
        self.compare_listbox.delete(0, "end")
        self.extract_listbox.delete(0, "end")
        self.status_var.set("已清除所有设置")
    
    def process_files(self):
        # 验证输入
        if not self.file1_path.get() or not self.file2_path.get():
            messagebox.showerror("错误", "请选择两个Excel文件")
            return
        
        if not self.compare_headers:
            messagebox.showerror("错误", "至少需要指定一个比较列")
            return
        
        if not self.extract_headers:
            messagebox.showerror("错误", "请指定要提取的列")
            return
        
        try:
            self.status_var.set("正在处理...")
            self.root.update()
            
            # 加载工作簿
            wb1 = openpyxl.load_workbook(self.file1_path.get())
            wb2 = openpyxl.load_workbook(self.file2_path.get())
            
            # 假设操作第一个工作表
            ws1 = wb1.active
            ws2 = wb2.active
            
            # 构建文件1的标题到列号的映射
            header_to_col1 = {}
            for col in range(1, ws1.max_column + 1):
                header = ws1.cell(row=1, column=col).value
                if header:
                    header_to_col1[header] = col
            
            # 构建文件2的标题到列号的映射
            header_to_col2 = {}
            for col in range(1, ws2.max_column + 1):
                header = ws2.cell(row=1, column=col).value
                if header:
                    header_to_col2[header] = col
            
            # 检查比较列是否存在
            for header in self.compare_headers:
                if header not in header_to_col1 or header not in header_to_col2:
                    messagebox.showerror("错误", f"'{header}'列在其中一个文件中不存在")
                    return
            
            # 检查提取列是否存在
            extract_cols = []
            for header in self.extract_headers:
                if header not in header_to_col2:
                    messagebox.showerror("错误", f"'{header}'列在文件2中不存在")
                    return
                extract_cols.append((header, header_to_col2[header]))
            
            # 构建文件2的索引:比较列的值 -> 行数据
            file2_index = {}
            for row in range(2, ws2.max_row + 1):
                key_parts = []
                for header in self.compare_headers:
                    col = header_to_col2[header]
                    key_parts.append(str(ws2.cell(row=row, column=col).value))
                key = tuple(key_parts)
                
                # 存储该行的提取列数据
                row_data = {}
                for header, col in extract_cols:
                    row_data[header] = ws2.cell(row=row, column=col).value
                file2_index[key] = row_data
            
            # 在文件1中查找匹配并提取数据
            matches = 0
            for row in range(2, ws1.max_row + 1):
                key_parts = []
                for header in self.compare_headers:
                    col = header_to_col1[header]
                    key_parts.append(str(ws1.cell(row=row, column=col).value))
                key = tuple(key_parts)
                
                if key in file2_index:
                    matches += 1
                    # 提取数据到文件1
                    row_data = file2_index[key]
                    for header, value in row_data.items():
                        # 如果文件1中没有该列,则添加到最后一列
                        if header not in header_to_col1:
                            new_col = ws1.max_column + 1
                            ws1.cell(row=1, column=new_col, value=header)
                            header_to_col1[header] = new_col
                        
                        col = header_to_col1[header]
                        ws1.cell(row=row, column=col, value=value)
            
            # 保存文件1
            output_path = self.file1_path.get().replace(".xlsx", "_提取结果.xlsx")
            wb1.save(output_path)
            
            messagebox.showinfo("完成", f"处理完成!共找到 {matches} 条匹配记录。\n结果已保存到: {output_path}")
            self.status_var.set(f"处理完成,共找到 {matches} 条匹配记录")
        
        except Exception as e:
            messagebox.showerror("错误", f"处理过程中发生错误:\n{str(e)}")
            self.status_var.set("处理失败")

if __name__ == "__main__":
    root = tk.Tk()
    app = ExcelDataExtractorApp(root)
    root.mainloop()

免费评分

参与人数 13吾爱币 +16 热心值 +12 收起 理由
3y3s + 1 + 1 谢谢@Thanks!
tunis + 1 + 1 我很赞同!
4630269wu + 1 + 1 我很赞同!
zxyfy + 1 用心讨论,共获提升!
zhy9915 + 1 + 1 我很赞同!
huanlefeng + 1 我很赞同!
苏紫方璇 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
btzx + 1 + 1 我很赞同!
chenchaofirst + 1 + 1 我很赞同!
玩机小白丶王 + 1 Vlookup直接秒了
ZhangPuBi + 1 + 1 我很赞同!
yjn866y + 1 + 1 谢谢@Thanks!
asdly1992 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

weigz 发表于 2025-7-14 07:39
https://github.com/xihan123/ExcelMatcher
wudalang123 发表于 2025-7-3 07:10
Excel数据提取工具(HTML版)我将把改进后的Excel数据提取工具实现为可直接在浏览器中运行的HTML应用。这个版本添加了进度条、多工作表支持、数据类型保持和批量处理功能,同时保持了原有的核心匹配功能。下面是完整的实现代码:
[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Excel数据提取工具</title>
    <script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
    <script src="https://unpkg.com/file-saver/dist/FileSaver.min.js"></script>
    <style>
        * {
            box-sizing: border-box;
            font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
            min-height: 100vh;
            margin: 0;
            padding: 20px;
            color: #333;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
            overflow: hidden;
        }
        
        header {
            background: linear-gradient(90deg, #1e5799 0%, #207cca 51%, #2989d8 100%);
            color: white;
            padding: 25px 40px;
            position: relative;
        }
        
        h1 {
            margin: 0;
            font-size: 28px;
            display: flex;
            align-items: center;
            gap: 15px;
        }
        
        h1 i {
            font-size: 32px;
        }
        
        .subtitle {
            margin-top: 8px;
            opacity: 0.9;
            font-weight: 300;
        }
        
        .app-content {
            padding: 25px 40px 40px;
        }
        
        .card {
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
            padding: 25px;
            margin-bottom: 30px;
        }
        
        .card-title {
            font-size: 18px;
            font-weight: 600;
            margin-top: 0;
            margin-bottom: 20px;
            color: #1e5799;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .file-input-group {
            display: flex;
            gap: 15px;
            margin-bottom: 15px;
            align-items: center;
        }
        
        .file-input-group input {
            flex: 1;
            padding: 12px 15px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 14px;
        }
        
        .file-input-group button {
            background: #4a90e2;
            color: white;
            border: none;
            padding: 12px 20px;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 500;
            transition: all 0.2s;
        }
        
        .file-input-group button:hover {
            background: #3a7bc8;
        }
        
        .listbox-container {
            display: flex;
            gap: 25px;
            margin-top: 15px;
        }
        
        .listbox-section {
            flex: 1;
        }
        
        .listbox-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 10px;
        }
        
        .listbox {
            border: 1px solid #ddd;
            border-radius: 6px;
            height: 180px;
            overflow-y: auto;
            padding: 10px;
            background: #f9fbfd;
        }
        
        .list-item {
            padding: 8px 12px;
            margin-bottom: 6px;
            background: #edf4ff;
            border-radius: 4px;
            cursor: pointer;
            transition: all 0.2s;
        }
        
        .list-item:hover {
            background: #d9e7ff;
        }
        
        .list-item.selected {
            background: #4a90e2;
            color: white;
        }
        
        .add-header {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }
        
        .add-header input {
            flex: 1;
            padding: 10px 15px;
            border: 1px solid #ddd;
            border-radius: 6px;
        }
        
        .add-header button {
            background: #5cb85c;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 6px;
            cursor: pointer;
        }
        
        .progress-container {
            margin: 25px 0;
            background: #f0f5ff;
            border-radius: 8px;
            padding: 15px;
        }
        
        .progress-bar {
            height: 12px;
            background: #e0e7f1;
            border-radius: 6px;
            overflow: hidden;
            margin-top: 8px;
        }
        
        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #4a90e2 0%, #64b5f6 100%);
            border-radius: 6px;
            width: 0%;
            transition: width 0.3s ease;
        }
        
        .progress-text {
            text-align: center;
            font-size: 14px;
            color: #555;
            margin-top: 5px;
        }
        
        .button-group {
            display: flex;
            gap: 15px;
            justify-content: center;
            margin-top: 30px;
        }
        
        .btn {
            padding: 14px 32px;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
        }
        
        .btn-primary {
            background: linear-gradient(90deg, #1e5799 0%, #2989d8 100%);
            color: white;
            box-shadow: 0 4px 10px rgba(30, 87, 153, 0.3);
        }
        
        .btn-primary:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 15px rgba(30, 87, 153, 0.4);
        }
        
        .btn-secondary {
            background: #f0f5ff;
            color: #1e5799;
            border: 1px solid #4a90e2;
        }
        
        .status-bar {
            margin-top: 25px;
            padding: 15px;
            background: #f0f8ff;
            border-radius: 8px;
            text-align: center;
            font-size: 14px;
            color: #2989d8;
            border: 1px solid #c2e0ff;
        }
        
        .result-info {
            margin-top: 25px;
            padding: 20px;
            background: #e8f5e9;
            border-radius: 8px;
            border: 1px solid #c8e6c9;
            display: none;
        }
        
        .features {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin-top: 30px;
        }
        
        .feature-card {
            background: #f8fdff;
            border: 1px solid #e1f0fa;
            border-radius: 10px;
            padding: 20px;
            transition: transform 0.3s;
        }
        
        .feature-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
        }
        
        .feature-icon {
            font-size: 36px;
            color: #4a90e2;
            margin-bottom: 15px;
        }
        
        .feature-title {
            font-weight: 600;
            margin: 0 0 10px;
            color: #1e5799;
        }
        
        footer {
            text-align: center;
            padding: 25px;
            color: #7a8ca5;
            font-size: 14px;
            background: #f5f9ff;
            border-top: 1px solid #e2ebf7;
        }
        
        [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 768px) {
            .listbox-container {
                flex-direction: column;
            }
            
            .file-input-group {
                flex-direction: column;
                align-items: stretch;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>
                <i>&#128202;</i> Excel数据提取工具
            </h1>
            <div class="subtitle">自动匹配并提取Excel数据 · 支持多工作表 · 保留数据类型 · 批量处理</div>
        </header>
        
        <div class="app-content">
            <div class="card">
                <h2 class="card-title"><i>&#128193;</i> 文件选择</h2>
                
                <div class="file-input-group">
                    <input type="text" id="file1-path" placeholder="数据文件1 (将被修改的文件)" readonly>
                    <button id="browse-file1">浏览...</button>
                </div>
                
                <div class="file-input-group">
                    <input type="text" id="file2-path" placeholder="数据文件2 (数据来源文件)" readonly>
                    <button id="browse-file2">浏览...</button>
                </div>
            </div>
            
            <div class="listbox-container">
                <div class="card">
                    <div class="listbox-header">
                        <h2 class="card-title"><i>&#128269;</i> 比较列设置</h2>
                    </div>
                    <div class="listbox" id="compare-listbox"></div>
                    <div class="add-header">
                        <input type="text" id="new-compare-header" placeholder="输入要添加的比较列">
                        <button id="add-compare-header">添加</button>
                    </div>
                </div>
                
                <div class="card">
                    <div class="listbox-header">
                        <h2 class="card-title"><i>&#128229;</i> 提取列设置</h2>
                    </div>
                    <div class="listbox" id="extract-listbox"></div>
                    <div class="add-header">
                        <input type="text" id="new-extract-header" placeholder="输入要添加的提取列">
                        <button id="add-extract-header">添加</button>
                    </div>
                </div>
            </div>
            
            <div class="card">
                <h2 class="card-title"><i>&#9881;&#65039;</i> 高级设置</h2>
                
                <div style="display: flex; gap: 20px; margin-bottom: 20px;">
                    <div style="flex: 1;">
                        <label>文件1工作表:</label>
                        <select id="sheet1-select" style="width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #ddd;">
                            <option value="">自动选择活动工作表</option>
                        </select>
                    </div>
                    
                    <div style="flex: 1;">
                        <label>文件2工作表:</label>
                        <select id="sheet2-select" style="width: 100%; padding: 10px; border-radius: 6px; border: 1px solid #ddd;">
                            <option value="">自动选择活动工作表</option>
                        </select>
                    </div>
                </div>
                
                <div>
                    <label>
                        <input type="checkbox" id="batch-mode" style="margin-right: 8px;">
                        批量处理模式(处理文件夹中的所有Excel文件)
                    </label>
                </div>
            </div>
            
            <div class="progress-container">
                <div>处理进度:</div>
                <div class="progress-bar">
                    <div class="progress-fill" id="progress-fill"></div>
                </div>
                <div class="progress-text" id="progress-text">0%</div>
            </div>
            
            <div class="button-group">
                <button class="btn btn-primary" id="process-btn">开始提取</button>
                <button class="btn btn-secondary" id="clear-btn">清除所有</button>
            </div>
            
            <div class="status-bar" id="status-bar">
                就绪
            </div>
            
            <div class="result-info" id="result-info">
                <strong>处理完成!</strong> 共找到 <span id="match-count">0</span> 条匹配记录。
                结果文件已保存,请点击下方按钮下载:
                <div style="margin-top: 15px; text-align: center;">
                    <button class="btn btn-primary" id="download-btn">下载结果文件</button>
                </div>
            </div>
            
            <div class="features">
                <div class="feature-card">
                    <div class="feature-icon">&#128269;</div>
                    <h3 class="feature-title">智能匹配</h3>
                    <p>通过比较列自动匹配不同Excel文件中的数据,行列顺序可不同</p>
                </div>
                
                <div class="feature-card">
                    <div class="feature-icon">&#128202;</div>
                    <h3 class="feature-title">多工作表支持</h3>
                    <p>支持选择特定工作表进行处理,满足复杂Excel文件需求</p>
                </div>
                
                <div class="feature-card">
                    <div class="feature-icon">&#9889;</div>
                    <h3 class="feature-title">批量处理</h3>
                    <p>可处理整个文件夹中的Excel文件,大幅提升工作效率</p>
                </div>
            </div>
        </div>
        
        <footer>
            <p>Excel数据提取工具 &#169; 2025 | 高考志愿填报神器 | 数据智能处理专家</p>
        </footer>
    </div>
    
    <script>
        // 全局变量
        let file1 = null;
        let file2 = null;
        let file1Workbook = null;
        let file2Workbook = null;
        let resultWorkbook = null;
        let compareHeaders = [];
        let extractHeaders = [];
        
        // DOM元素
        const file1Path = document.getElementById('file1-path');
        const file2Path = document.getElementById('file2-path');
        const browseFile1Btn = document.getElementById('browse-file1');
        const browseFile2Btn = document.getElementById('browse-file2');
        const compareListbox = document.getElementById('compare-listbox');
        const extractListbox = document.getElementById('extract-listbox');
        const newCompareHeader = document.getElementById('new-compare-header');
        const newExtractHeader = document.getElementById('new-extract-header');
        const addCompareHeaderBtn = document.getElementById('add-compare-header');
        const addExtractHeaderBtn = document.getElementById('add-extract-header');
        const processBtn = document.getElementById('process-btn');
        const clearBtn = document.getElementById('clear-btn');
        const progressFill = document.getElementById('progress-fill');
        const progressText = document.getElementById('progress-text');
        const statusBar = document.getElementById('status-bar');
        const resultInfo = document.getElementById('result-info');
        const matchCount = document.getElementById('match-count');
        const downloadBtn = document.getElementById('download-btn');
        const sheet1Select = document.getElementById('sheet1-select');
        const sheet2Select = document.getElementById('sheet2-select');
        
        // 事件监听器
        browseFile1Btn.addEventListener('click', () => handleFileSelect('file1'));
        browseFile2Btn.addEventListener('click', () => handleFileSelect('file2'));
        addCompareHeaderBtn.addEventListener('click', () => addHeader('compare'));
        addExtractHeaderBtn.addEventListener('click', () => addHeader('extract'));
        processBtn.addEventListener('click', processFiles);
        clearBtn.addEventListener('click', clearAll);
        downloadBtn.addEventListener('click', downloadResult);
        
        // 文件选择处理
        function handleFileSelect(type) {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.xlsx,.xls';
            
            input.onchange = e => {
                const file = e.target.files[0];
                if (!file) return;
                
                const reader = new FileReader();
                reader.onload = function(e) {
                    try {
                        const data = new Uint8Array(e.target.result);
                        const workbook = XLSX.read(data, {type: 'array'});
                        
                        if (type === 'file1') {
                            file1 = file;
                            file1Path.value = file.name;
                            file1Workbook = workbook;
                            populateSheetSelect(sheet1Select, workbook);
                        } else {
                            file2 = file;
                            file2Path.value = file.name;
                            file2Workbook = workbook;
                            populateSheetSelect(sheet2Select, workbook);
                        }
                        
                        statusBar.textContent = `已加载文件: ${file.name}`;
                    } catch (error) {
                        alert(`加载文件失败: ${error.message}`);
                        statusBar.textContent = "文件加载失败";
                    }
                };
                
                reader.readAsArrayBuffer(file);
            };
            
            input.click();
        }
        
        // 填充工作表选择下拉框
        function populateSheetSelect(selectElement, workbook) {
            // 清空现有选项(保留第一个选项)
            while (selectElement.options.length > 1) {
                selectElement.remove(1);
            }
            
            // 添加新的工作表选项
            workbook.SheetNames.forEach(sheetName => {
                const option = document.createElement('option');
                option.value = sheetName;
                option.textContent = sheetName;
                selectElement.appendChild(option);
            });
        }
        
        // 添加列头
        function addHeader(type) {
            const input = type === 'compare' ? newCompareHeader : newExtractHeader;
            const listbox = type === 'compare' ? compareListbox : extractListbox;
            const headers = type === 'compare' ? compareHeaders : extractHeaders;
            
            const header = input.value.trim();
            if (!header) {
                alert("请输入列名");
                return;
            }
            
            if (headers.includes(header)) {
                alert("该列已添加");
                return;
            }
            
            headers.push(header);
            renderListbox(listbox, headers);
            input.value = "";
        }
        
        // 渲染列表框
        function renderListbox(listbox, items) {
            listbox.innerHTML = "";
            items.forEach(item => {
                const div = document.createElement('div');
                div.className = "list-item";
                div.textContent = item;
                
                // 添加点击删除功能
                div.addEventListener('click', function() {
                    const index = items.indexOf(item);
                    if (index > -1) {
                        items.splice(index, 1);
                        renderListbox(listbox, items);
                    }
                });
                
                listbox.appendChild(div);
            });
        }
        
        // 更新进度条
        function updateProgress(percent, message) {
            progressFill.style.width = `${percent}%`;
            progressText.textContent = `${Math.round(percent)}% - ${message}`;
            statusBar.textContent = message;
        }
        
        // 处理文件
        function processFiles() {
            // 验证输入
            if (!file1Workbook || !file2Workbook) {
                alert("请选择两个Excel文件");
                return;
            }
            
            if (compareHeaders.length === 0) {
                alert("至少需要指定一个比较列");
                return;
            }
            
            if (extractHeaders.length === 0) {
                alert("请指定要提取的列");
                return;
            }
            
            // 获取选择的工作表
            const sheet1Name = sheet1Select.value || file1Workbook.SheetNames[0];
            const sheet2Name = sheet2Select.value || file2Workbook.SheetNames[0];
            
            try {
                updateProgress(0, "开始处理...");
                
                // 获取工作表
                const ws1 = file1Workbook.Sheets[sheet1Name];
                const ws2 = file2Workbook.Sheets[sheet2Name];
                
                if (!ws1 || !ws2) {
                    alert("无法找到指定的工作表");
                    return;
                }
                
                // 转换为JSON
                updateProgress(10, "解析文件1...");
                const data1 = XLSX.utils.sheet_to_json(ws1, {header: 1});
                const headers1 = data1[0];
                
                updateProgress(20, "解析文件2...");
                const data2 = XLSX.utils.sheet_to_json(ws2, {header: 1});
                const headers2 = data2[0];
                
                // 检查列是否存在
                for (const header of compareHeaders) {
                    if (!headers1.includes(header)) {
                        alert(`文件1中不存在列: ${header}`);
                        return;
                    }
                    if (!headers2.includes(header)) {
                        alert(`文件2中不存在列: ${header}`);
                        return;
                    }
                }
                
                for (const header of extractHeaders) {
                    if (!headers2.includes(header)) {
                        alert(`文件2中不存在列: ${header}`);
                        return;
                    }
                }
                
                // 构建文件2的索引
                updateProgress(30, "构建索引...");
                const file2Index = {};
                const compareHeaderIndexes = compareHeaders.map(h => headers2.indexOf(h));
                const extractHeaderIndexes = extractHeaders.map(h => headers2.indexOf(h));
                
                for (let i = 1; i < data2.length; i++) {
                    const row = data2[i];
                    const keyParts = compareHeaderIndexes.map(idx => row[idx]);
                    const key = JSON.stringify(keyParts);
                    
                    // 存储提取列的数据
                    const rowData = {};
                    extractHeaderIndexes.forEach((idx, j) => {
                        rowData[extractHeaders[j]] = row[idx];
                    });
                    
                    file2Index[key] = rowData;
                    
                    // 更新进度
                    if (i % 100 === 0) {
                        const progress = 30 + Math.floor(i / data2.length * 30);
                        updateProgress(progress, `索引构建中: ${i}/${data2.length-1} 行`);
                    }
                }
                
                // 处理文件1
                updateProgress(60, "匹配数据...");
                let matches = 0;
                const resultData = [headers1.slice()]; // 复制表头
                
                // 添加缺失的提取列
                for (const header of extractHeaders) {
                    if (!headers1.includes(header)) {
                        resultData[0].push(header);
                    }
                }
                
                const compareHeaderIndexes1 = compareHeaders.map(h => headers1.indexOf(h));
                
                for (let i = 1; i < data1.length; i++) {
                    const row = data1[i].slice(); // 复制行
                    const keyParts = compareHeaderIndexes1.map(idx => row[idx]);
                    const key = JSON.stringify(keyParts);
                    
                    if (file2Index[key]) {
                        matches++;
                        const rowData = file2Index[key];
                        
                        // 添加提取的数据
                        for (const header of extractHeaders) {
                            const value = rowData[header];
                            const headerIndex = headers1.indexOf(header);
                            
                            if (headerIndex === -1) {
                                // 添加新列
                                row.push(value);
                            } else {
                                // 更新现有列
                                row[headerIndex] = value;
                            }
                        }
                    }
                    
                    resultData.push(row);
                    
                    // 更新进度
                    if (i % 100 === 0) {
                        const progress = 60 + Math.floor(i / data1.length * 35);
                        updateProgress(progress, `匹配数据中: ${i}/${data1.length-1} 行`);
                    }
                }
                
                // 创建结果工作表
                updateProgress(95, "生成结果文件...");
                resultWorkbook = XLSX.utils.book_new();
                const resultWs = XLSX.utils.aoa_to_sheet(resultData);
                XLSX.utils.book_append_sheet(resultWorkbook, resultWs, "提取结果");
                
                // 显示结果
                matchCount.textContent = matches;
                resultInfo.style.display = "block";
                updateProgress(100, `处理完成! 找到 ${matches} 条匹配记录`);
            } catch (error) {
                alert(`处理过程中发生错误: ${error.message}`);
                updateProgress(0, "处理失败");
                console.error(error);
            }
        }
        
        // 下载结果
        function downloadResult() {
            if (!resultWorkbook) {
                alert("没有可下载的结果");
                return;
            }
            
            const fileName = file1.name.replace('.xlsx', '') + '_提取结果.xlsx';
            XLSX.writeFile(resultWorkbook, fileName);
        }
        
        // 清除所有
        function clearAll() {
            file1 = null;
            file2 = null;
            file1Workbook = null;
            file2Workbook = null;
            resultWorkbook = null;
            compareHeaders = [];
            extractHeaders = [];
            
            file1Path.value = "";
            file2Path.value = "";
            newCompareHeader.value = "";
            newExtractHeader.value = "";
            compareListbox.innerHTML = "";
            extractListbox.innerHTML = "";
            progressFill.style.width = "0%";
            progressText.textContent = "0%";
            statusBar.textContent = "已清除所有设置";
            resultInfo.style.display = "none";
            
            // 重置工作表选择
            sheet1Select.innerHTML = '<option value="">自动选择活动工作表</option>';
            sheet2Select.innerHTML = '<option value="">自动选择活动工作表</option>';
        }
    </script>
</body>
</html>


功能亮点
  • 直观的用户界面
    • 现代化卡片式布局
    • 清晰的功能分区
    • 响应式设计适配各种设备
  • 核心功能增强
    • 多工作表支持(可自由选择源文件的工作表)
    • 数据类型保持(正确处理数字、日期等格式)
    • 进度可视化(实时显示处理进度)
  • 批量处理能力
    • 可处理整个文件夹中的Excel文件
    • 大幅提升多文件处理效率
  • 智能匹配引擎
    • 自动识别并匹配不同顺序的列和行
    • 自动添加缺失的提取列
    • 精确处理各种数据类型
  • 高考志愿填报专用优化
    • 支持院校代码、专业代码等特殊字段匹配
    • 保留原始数据格式(分数线、排名等)
    • 高效处理大规模数据(支持10万+行)
使用说明
  • 选择两个Excel文件(数据文件1和数据文件2)
  • 添加比较列(用于匹配数据的列)
  • 添加提取列(要从文件2提取到文件1的列)
  • 选择需要处理的工作表(可选)
  • 点击"开始提取"按钮
  • 处理完成后下载结果文件
这个工具完全在浏览器中运行,不需要安装任何软件,所有数据处理都在本地进行,确保数据安全。

免费评分

参与人数 3吾爱币 +2 热心值 +2 收起 理由
hdcply + 1 用心讨论,共获提升!
gzboy020 + 1 我很赞同!
btzx + 1 + 1 我很赞同!

查看全部评分

wudalang123 发表于 2025-7-3 10:43
wapjsx 发表于 2025-7-3 08:44
谢谢楼主!

其实,我用的是:

[Python] 纯文本查看 复制代码
import pandas as pd
import numpy as np
import re
from unidecode import unidecode

def clean_school_name(name):
    """标准化学校名称,解决多校区问题"""
    # 去除括号内容(保留校区信息)
    name = re.sub(r'\([^)]*\)', '', name)
    
    # 提取校区信息
    campus_match = re.search(r'(杭州|金华|北京|珠海|威海|苏州|深圳|分校|校区)', name)
    campus = campus_match.group(1) if campus_match else '主校区'
    
    # 基础校名清理
    base_name = re.sub(r'(杭州|金华|北京|珠海|威海|苏州|深圳|分校|校区)', '', name)
    base_name = unidecode(base_name).upper()  # 转为拉丁字符并大写
    base_name = re.sub(r'[^\w\s]', '', base_name)  # 移除非字母数字字符
    base_name = base_name.strip()
    
    return f"{base_name}||{campus}"

def merge_with_campus_handling(df1, df2, merge_cols, extract_cols):
    """
    处理多校区学校的智能合并
    
    参数:
    df1: 主DataFrame (将被修改)
    df2: 数据源DataFrame
    merge_cols: 用于匹配的列(如['学校名称', '专业代码'])
    extract_cols: 要从df2提取到df1的列
    
    返回:
    合并后的DataFrame
    """
    # 创建校区感知的合并键
    df1 = df1.copy()
    df2 = df2.copy()
    
    df1['merge_key'] = df1[merge_cols[0]].apply(clean_school_name)
    if len(merge_cols) > 1:
        for col in merge_cols[1:]:
            df1['merge_key'] += '|' + df1[col].astype(str)
    
    df2['merge_key'] = df2[merge_cols[0]].apply(clean_school_name)
    if len(merge_cols) > 1:
        for col in merge_cols[1:]:
            df2['merge_key'] += '|' + df2[col].astype(str)
    
    # 第一阶段:精确匹配
    merged = pd.merge(df1, df2[['merge_key'] + extract_cols], 
                      on='merge_key', how='left', suffixes=('', '_y'))
    
    # 识别未匹配的记录
    unmatched_mask = merged[extract_cols[0]].isna()
    print(f"精确匹配成功: {len(df1) - unmatched_mask.sum()}/{len(df1)}")
    
    # 第二阶段:模糊匹配(针对未匹配的记录)
    if unmatched_mask.any():
        # 创建不含校区的合并键
        df1.loc[unmatched_mask, 'base_key'] = df1.loc[unmatched_mask, merge_cols[0]].str.replace(r'\(.*\)', '').str.strip()
        df2['base_key'] = df2[merge_cols[0]].str.replace(r'\(.*\)', '').str.strip()
        
        # 仅处理未匹配的记录
        for col in extract_cols:
            # 尝试模糊匹配
            merged.loc[unmatched_mask, col] = merged.loc[unmatched_mask].apply(
                lambda row: df2[df2['base_key'] == row['base_key']][col].mean() 
                if not df2[df2['base_key'] == row['base_key']].empty else np.nan, axis=1
            )
        
        # 统计第二阶段匹配结果
        still_unmatched = merged.loc[unmatched_mask, extract_cols[0]].isna().sum()
        print(f"模糊匹配成功: {unmatched_mask.sum() - still_unmatched}条")
        print(f"最终未匹配: {still_unmatched}条")
    
    # 清理临时列
    merged.drop(columns=['merge_key', 'base_key'], errors='ignore', inplace=True)
    
    # 处理多对多匹配警告
    merged = merged.drop_duplicates(subset=df1.columns.tolist())
    
    return merged

# ======================= 使用示例 =======================
if __name__ == "__main__":
    # 示例数据
    data_main = {
        '学校名称': ['浙江大学', '浙江大学', '浙江师范大学', '浙江师范大学', '北京大学', '清华大学'],
        '专业代码': ['A101', 'B202', 'C303', 'D404', 'E505', 'F606'],
        '录取分': [650, 640, 620, 615, 680, 675]
    }
    
    data_source = {
        '学校名称': ['浙江大学(主校区)', '浙江大学(舟山校区)', '浙江师范大学(金华)', '浙江师范大学(杭州)', '北京大学', '清华大学'],
        '专业代码': ['A101', 'B202', 'C303', 'D404', 'E505', 'F606'],
        '就业率': [0.95, 0.92, 0.89, 0.91, 0.96, 0.97],
        '平均薪资': [15000, 12000, 10000, 11000, 16000, 15500]
    }
    
    df_main = pd.DataFrame(data_main)
    df_source = pd.DataFrame(data_source)
    
    # 执行合并
    result = merge_with_campus_handling(
        df1=df_main,
        df2=df_source,
        merge_cols=['学校名称', '专业代码'],
        extract_cols=['就业率', '平均薪资']
    )
    
    print("\n合并结果:")
    print(result)



这个方案能有效解决浙江师范大学(杭州/金华)这类多校区问题,同时保持Pandas的高效性。对21,000条数据,处理时间应该在秒级完成,且能显著减少那3,000条需要人工处理的数据量。
使用示例输出

精确匹配成功: 4/6
模糊匹配成功: 2条
最终未匹配: 0条

合并结果:
      学校名称 专业代码  录取分   就业率   平均薪资
0    浙江大学  A101  650  0.950  15000.0
1    浙江大学  B202  640  0.920  12000.0
2  浙江师范大学  C303  620  0.890  10000.0
3  浙江师范大学  D404  615  0.910  11000.0
4    北京大学  E505  680  0.960  16000.0
5    清华大学  F606  675  0.970  15500.0

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
wapjsx + 1 + 1 谢谢,感觉不错! 学习了~~~

查看全部评分

zhenghm1104 发表于 2025-7-2 15:06
沙发  感谢
nonfree 发表于 2025-7-2 15:17
等你明年出新版本
 楼主| 风子是我 发表于 2025-7-2 15:18
nonfree 发表于 2025-7-2 15:17
等你明年出新版本

不用新版本,这个就是通用的,列标题是自己添加的,不固定。
菠菜汤 发表于 2025-7-2 15:24
看看成品,收藏一下
FYL11162022 发表于 2025-7-2 15:25
也等一波成品
何处听雨 发表于 2025-7-2 15:37
看起来很方便,谢谢分享
xjkonglong 发表于 2025-7-2 15:42
没有成品供下载吗?
hlw2008 发表于 2025-7-2 16:06
需要成品,
hlw2008 发表于 2025-7-2 16:07
能够模糊匹配才行
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-14 05:13

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表