吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]
查看: 4478|回复: 36
收起左侧

[Python 原创] 提取Word表格数据到Excel表实用工具(逆向邮件合并)

  [复制链接]
pythonfun 发表于 2025-8-10 12:10
本帖最后由 pythonfun 于 2025-8-10 14:46 编辑

一、软件名称:Word表格数据批量提取写入Excel表工具
二、软件功能
1. 根据template.docx中的占位符锁定word文件表格数据所在坐标,批量提取Files文件夹下其它同一版本word文件数据到Excel表。
2. 可以批量把多个Word文件数据批量提取存入到Excel表,一个文件一行数据。执行状态可显示。
3. 提取对象主要是Word表格中用占位符标记的数据。
三、使用方法
1. 模板中用 {{标记名}} 标注要提取的单元格,点击导入模板,右侧显示要提取数据信息
2. 点击文件标记信息按钮,软件按从上到下、从左到右提取数据,并遍历 Files 下所有 .docx ,doc要转为docx, 在相同(表序, 行, 列) 处取值写入汇总表.xlsx
四、注意事项
1. 首先要设定Word文件表格模板,要提取的数据要放入双花括号中。
2. 每个Word文件的表格列数、行数一致、版式相同,以便能够提取数据。
3. 更换Word文件时,注意修改模板文件。
五、软件截图
image.png
六、代码展示
[Python] 纯文本查看 复制代码
# -*- coding: utf-8 -*-
"""
基于模板标记的 Word 表格批量提取工具 (tkinter)
- 模板中用 {{标记名}} 标注要提取的单元格
- 解析模板所有表格,按 从上到下、从左到右 顺序记录 (表序, 行, 列, 标记名)
- 遍历 ./Files 下所有 .docx ,doc要转为docx, 在相同(表序, 行, 列) 处取值写入 ./汇总.xlsx
- UI 左:导入模板 / 提取文件标记信息 / 退出程序;右:Text 显示坐标与标记
"""
import os
import re
import traceback
import tkinter as tk
from tkinter import ttk, messagebox, filedialog

# 依赖
missing = []
try:
    from docx import Document
except Exception:
    missing = ["python-docx"]
try:
    from openpyxl import Workbook
except Exception:
    missing.append("openpyxl")

if missing:
    raise SystemExit("缺少依赖:{}\n请先执行:pip install {}".format(", ".join(missing), " ".join(missing)))

FILES_DIR = os.path.join(os.getcwd(), "Files")
OUTPUT_XLSX = os.path.join(os.getcwd(), "汇总.xlsx")

MARK_PATTERN = re.compile(r"\{\{(.+?)\}\}")  # {{标记名}}

# ----------------- Word 表解析:合并单元格锚点模型 -----------------
def _get_tcPr_prop(cell, name):
    try:
        tcPr = cell._tc.tcPr
        if tcPr is None:
            return None
        return getattr(tcPr, name)
    except Exception:
        return None

def _to_int(val, default=1):
    try:
        return int(str(val).strip())
    except Exception:
        return default

class GridCell:
    __slots__ = ("anchor", "visible", "text", "rowspan", "colspan")
    def __init__(self, anchor=None, visible=False, text="", rowspan=1, colspan=1):
        self.anchor = anchor
        self.visible = visible
        self.text = text
        self.rowspan = rowspan
        self.colspan = colspan

