---
title: "個資遮蔽"
description: "EMILY.RPA 個資遮蔽模組：將文字中的身分證、手機、地址、Email 等個資換成佔位符後再送給 LLM，取回結果後依對照表還原，明文個資不離開本機。"
source: https://docs.emily.tips/wap/pii-redaction
---

# 個資遮蔽

這個模組在本機把文字中的個資換成佔位符（placeholder），讓後續模組可以安全地把內容送給 LLM；拿回 LLM 的結果後，再用同一份對照表（map）把佔位符還原成原文。整個流程零相依、全非同步，明文個資不會隨 request 送出。

![](https://docs.emily.tips/img/zh/wap-1020.png)

## 參數

**`FILE`** - 文字輸入檔，點擊「**PICK**」選取檔案、鍵盤輸入工作資料夾中的檔名，或使用 **%FILENAME%** 變數。`ACTION` 為 **REDACT** 時是要遮蔽的原文；為 **RESTORE** 時是 LLM 回傳、含佔位符的文字。

**`ACTION`** - 執行動作：

- **REDACT** - 偵測 `FILE` 中的個資並換成佔位符，遮蔽後文字寫入 `OUTPUT FILE`，對照表寫入 `MAP FILE`。
- **RESTORE** - 讀取 `MAP FILE`，把 `FILE` 中的佔位符還原成原文後寫入 `OUTPUT FILE`。

**`OUTPUT FILE`** - 輸出檔名，預設 `output.txt`，寫入工作資料夾。

**`MAP FILE`** - 對照表檔名，預設 `redaction-map.json`。REDACT 時產生，RESTORE 時讀取。

:::danger 對照表就是明文個資本身
`MAP FILE` 的內容存的是未經處理的明文個資，不需要還原時直接刪掉這個檔案即可。
:::

## No-Code 編輯器

點擊「**ADD NO-CODE COMMAND**」可以加入下列無程式碼指令，指令會記錄在模組的指令列表，並在執行時套用到偵測規則上：

### 加入黑名單

填入的字串**保證不會流出去**：完全比對，命中就換成 `{{BLOCKED_n}}`。適合客戶姓名、未公開的專案代號等 regex 抓不到的機敏字串。

### 加入白名單

填入的字串即使被規則命中也**不遮蔽**，例如公司對外的客服信箱、官網網址。用來壓掉已知的誤報。

:::caution
黑名單內容本身是機敏資料。技能檔若會分享給他人，先確認裡面沒有不該公開的名字。
:::

## Low-Code 編輯器

在 Low-Code 編輯器中可以直接調整偵測選項，腳本在遮蔽執行前被呼叫，適合放無法用 No-Code 表達的規則。

![](https://docs.emily.tips/img/zh/wap-1021.png)

### input 輸入物件

```javascript
{
  text   // FILE 的完整文字內容
}
```

### options 選項物件

```javascript
{
  patterns,   // 啟用的內建規則集合，例如 ['tw', 'generic', 'financial']
  rules,      // 自訂規則陣列
  denylist,   // 黑名單，等同 No-Code 的「加入黑名單」
  allowlist,  // 白名單，等同 No-Code 的「加入白名單」
  disable     // 要關閉的內建型別，例如 ['URL']
}
```

範例：

```javascript
// 自訂員工編號規則
options.rules.push({ type: 'EMP_ID', pattern: /EMP-\d{6}/g })

// 黑名單與白名單
options.denylist.push('Best Ltd.')
options.allowlist.push('support@emily.tips')

// URL 通常不是個資，太吵就關掉
options.disable.push('URL')
```

## 內建型別

`patterns` 決定載入哪些規則集合，只啟用 `['tw']` 就真的只載入台灣規則。

| 集合 | 型別 | priority | 備註 |
|---|---|---|---|
| `tw` | `TW_ADDRESS` | 40 | 最高。地址內含數字，不然會被電話/統編咬走 |
| `tw` | `TW_ID` | 35 / 30 | 帶「身分證:」標籤的走 35 且不驗檢查碼；裸 pattern 走 30 且驗檢查碼 |
| `tw` | `TW_ARC` | 30 | 新式 `[A-Z][89]\d{8}` 與舊式 `[A-Z][A-D]\d{8}` |
| `tw` | `TW_MOBILE` | 22 | |
| `tw` | `TW_TEL` | 20 | |
| `tw` | `TW_GUI` | 15 | 裸 8 位數，誤報風險最高，檢查碼必驗 |
| `generic` | `EMAIL` | 30 | |
| `generic` | `URL` / `IPV4` | 25 | URL 常常不是個資，覺得太吵用 `disable: ['URL']` |
| `financial` | `CREDIT_CARD` / `IBAN` | 30 | Luhn / mod-97 |
| — | 自訂規則 | 50（預設） | 讓自訂勝過內建成為直覺行為 |

**地址 priority 最高**：自訂的數字規則若沒生效，先確認是不是被 `TW_ADDRESS` 蓋掉了。

`LITERAL` 與 `BLOCKED` 是保留型別，自訂規則不能使用。

## 核心不變式

```javascript
restore(redact(x).redacted, redact(x).map).text === x
```

對任意輸入、任意規則組合、任意黑名單都成立，沒有例外分支。

做得到是因為偵測跑在正規化後的文字上（全形→半形、刪零寬字元），但**替換發生在原始輸入上**，對照表存的也是原始子字串。使用者輸入 `０９１２－３４５－６７８` 還原後仍是 `０９１２－３４５－６７８`，不會被靜默改成半形。

遮蔽只有一種模式：偵測到的 span **完整置換成佔位符**。沒有 mask、沒有 format-preserving。

## 黑名單比對規則

黑名單是完全比對，比對對象是正規化後的文字（否則輸入全形公司名就抓不到），大小寫預設不敏感（只影響 ASCII，CJK 沒有大小寫概念）。

「優先級最高」的意思是保證覆蓋，不是搶佔佔位符：

| 情況 | 結果 |
|---|---|
| span 完全相同 | 黑名單勝 |
| 黑名單 span ⊃ 其他 span | 黑名單勝 |
| 部分重疊 | 黑名單勝，另一方捨棄 |
| 黑名單 span ⊂ 其他 span | **保留較大的那個** |

最後一列是刻意的。黑名單有「王小明」、地址規則抓到整串「台北市信義區信義路五段7號 王小明 收」時，若讓黑名單搶贏，結果是地址整串沒遮，優先級最高反而造成外洩。不提供「嚴格模式」，嚴格換來的是更差的保護。

沒有 `wholeWord` 選項：需要詞界的情境請用自訂規則寫 regex，CJK 沒有詞界、子字串比對是正確預設。

## 自訂規則

```javascript
options.rules.push({
  type: 'CASE_NO',                       // 必須符合 /^[A-Z][A-Z0-9_]*$/
  pattern: /案號\s*[:：]\s*(?<redact>\d{6,10})/g,
  priority: 60,
  validate: (m) => m.value !== '000000', // 可 async
})
```

- **`redact` 具名群組**：有就只遮該群組，沒有就遮整個 match。用來處理「需要上下文才能確定，但不該把上下文一起遮掉」。
- **`g` flag 會自動補上**（沒有 g 只會抓到第一個），並發一次 warning。
- **`lastIndex` 不會殘留**：每次執行都複製一份 RegExp，同一個 rule 物件連續執行結果一致。
- **type 撞到內建型別會 fail fast**，要取代內建請把它加進 `disable`。
- **`pattern` 可以是字串**，讓規則能從 JSON 設定檔來。但**含 `validate` 的規則無法序列化**，需要驗證邏輯的必須寫在腳本裡。

## 還原與模糊比對

模型會弄壞佔位符，還原時分層處理，`maxTier` 控制放寬到哪一層：

| Tier | 處理 | 預設 |
|---|---|---|
| 1 | 完全比對 | ✓ |
| 2 | 大小寫不敏感 | ✓ |
| 3 | 全形/半形括號正規化（`｛｛`→`{{`）、缺一邊括號 | ✓ |
| 4 | 括號可選（`TW_ID_1` 裸寫也認） | 需明確指定 |

**編號距離 ≤1 的猜測不實作。** 把 `{{NAME_2}}` 猜成 `NAME_1` 還原成錯的人，比不還原嚴重得多，使用者會相信畫面上的內容。還原失敗的 fail-safe 是保留佔位符原樣。

還原結果的 `expected` / `matched` / `orphaned` 是統計數字，不含個資，可以安全上報：

- `matched < expected` → 模型吃掉了佔位符，應提示使用者有內容可能未正確還原
- `orphaned` 非空 → 模型自己編了一個佔位符，通常是它模仿了格式

兩個數字都在告訴你送給 LLM 的 system prompt 指示夠不夠強。

### 佔位符注入防護

輸入本身含 `{{NAME_1}}` 時（使用者貼上的，或惡意構造），還原會被輸入牽著走，是資訊洩漏。模組會把它登記成 `LITERAL` 對應回自己：

```
輸入:  我叫王小明，參考 {{NAME_1}} 這個格式
遮蔽:  我叫{{BLOCKED_1}}，參考 {{LITERAL_1}} 這個格式
map:   { "{{BLOCKED_1}}": "王小明", "{{LITERAL_1}}": "{{NAME_1}}" }
```

還原是**單次掃描、不遞迴**，`{{LITERAL_1}}` 還原出的 `{{NAME_1}}` 不會被第二輪再替換一次。

## 典型流程

1. 「**個資遮蔽**」模組 `ACTION` 設 **REDACT**，把使用者輸入遮蔽成 `output.txt`，對照表存 `redaction-map.json`。
2. 以 [AI Agent](/ai-agent/intro) 或其他 LLM 模組讀取 `output.txt` 產生回答。
3. 再放一個「**個資遮蔽**」模組，`ACTION` 設 **RESTORE**，`FILE` 指向 LLM 的回答檔，`MAP FILE` 用同一份 `redaction-map.json`，還原後的文字即為最終結果。
