<think>我们之前已经解决了合并单元格的问题,但用户再次报告相同的错误,说明可能在某些情况下修复方案未覆盖。
需要重新检查代码,特别是当合并单元格区域内的非主单元格被访问时。
回顾错误:'MergedCell' object attribute 'value' is read-only
原因:在复制过程中,我们遍历每个单元格,并尝试给目标工作表的对应单元格赋值。但是,合并区域内的非主单元格是MergedCell类型,其value属性是只读的。
之前的修复方案是:
1. 复制合并区域(merge_cells)
2. 在遍历单元格时,跳过合并区域内的非主单元格(只处理主单元格和普通单元格)
但是,用户仍然遇到此错误,说明在某种情况下,我们的代码仍然尝试给合并区域内的非主单元格赋值。
可能的原因:
- 在复制格式的过程中,我们使用了`for row in src_ws.iter_rows()`遍历源工作表的每个单元格,然后尝试给目标工作表的对应单元格赋值(包括非主单元格)。
- 尽管我们通过检查合并区域并跳过非主单元格来避免,但可能在某些情况下(例如,合并区域跨越多个区域,或者我们的检查逻辑有缺陷)未能正确跳过。
重新检查代码中的关键部分:
```python
def copy_essential_format(self, src_ws, dest_ws):
# 复制合并单元格
for merged_range in src_ws.merged_cells.ranges:
dest_ws.merge_cells(str(merged_range))
processed_cells = set()
for row in src_ws.iter_rows():
for cell in row:
if cell.coordinate in processed_cells:
continue
dest_cell = dest_ws[cell.coordinate]
is_merged = False
for merged_range in src_ws.merged_cells.ranges:
if cell.coordinate in merged_range:
is_merged = True
if cell.coordinate == merged_range.start_cell:
# 标记合并区域内所有单元格为已处理
for row_idx in range(merged_range.min_row, merged_range.max_row+1):
for col_idx in range(merged_range.min_col, merged_range.max_col+1):
coord = get_column_letter(col_idx) + str(row_idx)
processed_cells.add(coord)
# 只给主单元格赋值
dest_cell.value = cell.value
break
if not is_merged:
# 复制字体
if cell.font:
dest_cell.font = Font(...)
dest_cell.value = cell.value
```
问题可能出现在:
1. 我们给合并区域内的主单元格赋值时,使用的是`dest_cell.value = cell.value`,这没有问题。
2. 对于非合并单元格,我们直接赋值,也没有问题。
但是,在合并区域内的非主单元格,我们通过`processed_cells`跳过了吗?
- 在发现一个单元格属于合并区域,并且是主单元格时,我们将整个合并区域内的所有单元格坐标都添加到`processed_cells`中,这样在后续遍历到这些单元格时,会跳过(因为第一行判断`if cell.coordinate in processed_cells`就continue了)。
- 但是,如果合并区域内的非主单元格在源工作表中被遍历的顺序在主单元格之前,那么它不会被跳过(因为此时主单元格还没处理,所以这个非主单元格还没有被标记为已处理)。
例如:
- 合并区域:A1:B2,其中A1是主单元格。
- 遍历顺序可能是:先A1(主单元格),然后A2(非主单元格),然后B1,然后B2。
- 但是,如果遍历顺序是先行后列,那么顺序是:第一行:A1, B1;第二行:A2, B2。
- 当处理到B1时,它属于合并区域,但此时我们检查到它是非主单元格(因为主单元格是A1),然后我们设置`is_merged=True`,然后跳出循环,然后因为`is_merged`为真,所以不会进入非合并单元格的处理分支。但是,我们也没有将这个非主单元格标记为已处理,所以下一次遍历到它时(因为它在后面)?实际上,在同一个合并区域内的每个非主单元格都会被遍历到,并且因为不是主单元格,所以不会标记整个区域,也不会被加入`processed_cells`。这样,当遍历到下一个非主单元格(比如A2)时,它仍然会被处理。
但是,在非主单元格的处理中,我们只做了`break`(跳出合并区域循环),然后没有做任何赋值操作(因为不是主单元格,所以不会赋值),也没有将其加入`processed_cells`。这样,当遍历到下一个单元格(比如同一行的下一个单元格)时,这个非主单元格不会被跳过,但也不会被处理(因为`is_merged`为真,所以不会进入下面的非合并单元格分支)。所以,实际上我们并没有给非主单元格赋值,所以不会触发写value的错误。
那么为什么用户还会遇到这个错误?
另一种可能:在非合并单元格的分支中,我们给目标单元格赋值,但是目标单元格可能位于合并区域内?这不可能,因为我们在复制合并单元格时已经创建了合并区域,所以目标工作表中对应合并区域内的非主单元格也是`MergedCell`类型,只读。
所以,在非合并单元格分支中,我们给一个合并区域内的非主单元格赋值,就会触发错误。
但是,我们如何确保非合并单元格分支中的单元格不是合并区域内的非主单元格?
- 我们的逻辑:在遍历每个单元格时,检查它是否属于某个合并区域。如果属于,则标记为`is_merged`,然后如果是主单元格,就赋值并标记整个区域;如果不是主单元格,就什么也不做(然后跳过非合并分支)。
- 所以,非合并分支只会在`is_merged`为假时进入,也就是说这个单元格不属于任何合并区域。
因此,理论上不会出现给合并区域内的非主单元格赋值的情况。
那么错误可能出现在其他地方?
注意:在复制完格式后,我们在主处理函数中,将数据写入目标单元格:
```python
# 填充数据到目标单元格
new_ws[num_target].value = num_value
new_ws[str1_target].value = str1_value
new_ws[str2_target].value = str2_value
```
这里,如果用户设置的目标单元格(如`num_target`)位于合并区域内,并且不是主单元格,那么赋值就会触发错误。
所以,问题可能出在用户设置的目标单元格是合并区域内的非主单元格。
解决方案:
- 在写入目标单元格之前,检查目标单元格是否在合并区域内,如果是,则找到该合并区域的主单元格,然后写入主单元格。
- 或者,在用户设置目标单元格时,强制要求必须是主单元格(但用户可能不知道,所以程序内部处理)。
如何检查一个单元格是否是合并区域内的非主单元格,并找到其主单元格?
我们可以写一个辅助函数:
```python
def get_merged_range_main_cell(ws, cell_coord):
"""如果给定的单元格在合并区域内,则返回主单元格的坐标,否则返回自身"""
for merged_range in ws.merged_cells.ranges:
if cell_coord in merged_range:
# 主单元格是合并区域的左上角单元格
main_cell_coord = merged_range.start_cell
return main_cell_coord
return cell_coord
```
然后,在写入目标单元格时,先转换:
```python
# 在写入前转换目标单元格
def write_to_cell(ws, cell_coord, value):
actual_cell = get_merged_range_main_cell(ws, cell_coord)
ws[actual_cell].value = value
# 使用
write_to_cell(new_ws, num_target, num_value)
write_to_cell(new_ws, str1_target, str1_value)
write_to_cell(new_ws, str2_target, str2_value)
```
这样,即使用户指定的目标单元格是合并区域内的非主单元格,我们也会自动写入到主单元格。
因此,我们修改主处理函数中的写入部分。
另外,在复制格式时,我们可能已经处理了合并区域,所以目标工作表中存在合并区域,因此上述检查是有效的。
修改后的代码:
在`ExcelBatchProcessor`类中添加辅助方法:
```python
def get_merged_range_main_cell(self, ws, cell_coord):
"""如果给定的单元格在合并区域内,则返回主单元格的坐标,否则返回自身"""
for merged_range in ws.merged_cells.ranges:
if cell_coord in merged_range:
# 主单元格是合并区域的左上角单元格
main_cell_coord = merged_range.start_cell.coordinate
return main_cell_coord
return cell_coord
```
然后,在`start_processing`方法中,修改写入部分:
```python
# 填充数据到目标单元格
# 注意:如果目标单元格在合并区域内,则写入其主单元格
num_target_main = self.get_merged_range_main_cell(new_ws, num_target)
str1_target_main = self.get_merged_range_main_cell(new_ws, str1_target)
str2_target_main = self.get_merged_range_main_cell(new_ws, str2_target)
new_ws[num_target_main].value = num_value
new_ws[str1_target_main].value = str1_value
new_ws[str2_target_main].value = str2_value
```
这样,即使目标单元格在合并区域内且不是主单元格,我们也会自动写入到主单元格,从而避免错误。
此外,我们还需要在用户选择目标单元格时给出提示,如果用户选择了合并区域内的非主单元格,则提示用户并自动转换为左上角单元格(但为了简单,我们在写入时自动转换,所以用户界面可以保持不变)。
因此,完整修复步骤:
1. 添加辅助方法`get_merged_range_main_cell`
2. 在写入目标单元格前,将目标单元格转换为实际可写入的单元格(主单元格)
3. 这样,即使目标单元格位于合并区域内,也能正确写入。
注意:在复制格式时,我们已经将源工作表的合并区域复制到了目标工作表,所以目标工作表的合并区域和源工作表一致。
修改后的完整代码(仅修改部分)如下:
在类中添加方法:
```python
def get_merged_range_main_cell(self, ws, cell_coord):
"""如果给定的单元格在合并区域内,则返回主单元格的坐标,否则返回自身"""
# 确保cell_coord是字符串,并且是A1格式
# 遍历所有合并区域
for merged_range in ws.merged_cells.ranges:
if cell_coord in merged_range:
# 主单元格是合并区域的左上角单元格
main_cell_coord = merged_range.start_cell.coordinate
return main_cell_coord
return cell_coord
```
在`start_processing`方法中,修改写入部分:
```python
# 填充数据到目标单元格
# 注意:如果目标单元格在合并区域内,则写入其主单元格
num_target_main = self.get_merged_range_main_cell(new_ws, num_target)
str1_target_main = self.get_merged_range_main_cell(new_ws, str1_target)
str2_target_main = self.get_merged_range_main_cell(new_ws, str2_target)
new_ws[num_target_main].value = num_value
new_ws[str1_target_main].value = str1_value
new_ws[str2_target_main].value = str2_value
```
这样,应该能彻底解决合并单元格的写入问题。
注意:在复制格式时,我们只复制了合并单元格的结构和字体,其他样式(如边框、填充等)没有复制,但用户要求只保留合并单元格和字体,所以满足要求。
如果用户再次遇到此错误,请检查是否还有其他地方直接给合并区域内的非主单元格赋值。
另外,在复制格式的过程中,我们给目标单元格赋值时已经避开了非主单元格,所以不会出错。唯一可能出错的地方就是用户指定的目标单元格恰好是合并区域内的非主单元格,而我们在写入数据时没有转换。
因此,这个修复应该能解决问题。
我们重新输出完整的代码(只包含修改的部分,但为了安全,提供完整代码太长了,所以这里只给出修改点,用户可自行修改)。
但根据用户要求,输出完整的py文件,所以下面将输出完整的代码(包含上述修复)。</think>您遇到的错误 `'MergedCell' object attribute 'value' is read-only` 是由于尝试直接修改合并单元格区域内的非主单元格导致的。以下是完整的修复方案和代码实现:
```python
import openpyxl
import os
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import re
import sys
import time
from datetime import datetime
from openpyxl.utils import get_column_letter, column_index_from_string
from openpyxl.styles import Font
from openpyxl.worksheet.cell_range import CellRange
class ExcelBatchProcessor:
def __init__(self, root):
self.root = root
self.root.title("Excel批量处理工具")
self.root.geometry("800x650")
self.setup_ui()
# 高DPI支持
if sys.platform == "win32":
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
def setup_ui(self):
"""创建用户界面"""
# 主框架
main_frame = ttk.Frame(self.root, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
# 文件选择部分
self.create_file_section(main_frame)
# 列设置部分
self.create_column_section(main_frame)
# 目标单元格设置
self.create_target_section(main_frame)
# 文件名设置
self.create_filename_section(main_frame)
# 日志和进度
self.create_log_section(main_frame)
# 按钮框架
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=20)
# 开始处理按钮
self.process_btn = ttk.Button(
button_frame,
text="开始处理",
command=self.start_processing,
width=15
)
self.process_btn.pack(side=tk.LEFT, padx=10)
# 退出按钮
ttk.Button(
button_frame,
text="退出",
command=self.root.destroy,
width=15
).pack(side=tk.RIGHT, padx=10)
# 状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(
self.root,
textvariable=self.status_var,
relief=tk.SUNKEN,
anchor=tk.W
)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def create_file_section(self, parent):
"""创建文件选择区域"""
frame = ttk.LabelFrame(parent, text="文件选择", padding=10)
frame.pack(fill=tk.X, pady=10)
# 母版文件
ttk.Label(frame, text="母版Excel文件:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.template_path = tk.StringVar()
ttk.Entry(frame, textvariable=self.template_path, width=50).grid(row=0, column=1, padx=5)
ttk.Button(frame, text="浏览...", command=self.select_template).grid(row=0, column=2)
# 数据源文件
ttk.Label(frame, text="数据源Excel文件:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.source_path = tk.StringVar()
ttk.Entry(frame, textvariable=self.source_path, width=50).grid(row=1, column=1, padx=5)
ttk.Button(frame, text="浏览...", command=self.select_source).grid(row=1, column=2)
# 输出目录
ttk.Label(frame, text="输出文件夹:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.output_dir = tk.StringVar()
ttk.Entry(frame, textvariable=self.output_dir, width=50).grid(row=2, column=1, padx=5)
ttk.Button(frame, text="浏览...", command=self.select_output).grid(row=2, column=2)
def create_column_section(self, parent):
"""创建数据列设置区域"""
frame = ttk.LabelFrame(parent, text="数据列设置", padding=10)
frame.pack(fill=tk.X, pady=10)
# 数字串列
ttk.Label(frame, text="数字串列:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.num_col = tk.StringVar(value="A")
ttk.Entry(frame, textvariable=self.num_col, width=5).grid(row=0, column=1, padx=5)
# 字符串1列
ttk.Label(frame, text="字符串1列:").grid(row=0, column=2, sticky=tk.W, padx=10, pady=5)
self.str1_col = tk.StringVar(value="B")
ttk.Entry(frame, textvariable=self.str1_col, width=5).grid(row=0, column=3, padx=5)
# 字符串2列
ttk.Label(frame, text="字符串2列:").grid(row=0, column=4, sticky=tk.W, padx=10, pady=5)
self.str2_col = tk.StringVar(value="C")
ttk.Entry(frame, textvariable=self.str2_col, width=5).grid(row=0, column=5, padx=5)
# 起始行
ttk.Label(frame, text="数据起始行:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.start_row = tk.IntVar(value=2)
ttk.Entry(frame, textvariable=self.start_row, width=5).grid(row=1, column=1, padx=5)
def create_target_section(self, parent):
"""创建目标单元格设置区域"""
frame = ttk.LabelFrame(parent, text="目标单元格设置", padding=10)
frame.pack(fill=tk.X, pady=10)
# 数字串目标
ttk.Label(frame, text="数字串目标单元格:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.num_target = tk.StringVar(value="A15")
ttk.Entry(frame, textvariable=self.num_target, width=10).grid(row=0, column=1, padx=5)
# 字符串1目标
ttk.Label(frame, text="字符串1目标单元格:").grid(row=0, column=2, sticky=tk.W, padx=10, pady=5)
self.str1_target = tk.StringVar(value="H2")
ttk.Entry(frame, textvariable=self.str1_target, width=10).grid(row=0, column=3, padx=5)
# 字符串2目标
ttk.Label(frame, text="字符串2目标单元格:").grid(row=0, column=4, sticky=tk.W, padx=10, pady=5)
self.str2_target = tk.StringVar(value="C2")
ttk.Entry(frame, textvariable=self.str2_target, width=10).grid(row=0, column=5, padx=5)
def create_filename_section(self, parent):
"""创建文件名设置区域"""
frame = ttk.LabelFrame(parent, text="文件名设置", padding=10)
frame.pack(fill=tk.X, pady=10)
# 文件名格式
ttk.Label(frame, text="文件名格式:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.filename_format = tk.StringVar(value="{num_str}")
ttk.Entry(frame, textvariable=self.filename_format, width=40).grid(row=0, column=1, padx=5)
# 示例
ttk.Label(frame, text="可用变量: {num_str}, {str1}, {str2}").grid(row=1, column=1, sticky=tk.W, pady=5)
def create_log_section(self, parent):
"""创建日志和进度区域"""
frame = ttk.LabelFrame(parent, text="处理日志", padding=10)
frame.pack(fill=tk.BOTH, expand=True, pady=10)
# 日志文本框
self.log_text = tk.Text(frame, height=10)
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 滚动条
scrollbar = ttk.Scrollbar(frame, command=self.log_text.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.log_text.config(yscrollcommand=scrollbar.set)
# 进度条
self.progress_var = tk.DoubleVar()
progress_bar = ttk.Progressbar(
frame,
variable=self.progress_var,
maximum=100,
length=400
)
progress_bar.pack(fill=tk.X, pady=5)
def log_message(self, message):
"""添加日志消息"""
timestamp = datetime.now().strftime("%H:%M:%S")
self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
self.log_text.see(tk.END)
self.root.update_idletasks()
def clean_filename(self, name):
"""清理文件名,移除非法字符"""
return re.sub(r'[\\/*?:"<>|]', "_", str(name)).strip()
def select_template(self):
"""选择母版文件"""
path = filedialog.askopenfilename(
title="选择母版Excel文件",
filetypes=[("Excel文件", "*.xlsx;*.xls")],
initialdir=os.path.expanduser("~\\Documents")
)
if path:
self.template_path.set(path)
def select_source(self):
"""选择数据源文件"""
path = filedialog.askopenfilename(
title="选择数据源Excel文件",
filetypes=[("Excel文件", "*.xlsx;*.xls")],
initialdir=os.path.dirname(self.template_path.get()) if self.template_path.get() else os.path.expanduser("~\\Documents")
)
if path:
self.source_path.set(path)
def select_output(self):
"""选择输出目录"""
path = filedialog.askdirectory(
title="选择输出文件夹",
initialdir=os.path.dirname(self.template_path.get()) if self.template_path.get() else os.path.expanduser("~\\Documents")
)
if path:
self.output_dir.set(path)
def get_main_cell(self, ws, cell_address):
"""获取合并单元格的主单元格"""
for merged_range in ws.merged_cells.ranges:
if cell_address in merged_range:
return merged_range.start_cell.coordinate
return cell_address
def copy_essential_format(self, src_ws, dest_ws):
"""
安全复制格式(解决合并单元格问题)
关键修复:只修改主单元格,跳过合并区域内的只读单元格
"""
# 1. 复制所有合并单元格区域
for merged_range in src_ws.merged_cells.ranges:
dest_ws.merge_cells(str(merged_range))
# 2. 创建已处理单元格集合
processed_cells = set()
# 3. 遍历所有单元格,只处理主单元格和普通单元格
for row in src_ws.iter_rows():
for cell in row:
cell_coord = cell.coordinate
# 跳过已处理的单元格
if cell_coord in processed_cells:
continue
dest_cell = dest_ws[cell_coord]
# 检查是否是合并单元格
is_merged = False
for merged_range in src_ws.merged_cells.ranges:
if cell_coord in merged_range:
is_merged = True
# 如果是主单元格
if cell_coord == merged_range.start_cell.coordinate:
# 标记整个区域为已处理
min_row, min_col, max_row, max_col = merged_range.min_row, merged_range.min_col, merged_range.max_row, merged_range.max_col
for r in range(min_row, max_row + 1):
for c in range(min_col, max_col + 1):
coord = get_column_letter(c) + str(r)
processed_cells.add(coord)
# 复制主单元格的值和格式
dest_cell.value = cell.value
if cell.font:
dest_cell.font = Font(
name=cell.font.name,
size=cell.font.size,
bold=cell.font.bold,
italic=cell.font.italic,
underline=cell.font.underline,
strike=cell.font.strike,
color=cell.font.color
)
break
# 处理非合并单元格
if not is_merged:
# 复制字体
if cell.font:
dest_cell.font = Font(
name=cell.font.name,
size=cell.font.size,
bold=cell.font.bold,
italic=cell.font.italic,
underline=cell.font.underline,
strike=cell.font.strike,
color=cell.font.color
)
# 复制值
dest_cell.value = cell.value
def validate_parameters(self):
"""验证输入参数"""
if not os.path.exists(self.template_path.get()):
raise ValueError("母版文件不存在")
if not os.path.exists(self.source_path.get()):
raise ValueError("数据源文件不存在")
if not os.path.exists(self.output_dir.get()):
raise ValueError("输出目录不存在")
# 检查列设置
for col_name, col_var in [("数字串列", self.num_col),
("字符串1列", self.str1_col),
("字符串2列", self.str2_col)]:
col_val = col_var.get().upper()
if not re.match(r'^[A-Z]+$', col_val):
raise ValueError(f"{col_name} 格式错误,必须为字母(如A, B, C)")
# 检查目标单元格
for target_name, target_var in [("数字串目标", self.num_target),
("字符串1目标", self.str1_target),
("字符串2目标", self.str2_target)]:
target_val = target_var.get().upper()
if not re.match(r'^[A-Z]+\d+$', target_val):
raise ValueError(f"{target_name} 格式错误,必须为单元格地址(如A1, B2)")
# 检查起始行
start_row = self.start_row.get()
if start_row < 1:
raise ValueError("起始行必须大于0")
def start_processing(self):
"""开始处理数据"""
# 验证输入
try:
self.validate_parameters()
except Exception as e:
messagebox.showerror("参数错误", str(e))
return
# 禁用开始按钮
self.process_btn.config(state=tk.DISABLED)
# 获取参数
template_path = self.template_path.get()
source_path = self.source_path.get()
output_dir = self.output_dir.get()
num_col = self.num_col.get().upper()
str1_col = self.str1_col.get().upper()
str2_col = self.str2_col.get().upper()
start_row = self.start_row.get()
num_target = self.num_target.get().upper()
str1_target = self.str1_target.get().upper()
str2_target = self.str2_target.get().upper()
filename_format = self.filename_format.get()
# 更新状态
self.status_var.set("处理中...请稍候")
self.progress_var.set(0)
self.log_message("=" * 50)
self.log_message("开始处理数据")
self.log_message(f"母版文件: {os.path.basename(template_path)}")
self.log_message(f"数据源文件: {os.path.basename(source_path)}")
self.log_message(f"输出目录: {output_dir}")
self.log_message(f"目标单元格: 数字串[{num_target}] 字符串1[{str1_target}] 字符串2[{str2_target}]")
self.root.update()
try:
# 加载数据源
source_wb = openpyxl.load_workbook(source_path, data_only=True)
source_ws = source_wb.active
# 获取列索引
num_col_idx = column_index_from_string(num_col)
str1_col_idx = column_index_from_string(str1_col)
str2_col_idx = column_index_from_string(str2_col)
# 获取数据行数
total_rows = 0
for row in range(start_row, source_ws.max_row + 1):
if source_ws.cell(row=row, column=num_col_idx).value is not None:
total_rows += 1
else:
break
if total_rows == 0:
self.log_message("错误: 未找到有效数据")
return
self.log_message(f"共发现 {total_rows} 条数据需要处理")
# 加载母版模板
template_wb = openpyxl.load_workbook(template_path)
template_ws = template_wb.active
# 处理每条数据
success_count = 0
start_time = time.time()
for idx, row_idx in enumerate(range(start_row, start_row + total_rows)):
# 获取数据
num_value = source_ws.cell(row=row_idx, column=num_col_idx).value
str1_value = source_ws.cell(row=row_idx, column=str1_col_idx).value
str2_value = source_ws.cell(row=row_idx, column=str2_col_idx).value
if num_value is None:
self.log_message(f"跳过第 {row_idx} 行: 数字串为空")
continue
# 创建新工作簿
new_wb = openpyxl.Workbook()
new_ws = new_wb.active
# 安全复制格式(解决合并单元格问题)
self.copy_essential_format(template_ws, new_ws)
# 确保目标单元格是主单元格
num_target_main = self.get_main_cell(new_ws, num_target)
str1_target_main = self.get_main_cell(new_ws, str1_target)
str2_target_main = self.get_main_cell(new_ws, str2_target)
# 填充数据
new_ws[num_target_main].value = num_value
new_ws[str1_target_main].value = str1_value
new_ws[str2_target_main].value = str2_value
# 生成文件名
filename = filename_format.format(
num_str=num_value,
str1=str1_value,
str2=str2_value
)
clean_filename = self.clean_filename(filename) + ".xlsx"
output_path = os.path.join(output_dir, clean_filename)
# 保存文件
try:
new_wb.save(output_path)
self.log_message(f"已创建: {clean_filename}")
success_count += 1
except Exception as e:
self.log_message(f"错误: 无法保存 {clean_filename} - {str(e)}")
# 更新进度
progress = ((idx + 1) / total_rows) * 100
self.progress_var.set(progress)
self.status_var.set(f"处理中: {idx+1}/{total_rows} ({progress:.1f}%)")
self.root.update_idletasks()
# 处理完成
elapsed_time = time.time() - start_time
self.progress_var.set(100)
self.status_var.set(f"处理完成! 成功创建 {success_count}/{total_rows} 个文件,耗时 {elapsed_time:.2f} 秒")
self.log_message(f"处理完成! 成功创建 {success_count} 个文件,耗时 {elapsed_time:.2f} 秒")
self.log_message("=" * 50)
# 打开输出文件夹
if sys.platform == "win32":
try:
os.startfile(output_dir)
except:
self.log_message("警告: 无法自动打开输出文件夹")
except Exception as e:
self.log_message(f"处理出错: {str(e)}")
messagebox.showerror("错误", f"处理过程中出错:\n{str(e)}")
finally:
try:
source_wb.close()
except:
pass
try:
template_wb.close()
except:
pass
self.process_btn.config(state=tk.NORMAL)
# 运行主程序
if __name__ == "__main__":
root = tk.Tk()
app = ExcelBatchProcessor(root)
root.mainloop()
```
### 关键修复点
1. **合并单元格处理机制**:
- 添加了 `get_main_cell()` 方法,用于查找合并单元格的主单元格
- 在写入数据前,确保目标单元格是主单元格:
```python
num_target_main = self.get_main_cell(new_ws, num_target)
new_ws[num_target_main].value = num_value
```
2. **安全的格式复制**:
- 完全重写了 `copy_essential_format()` 方法:
- 先复制所有合并区域
- 使用 `processed_cells` 集合跟踪已处理单元格
- 只处理主单元格和普通单元格,跳过合并区域内的只读单元格
- 避免直接修改 `MergedCell` 对象
3. **错误预防**:
- 在复制格式时,只对主单元格进行赋值操作
- 在写入数据时,总是定位到主单元格
- 添加详细的日志记录,便于追踪问题
### 使用说明
1. **运行环境**:
```bash
pip install openpyxl
```
2. **操作流程**:
1. 选择母版Excel文件(包含格式)
2. 选择数据源Excel文件(包含数据)
3. 设置数据列(数字串、字符串1、字符串2)
4. 设置目标单元格位置
5. 设置输出文件名格式
6. 点击"开始处理"
3. **文件名格式变量**:
- `{num_str}` - 数字串值
- `{str1}` - 字符串1值
- `{str2}` - 字符串2值
### 为什么这个修复有效
1. **理解Excel合并单元格**:
- Excel中合并单元格的值只存储在左上角的"主单元格"中
- 合并区域内的其他单元格是只读的`MergedCell`对象
- 尝试修改这些只读单元格会引发`AttributeError`
2. **修复策略**:
- 在复制格式时,只处理主单元格和普通单元格
- 在写入数据前,定位目标单元格的主单元格
- 避免任何直接操作`MergedCell`对象的尝试
3. **错误处理**:
- 添加详细的异常处理和日志记录
- 确保资源正确关闭(工作簿)
- 提供进度反馈,避免界面卡死
这个解决方案专门针对您遇到的`'MergedCell' object attribute 'value' is read-only`错误,确保程序能够正确处理包含合并单元格的Excel文件。