def build_table_grid(table):
    """将 python-docx 的表解析为仅锚点可见的网格,处理 gridSpan / vMerge(continue/restart)"""
    # 列数(考虑横向合并)
    row_col_counts = []
    for row in table.rows:
        cnt = 0
        for cell in row.cells:
            gs = _get_tcPr_prop(cell, "gridSpan")
            val = getattr(gs, "val", None) if (gs is not None) else None
            colspan = _to_int(val, 1) if (val is not None) else 1
            cnt += max(1, colspan)
        row_col_counts.append(cnt)
    n_rows = len(table.rows)
    n_cols = max(row_col_counts) if row_col_counts else 0

    grid = [[None for _ in range(n_cols)] for _ in range(n_rows)]
    anchors = {}

    def occupy(ar, ac, rs, cs, text):
        for rr in range(ar, ar + rs):
            for cc in range(ac, ac + cs):
                grid[rr][cc] = GridCell(anchor=(ar, ac),
                                        visible=(rr == ar and cc == ac),
                                        text=(text if rr == ar and cc == ac else ""),
                                        rowspan=(rs if rr == ar and cc == ac else 1),
                                        colspan=(cs if rr == ar and cc == ac else 1))
    for r, row in enumerate(table.rows):
        c = 0
        for cell in row.cells:
            # 跳到空位
            while c < n_cols and grid[r][c] is not None:
                c += 1
            if c >= n_cols:
                break

            gs = _get_tcPr_prop(cell, "gridSpan")
            colspan = _to_int(getattr(gs, "val", 1), 1) if (gs is not None) else 1
            colspan = max(1, colspan)

            vm = _get_tcPr_prop(cell, "vMerge")
            vm_val = None
            if vm is not None:
                try:
                    vm_val = (str(vm.val).strip().lower() if vm.val is not None else None)
                except Exception:
                    vm_val = None
            # 兼容 <w:vMerge/> 与 w:val="continue"
            is_continue = (vm is not None) and (vm_val is None or vm_val == "continue")

            text = cell.text.replace("\n", " ").strip()

            if not is_continue:
                # 普通/起点:创建锚点
                occupy(r, c, 1, colspan, text)
                anchors[(r, c)] = {"rowspan": 1, "colspan": colspan}
            else:
                # 继续:向上找锚点并扩展
                rr = r - 1
                anchor_found = None
                while rr >= 0 and anchor_found is None:
                    gc = grid[rr][c]
                    if gc is not None:
                        anchor_found = gc.anchor
                        break
                    rr -= 1
                if anchor_found is None:
                    occupy(r, c, 1, colspan, text)
                    anchors[(r, c)] = {"rowspan": 1, "colspan": colspan}
                else:
                    ar, ac = anchor_found
                    meta = anchors.get((ar, ac))
                    if meta:
                        meta["rowspan"] += 1
                        for cc in range(ac, ac + meta["colspan"]):
                            grid[r][cc] = GridCell(anchor=(ar, ac), visible=False)
                    else:
                        occupy(r, c, 1, colspan, text)
                        anchors[(r, c)] = {"rowspan": 1, "colspan": colspan}

            c += colspan
            while c < n_cols and grid[r][c] is not None:
                c += 1

    # 回填跨度
    for (ar, ac), meta in anchors.items():
        gc = grid[ar][ac]
        if gc:
            gc.rowspan = meta["rowspan"]
            gc.colspan = meta["colspan"]

    # 填补
    for r in range(n_rows):
        for c in range(n_cols):
            if grid[r][c] is None:
                grid[r][c] = GridCell()
    return grid, n_rows, n_cols

# ----------------- 模板解析:收集 {{标记}} 与坐标 -----------------
def collect_marks_from_template(doc_path):
    """
    返回一个按读取顺序的列表:
    [{'table': ti, 'row': r, 'col': c, 'name': '标记名'} , ...]
    顺序:表格从前到后;表内从上到下、从左到右(锚点可见格)。
    """
    if not os.path.isfile(doc_path):
        raise FileNotFoundError(doc_path)
    doc = Document(doc_path)
    marks = []
    for ti, table in enumerate(doc.tables):
        grid, n_rows, n_cols = build_table_grid(table)
        for r in range(n_rows):
            for c in range(n_cols):
                gc = grid[r][c]
                if not gc.visible:
                    continue
                if not gc.text:
                    continue
                # 匹配一个或多个 {{...}}(若同一格多个,依次加入)
                for m in MARK_PATTERN.finditer(gc.text):
                    name = m.group(1).strip()
                    if name:
                        marks.append({"table": ti, "row": r, "col": c, "name": name})
    return marks

