|
| 1 | +--- |
| 2 | +title: 從手動到自動:數萬份電子簽名處理的最佳實務 |
| 3 | +authors: 袋鼠 |
| 4 | +description: 本文示範如何在本機端使用 Python(PyPDF2、reportlab、Pillow)批次為大量 PDF 自動加上電子簽名,包含簽名欄位偵測、座標調整與實務注意事項,適合需保護諮商紀錄隱私的單位。 |
| 5 | +tags: [PDF,Python,PDF自動簽名,諮商,本機處理,批次處理] |
| 6 | +date: 2026-01-07 |
| 7 | +--- |
| 8 | +# 從手動到自動:數份電子簽名處理的最佳實務 |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +## 問題意識與情境 |
| 13 | + |
| 14 | +那天,在工作的諮商所遇到了一個實際又棘手的狀況。由於過往部分心理師的諮商紀錄檔案中,未附上正式的電子簽名,因此我們必須協助補齊心理師的簽名檔案,才能符合衛生局的要求。 |
| 15 | + |
| 16 | +一開始,我們採取最直觀的方式:逐一打開 PDF 檔案,手動加入心理師的簽名。然而,當我們發現需要處理的紀錄橫跨 2020 年到 2025 年,累積上萬筆檔案 時,這個方法幾乎是不可能完成的任務,光是打開檔案就足以讓人崩潰。原本我們使用的是 **Adobe PDF** 來加入簽名檔,但實際操作後發現速度非常慢,效率極低。於是我開始思考: |
| 17 | + |
| 18 | +> ***是否有辦法一次性替某一位心理師的所有紀錄加上電子簽名?*** |
| 19 | +> |
| 20 | +
|
| 21 | +但問題並沒有這麼單純。不同的紀錄,**簽名欄位的位置並不固定**: |
| 22 | + |
| 23 | +有些在第一頁,有些在第二頁,甚至格式略有差異,無法單純用「固定頁數」處理。也因此,我開始研究是否能有一種方式,讓系統**自動判斷簽名欄位的位置,並正確地將電子簽名加上去**。在這個過程中,我開始與 **Claude AI** 討論可能的解法,也逐漸整理出一條可行的處理流程,於是便有了這篇紀錄文。 |
| 24 | + |
| 25 | +這套工具最大的好處在於: |
| 26 | + |
| 27 | +- **可在本機端一次處理大量 PDF 檔案**,不需逐一手動開啟 |
| 28 | +- 系統能自動辨識簽名位置(通常會有簽名欄位),並正確放置電子簽名 |
| 29 | +- 全程在自己的電腦上完成,符合諮商專業對於隱私與資料保護的要求 |
| 30 | + |
| 31 | +對於需要大量補齊電子簽名、又無法將檔案上傳至雲端或第三方服務的情境來說,這是一個非常實用的解法。 |
| 32 | + |
| 33 | +## 操作流程 |
| 34 | +> 重要申明:以下皆為本人自己製作檔案與模擬測試,皆無使用任何諮商紀錄! |
| 35 | +> |
| 36 | +
|
| 37 | +### 一、安裝 Python |
| 38 | +1.前往 Python 官網:https://www.python.org/downloads/ |
| 39 | +2.點選Or get the standalone installer for Python XXX(最新版本),即開始下載Python |
| 40 | +3.下載完後,開始執行安裝檔。在安裝第一個畫面的最下方,**必須勾選**: |
| 41 | + |
| 42 | + |
| 43 | + ☑ Add python.exe to PATH |
| 44 | + |
| 45 | +4.驗證安裝是否成功:Window系統開啟「命令提示字元」或「終端機」(可直接在電腦搜尋),執行:`python --version` |
| 46 | + |
| 47 | + 應該顯示類似:`Python xxx`(版本號碼)或是他是空白的,都代表安裝成功。 |
| 48 | + |
| 49 | +### 二、安裝 Python 套件 |
| 50 | + |
| 51 | +1.在命令提示字元或終端機輸入: |
| 52 | + |
| 53 | + python -m pip install --upgrade pip |
| 54 | + python -m pip install PyPDF2 reportlab pillow |
| 55 | + |
| 56 | +2.驗證安裝,並確認清單包含:PyPDF2、reportlab、pillow |
| 57 | + |
| 58 | + python -m pip list |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | +### 三、準備工作環境 |
| 64 | + |
| 65 | +1. 建立資料夾結構:(可以改成自己習慣或編排的模式) |
| 66 | + |
| 67 | + ```python |
| 68 | + 📁 實作程式/ |
| 69 | + 📄 pdf_signature.py ← 程式檔案 |
| 70 | + 🖼️ 簽名01.png ← 簽名圖片(PNG格式,建議背景透明) |
| 71 | + 📁 原始PDF資料夾/ ← 放入要處理的PDF |
| 72 | + 📄 記錄001.pdf |
| 73 | + 📄 記錄002.pdf |
| 74 | + 📄 記錄003.pdf |
| 75 | + ... |
| 76 | + 📁 已簽名PDF |
| 77 | + ``` |
| 78 | + |
| 79 | +2. 打開程式編輯器(推薦使用:[VS Code](https://code.visualstudio.com/)、[notepad++](https://notepad-plus-plus.org/downloads/)),並將以下程式碼貼上(詳細要修改程式碼內容可以參見底下附錄一) |
| 80 | + |
| 81 | + A. 程式結構 |
| 82 | + |
| 83 | + ``` |
| 84 | + 程式結構: |
| 85 | + ├── find_signature_page() → 找到「請簽名:」在哪一頁 |
| 86 | + ├── add_signature_to_pdf() → 在PDF上加簽名 |
| 87 | + ├── batch_process_pdfs() → 批次處理多個PDF |
| 88 | + └── 主程式 (if __name__ == "__main__") → 程式入口,設定參數 |
| 89 | + ``` |
| 90 | + |
| 91 | + B. 完整程式碼 |
| 92 | + |
| 93 | + ```python |
| 94 | + # -*- coding: utf-8 -*- |
| 95 | + # PDF 自動簽名程式 |
| 96 | + # 請先安裝需要的套件: pip install PyPDF2 reportlab pillow |
| 97 | + |
| 98 | + import os |
| 99 | + from PyPDF2 import PdfReader, PdfWriter |
| 100 | + from reportlab.pdfgen import canvas |
| 101 | + from reportlab.lib.pagesizes import letter |
| 102 | + from PIL import Image |
| 103 | + import io |
| 104 | + |
| 105 | + def find_signature_page(pdf_path, keyword="請簽名:"): |
| 106 | + """尋找包含簽名關鍵字的頁面""" |
| 107 | + try: |
| 108 | + reader = PdfReader(pdf_path) |
| 109 | + for page_num, page in enumerate(reader.pages): |
| 110 | + text = page.extract_text() |
| 111 | + if keyword in text: |
| 112 | + return page_num |
| 113 | + return None |
| 114 | + except Exception as e: |
| 115 | + print(f"讀取 {pdf_path} 時發生錯誤: {e}") |
| 116 | + return None |
| 117 | + |
| 118 | + def add_signature_to_pdf(input_pdf, output_pdf, signature_image, x=100, y=100, width=150, height=50): |
| 119 | + """在PDF指定位置加入簽名圖片""" |
| 120 | + try: |
| 121 | + reader = PdfReader(input_pdf) |
| 122 | + writer = PdfWriter() |
| 123 | + |
| 124 | + signature_page_num = find_signature_page(input_pdf) |
| 125 | + |
| 126 | + if signature_page_num is None: |
| 127 | + print(f"警告: {input_pdf} 找不到「請簽名:」字樣,跳過此檔案") |
| 128 | + return False |
| 129 | + |
| 130 | + packet = io.BytesIO() |
| 131 | + can = canvas.Canvas(packet, pagesize=letter) |
| 132 | + can.drawImage(signature_image, x, y, width=width, height=height, mask='auto', preserveAspectRatio=True) |
| 133 | + can.save() |
| 134 | + |
| 135 | + packet.seek(0) |
| 136 | + signature_pdf = PdfReader(packet) |
| 137 | + |
| 138 | + for page_num, page in enumerate(reader.pages): |
| 139 | + if page_num == signature_page_num: |
| 140 | + page.merge_page(signature_pdf.pages[0]) |
| 141 | + writer.add_page(page) |
| 142 | + |
| 143 | + with open(output_pdf, 'wb') as output_file: |
| 144 | + writer.write(output_file) |
| 145 | + |
| 146 | + print(f"完成: {input_pdf} -> 簽名加在第 {signature_page_num + 1} 頁") |
| 147 | + return True |
| 148 | + |
| 149 | + except Exception as e: |
| 150 | + print(f"處理 {input_pdf} 時發生錯誤: {e}") |
| 151 | + return False |
| 152 | + |
| 153 | + def batch_process_pdfs(input_folder, output_folder, signature_image): |
| 154 | + """批次處理資料夾內所有PDF""" |
| 155 | + |
| 156 | + if not os.path.exists(output_folder): |
| 157 | + os.makedirs(output_folder) |
| 158 | + |
| 159 | + pdf_files = [f for f in os.listdir(input_folder) if f.endswith('.pdf')] |
| 160 | + |
| 161 | + if not pdf_files: |
| 162 | + print("錯誤: 找不到任何PDF檔案") |
| 163 | + return |
| 164 | + |
| 165 | + print(f"找到 {len(pdf_files)} 個PDF檔案") |
| 166 | + print("開始處理...") |
| 167 | + print("") |
| 168 | + |
| 169 | + success_count = 0 |
| 170 | + fail_count = 0 |
| 171 | + |
| 172 | + for pdf_file in pdf_files: |
| 173 | + input_path = os.path.join(input_folder, pdf_file) |
| 174 | + output_path = os.path.join(output_folder, f"已簽名_{pdf_file}") |
| 175 | + |
| 176 | + if add_signature_to_pdf(input_path, output_path, signature_image): |
| 177 | + success_count += 1 |
| 178 | + else: |
| 179 | + fail_count += 1 |
| 180 | + |
| 181 | + print("") |
| 182 | + print("處理完成!") |
| 183 | + print(f"成功: {success_count} 個檔案") |
| 184 | + print(f"失敗: {fail_count} 個檔案") |
| 185 | + |
| 186 | + if __name__ == "__main__": |
| 187 | + # ===== 設定參數(可依需求修改)===== |
| 188 | + INPUT_FOLDER = "原始PDF資料夾" # 原始PDF所在資料夾 |
| 189 | + OUTPUT_FOLDER = "已簽名PDF" # 輸出資料夾 |
| 190 | + SIGNATURE_IMAGE = "簽名.png" # 簽名圖片檔名 |
| 191 | + |
| 192 | + # 簽名位置與大小(可調整) |
| 193 | + SIGNATURE_X = 350 # X座標(左右位置,數字越大越右) |
| 194 | + SIGNATURE_Y = 150 # Y座標(上下位置,數字越大越上) |
| 195 | + SIGNATURE_WIDTH = 120 # 簽名寬度 |
| 196 | + SIGNATURE_HEIGHT = 40 # 簽名高度 |
| 197 | + |
| 198 | + print("PDF 自動簽名程式") |
| 199 | + print("=" * 50) |
| 200 | + |
| 201 | + # 檢查簽名圖片 |
| 202 | + if not os.path.exists(SIGNATURE_IMAGE): |
| 203 | + print(f"錯誤: 找不到簽名圖片 {SIGNATURE_IMAGE}") |
| 204 | + print("請確保簽名圖片與程式在同一個資料夾") |
| 205 | + input("按 Enter 鍵結束...") |
| 206 | + exit() |
| 207 | + |
| 208 | + # 檢查輸入資料夾 |
| 209 | + if not os.path.exists(INPUT_FOLDER): |
| 210 | + print(f"錯誤: 找不到資料夾 {INPUT_FOLDER}") |
| 211 | + print("請建立資料夾並放入要處理的PDF檔案") |
| 212 | + input("按 Enter 鍵結束...") |
| 213 | + exit() |
| 214 | + |
| 215 | + # 執行批次處理 |
| 216 | + batch_process_pdfs(INPUT_FOLDER, OUTPUT_FOLDER, SIGNATURE_IMAGE) |
| 217 | + |
| 218 | + print("") |
| 219 | + input("按 Enter 鍵結束...") |
| 220 | + ``` |
| 221 | + |
| 222 | + |
| 223 | +### 四、執行程式 |
| 224 | + |
| 225 | +1.開啟命令提示字元或終端機,並切換到工作資料夾(請依實際建立的位置調整路徑),以下範例為存在桌面中有個「實作程式」的資料夾 |
| 226 | +:::info |
| 227 | +▶️知識+:什麼是cd? |
| 228 | + |
| 229 | + **cd** 是 **Change Directory** 的縮寫,中文意思是「**切換目錄**」或「**更改資料夾**」。 |
| 230 | + 舉例:當你打開命令提示字元時,通常會看到:`C:\Users\袋鼠>` |
| 231 | +
|
| 232 | +這表示您現在「站在」`C:\Users\袋鼠` 這個資料夾裡。 |
| 233 | +
|
| 234 | +如果不想要那麼麻煩去找他在哪,可以點選資料夾上排,按右鍵「複製位置」即可找到資料夾在那了。 |
| 235 | + |
| 236 | +::: |
| 237 | +2.執行程式:`python pdf_signature.py` |
| 238 | + |
| 239 | +3.完成的檔案呈現 |
| 240 | + |
| 241 | + |
| 242 | + |
| 243 | +### 附錄一:舉一反三修改程式-簽名位置與簽名關鍵字設定指南 |
| 244 | + |
| 245 | +#### (一)如何修改千名關鍵字 |
| 246 | + |
| 247 | +1.在程式的第 10 行: |
| 248 | + |
| 249 | + ```python |
| 250 | + def find_signature_page(pdf_path, keyword="請簽名:"): |
| 251 | + ``` |
| 252 | + |
| 253 | +(1)程式會在PDF中搜尋這個關鍵字 |
| 254 | +(2)找到關鍵字的那一頁,就是要加簽名的頁面 |
| 255 | +(3)**無論關鍵字在第1頁、第2頁或任何頁,程式都會自動找到** |
| 256 | + |
| 257 | +2.修改範例: |
| 258 | +(1)範例 1:改成「簽章:」 |
| 259 | +`def find_signature_page(pdf_path, keyword="簽章:"):` |
| 260 | +(2)範例 2:改成英文 |
| 261 | +`def find_signature_page(pdf_path, keyword="Signature:"):` |
| 262 | +(3)如果有多種可能的關鍵字-修改程式的第 16 行: |
| 263 | + |
| 264 | + ```python |
| 265 | + # 原本: |
| 266 | + if keyword in text: |
| 267 | + |
| 268 | + # 改成(可接受多種關鍵字): |
| 269 | + if keyword in text or "簽章:" in text or "請蓋章:" in text: |
| 270 | + ``` |
| 271 | + |
| 272 | + |
| 273 | + |
| 274 | +#### (二)如何調整簽名位置與大小 |
| 275 | + |
| 276 | +1.在程式的第 101-104 行: |
| 277 | + |
| 278 | +```python |
| 279 | +SIGNATURE_X = 350 # X座標(左右位置) |
| 280 | +SIGNATURE_Y = 150 # Y座標(上下位置) |
| 281 | +SIGNATURE_WIDTH = 120 # 簽名寬度 |
| 282 | +SIGNATURE_HEIGHT = 40 # 簽名高度 |
| 283 | +``` |
| 284 | + |
| 285 | +2.PDF 座標系統說明-座標原點在「左下角」 |
| 286 | + |
| 287 | +``` |
| 288 | +PDF頁面示意圖: |
| 289 | +
|
| 290 | + Y軸越大越上 ↑ |
| 291 | + | |
| 292 | + (0, 800) | (600, 800) ← 頁面上方 |
| 293 | + | |
| 294 | + | |
| 295 | + (0, 400) | (600, 400) ← 頁面中間 |
| 296 | + | |
| 297 | + | |
| 298 | + (0, 0) |________→ X軸越大越右 |
| 299 | + 左下角原點 (600, 0) |
| 300 | +``` |
| 301 | + |
| 302 | +| 參數 | 說明 | 效果 | |
| 303 | +| --- | --- | --- | |
| 304 | +| **X 增加** | 數字變大 | 簽名往右移 | |
| 305 | +| **X 減少** | 數字變小 | 簽名往左移 | |
| 306 | +| **Y 增加** | 數字變大 | 簽名往上移 | |
| 307 | +| **Y 減少** | 數字變小 | 簽名往下移 | |
| 308 | +| **WIDTH 增加** | 寬度變大 | 簽名變寬 | |
| 309 | +| **HEIGHT 增加** | 高度變大 | 簽名變高 | |
| 310 | + |
| 311 | +(1)步驟 1:用預設值測試-先用預設值處理 **1個PDF**,看看簽名在哪裡 |
| 312 | +(2)步驟 2:判斷要往哪個方向調整 |
| 313 | + |
| 314 | +| 簽名位置問題 | 調整方法 | 範例 | |
| 315 | +| --- | --- | --- | |
| 316 | +| 太左邊 | **增加 X** | 350 → 400 | |
| 317 | +| 太右邊 | **減少 X** | 350 → 300 | |
| 318 | +| 太下面 | **增加 Y** | 150 → 200 | |
| 319 | +| 太上面 | **減少 Y** | 150 → 100 | |
| 320 | +| 太小 | **增加 WIDTH 和 HEIGHT** | 120 → 150 | |
| 321 | +| 太大 | **減少 WIDTH 和 HEIGHT** | 120 → 80 | |
| 322 | + |
| 323 | +(3)步驟 3:每次調整 50 左右-不要一次調太多,建議每次調整 **30-50** 的幅度 |
| 324 | + |
| 325 | +## 總結 |
| 326 | + |
| 327 | +回頭看這個問題,其實它並不只是「怎麼把簽名加上去」那麼單純,而是反映了許多助人工作現場正在面對的現實:**行政符合規範的壓力,往往會在不知不覺中吞噬大量專業人力**。當制度要求被一條一條補齊時,如果只能依靠人工處理,最終消耗的,往往是工作者本就有限的時間與心力。 |
| 328 | + |
| 329 | +這次嘗試用程式解決問題,是希望把那些**可以被自動化的工作交給工具處理**,讓人能夠回到真正需要人去承接的地方。 |
| 330 | + |
| 331 | +如果你也正面臨大量文件處理、卻又受限於隱私與合規無法使用雲端服務,或許這個做法能成為一個參考起點。技術不一定要很炫,只要能**實際減輕現場負擔**,它就已經完成了它最重要的任務。 |
| 332 | + |
0 commit comments