# ----------------- UI -----------------
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Word 表格批量提取(模板标记)pythonfun作品")
        self.geometry("610x380")

        # 字体样式(微软雅黑 11号)
        try:
            import tkinter.font as tkfont
            default_font = tkfont.nametofont("TkDefaultFont")
            text_font = tkfont.nametofont("TkTextFont")
            fixed_font = tkfont.nametofont("TkFixedFont")
            for f in (default_font, text_font, fixed_font):
                f.configure(family="Microsoft YaHei", size=11)
        except Exception:
            pass

        style = ttk.Style()
        style.configure("TButton", padding=(10, 6))
        style.configure("TLabel", padding=(2, 2))
        style.configure("TFrame", padding=(8, 8))

        # 状态
        self.template_path = None
        self.marks = []  # [{'table', 'row','col','name'}]

        self._build_ui()

        if not os.path.isdir(FILES_DIR):
            os.makedirs(FILES_DIR, exist_ok=True)

    def _build_ui(self):
        self.grid_rowconfigure(0, weight=1)
        # 固定左侧最小宽度为 180 像素
        self.grid_columnconfigure(0, minsize=180, weight=0)
        # 右侧权重高一些,优先扩展
        self.grid_columnconfigure(1, weight=1)


        # 左栏
        left = ttk.Frame(self)
        left.grid(row=0, column=0, sticky="ns")
        left.grid_rowconfigure(10, weight=1)

        ttk.Label(left, text="操作").grid(row=0, column=0, sticky="w", pady=(0, 6))

        ttk.Button(left, text="导入模板(template.docx)", command=self.on_load_template)\
            .grid(row=1, column=0, sticky="ew", pady=6)
        ttk.Button(left, text="提取文件标记信息(Files目录下)", command=self.on_extract_all)\
            .grid(row=2, column=0, sticky="ew", pady=6)
        ttk.Button(left, text="打开汇总表", command=self.open_xlsx)\
            .grid(row=3, column=0, sticky="ew", pady=6)
        ttk.Button(left, text="退出程序", command=self.destroy)\
            .grid(row=4, column=0, sticky="ew", pady=6)

        tip = ("使用说明:\n"
               "1) 点击“导入模板”,选择含 {{标记}} 的 DOCX。\n"
               "2) 右侧将列出 表序/坐标/标记。\n"
               "3) 点击“提取文件标记信息”,遍历 Files 目录下所有 DOCX,\n"
               "在相同表与坐标读取值,写入“汇总.xlsx”。")
        ttk.Label(left, text=tip, justify="left").grid(row=5, column=0, sticky="w", pady=(8,0))

        # 右侧:Text 显示标记列表
        right = ttk.Frame(self)
        right.grid(row=0, column=1, sticky="nsew")
        right.grid_rowconfigure(1, weight=1)
        right.grid_columnconfigure(0, weight=1)

        ttk.Label(right, text="标记从上到下→左到右").grid(row=0, column=0, sticky="w", pady=(0,4))
        self.text = tk.Text(right, wrap="word")
        self.text.grid(row=1, column=0, sticky="nsew")
        ybar = ttk.Scrollbar(right, orient="vertical", command=self.text.yview)
        self.text.configure(yscrollcommand=ybar.set)
        ybar.grid(row=1, column=1, sticky="ns")

        # 底部状态条
        self.status = ttk.Label(self, text="准备就绪")
        self.status.grid(row=1, column=0, columnspan=2, sticky="ew")
    def open_xlsx(self):
        try:
            os.startfile("汇总.xlsx")
        except:
            messagebox("汇总表没有生成!")
    def set_status(self, msg):
        self.status.config(text=msg)
        self.update_idletasks()

    # ---------- 事件 ----------
    def on_load_template(self):
        path = filedialog.askopenfilename(
            title="选择模板(包含 {{标记}} 的 DOCX)",
            filetypes=[("Word 文档", "*.docx")]
        )
        if not path:
            return
        try:
            self.set_status("正在解析模板...")
            self.marks = collect_marks_from_template(path)
            self.template_path = path
            self.show_marks()
            if not self.marks:
                messagebox.showwarning("提示", "未在模板的表格里找到 {{标记}}。")
            else:
                messagebox.showinfo("完成", f"模板解析完成,共找到 {len(self.marks)} 个标记。")
            self.set_status("模板解析完成")
        except Exception:
            messagebox.showerror("错误", "解析模板失败,请查看控制台日志。")
            traceback.print_exc()
            self.set_status("解析失败")

    def show_marks(self):
        self.text.delete("1.0", "end")
        if not self.marks:
            self.text.insert("end", "尚未加载模板或未识别到标记。\n")
            return
        lines = []
        for i, m in enumerate(self.marks, 1):
            lines.append(f"[{i:02d}] T{m['table']+1}  R{m['row']+1}C{m['col']+1}  ->  {{ {m['name']} }}")
        self.text.insert("end", "\n".join(lines))

    def on_extract_all(self):
        if not self.marks:
            messagebox.showinfo("提示", "请先导入包含 {{标记}} 的模板。")
            return
        if not os.path.isdir(FILES_DIR):
            messagebox.showwarning("提示", f"未找到目录:{FILES_DIR}")
            return
        docx_paths = [os.path.join(FILES_DIR, f) for f in os.listdir(FILES_DIR) if f.lower().endswith(".docx")]
        if not docx_paths:
            messagebox.showinfo("提示", "Files 目录中没有 .docx 文件。")
            return

        self.set_status("正在遍历 Files 提取数据...")
        # 准备列头
        #headers = ["文件名"] + [m["name"] for m in self.marks]

        rows_out = []

        for p in docx_paths:
            try:
                doc = Document(p)
            except Exception as e:
                fname = os.path.splitext(os.path.basename(p))[0]
                rows_out.append([fname] + [""]*len(self.marks))
                continue

            values = []
            for m in self.marks:
                ti, r, c = m["table"], m["row"], m["col"]
                if ti >= len(doc.tables):
                    values.append("")
                    continue
                grid, n_rows, n_cols = build_table_grid(doc.tables[ti])
                if not (0 <= r < n_rows and 0 <= c < n_cols):
                    values.append("")
                    continue
                gc = grid[r][c]
                # 如果坐标落在被覆盖区域,取其锚点文本
                if gc.visible:
                    txt = gc.text
                elif gc.anchor:
                    ar, ac = gc.anchor
                    txt = grid[ar][ac].text if (0 <= ar < n_rows and 0 <= ac < n_cols) else ""
                else:
                    txt = ""
                # 清理文本中的可能残留的 {{...}}(目标文件一般不会有,但以防万一)
                txt = MARK_PATTERN.sub("", txt).strip()
                values.append(txt)
            fname = os.path.splitext(os.path.basename(p))[0]
            rows_out.append([fname] + values)

        # 写入 Excel
        try:
                
            # >>> 在这里加过滤行
            rows_out = [row for row in rows_out if any(cell for cell in row[1:])]

            wb = Workbook()
            ws = wb.active
            ws.title = "提取结果"
            #ws.append(headers)
            for row in rows_out:
                ws.append(row)
            wb.save(OUTPUT_XLSX)
            self.set_status(f"已写入:{OUTPUT_XLSX}")
            messagebox.showinfo("完成", f"提取完成,已写入:\n{OUTPUT_XLSX}")
        except Exception:
            messagebox.showerror("写入失败", "保存 Excel 时出错,请查看控制台日志。")
            traceback.print_exc()
            self.set_status("写入失败")


if __name__ == "__main__":
    try:
        app = App()
        app.mainloop()
    except SystemExit as e:
        print(str(e))
    except Exception:
        traceback.print_exc()

七、源码、样例下载
提取word表格数据写入到Excel pythonfun作品.zip (75.52 KB, 下载次数: 364)

免费评分

参与人数 6吾爱币 +13 热心值 +5 收起 理由
kexue8 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ssk148150105 + 1 我很赞同!
grrr_zhao + 1 + 1 谢谢@Thanks!
sunflash + 1 + 1 谢谢@Thanks!
cxx0515 + 2 + 1 用心讨论,共获提升!
hrh123 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

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

xiguapi66 发表于 2025-11-8 22:33
大佬好,遇到这个问题,模版里的占位符有在表格里,也有在非表格的数据,需要都提取出来。而且占位符{{1}},可能有多次出现,如果有三个占位符{{1}},那么按模版占位符从左到右,提取到exceL的A列,A列就有三行数据了。占位符{{2}}映射到EXCEL的B列,占位符{{3}}映射到EXCEL的C列,里面的数字映射EXCEL的第几列。非表格的数据,比如温度,一个模版就出现一次,那么一个word文档单独映射到EXCEL的独立列。问了AI,数据提取不出来。大佬有空的时候,能否指导一二。感谢
 楼主| pythonfun 发表于 2025-8-10 12:40
zhh199234 发表于 2025-8-10 12:33
要是能反过来用就好了

反过来也有工具呀,用这个:https://www.52pojie.cn/forum.php?mod=viewthread&tid=2052125&page=2#pid53637102
zhh199234 发表于 2025-8-10 12:33
twapj 发表于 2025-8-10 14:22
很不错的工具
yly 发表于 2025-8-10 14:36
不错的工具,谢谢楼主分享。
ruanxiaoqi 发表于 2025-8-10 15:40
这个太及时了,工作中确实需要将word表格内容转化成Excel表格,方便统计和整理。
aguangchang 发表于 2025-8-10 16:35
好像很好用的样子
xingdh 发表于 2025-8-10 16:58
代码能在本人的python环境中运行,就是不明白模板文件应该怎么去制作的,感谢楼主提供源代码。
shiqiang 发表于 2025-8-10 17:04
感谢分享,能否把excel表写入Word表格?
 楼主| pythonfun 发表于 2025-8-10 17:07
shiqiang 发表于 2025-8-10 17:04
感谢分享,能否把excel表写入Word表格?

有呀,昨天发的就有,你可以看看。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-7-21 20:59

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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