---
name: contract-rework-desensitize
description: 合同审查返工任务处理+文件脱敏流程。从Nextcloud取返工文件，按事件分组处理，完成后对敏感信息进行脱敏（当事人名称、金额、个人信息），交付脱敏版本。
tags: [contract, rework, desensitize, docx, nextcloud]
---

# 合同审查返工任务——处理与脱敏

## 触发条件
- 收到返工任务记录文件（通常包含多个"事件"）
- 需要对已审查合同的修订文件进行脱敏处理
- Doro或其他指导人要求将返工文件脱敏后归档

## 第一阶段：返工文件获取与确认

### Step 1: 定位文件
1. 在Nextcloud的Doro目录下查找返工文件：
   - 主目录：`/var/www/html/data/doro/files/Doro合同审查任务/`
   - 子目录：`待审查/`、`任务交付/`、`参考文件/`
   - 也可能在：`/var/www/html/data/admin/files/小Maggie协作区/`
2. **必须精确匹配文件名**，"差不多"等于没找到
3. 找到后先报告：文件名、路径、修改时间，确认后再操作

### Step 2: 复制到工作区
```bash
# 从Nextcloud容器复制到本地工作目录
docker cp nextcloud-aio-nextcloud:/var/www/html/data/<user>/files/<path>/<filename> /tmp/rework/
```

### Step 3: 确认返工内容
- 阅读返工任务记录，理解每个"事件"的返工要求
- 按事件分组整理对应的合同文件
- 确认哪些文件需要脱敏

## 第二阶段：脱敏处理

### 核心原则
- 使用**XML级别操作**（zipfile + lxml），不用python-docx（它会破坏修订标记）
- 必须处理**修订标记内容**（w:ins / w:del 中的文本）
- 需要**多轮扫描**，因为文本可能跨多个XML run被拆分
- 每轮替换后运行验证脚本确认

### Step 4: 建立替换规则
根据合同内容，确定以下替换映射：

| 类别 | 原始内容 | 替换为 |
|------|---------|--------|
| 甲方名称 | 具体单位名 | `甲方单位` |
| 乙方名称 | 具体公司名 | `乙方单位` |
| 个人姓名 | 法定代表人、联系人等 | `XXX` |
| 金额 | 具体数字 | `XXXXXX` |
| 地址 | 具体地址 | `XX路XX号` |
| 电话 | 手机/座机 | `XXXXXXXXXXX` |
| 银行账号 | 具体账号 | `XXXXXXXXXXXXXXX` |
| 统一社会信用代码 | 具体代码 | `XXXXXXXXXXXXXXXXXX` |

### Step 5: 执行脱敏（Python脚本）

```python
import zipfile, shutil, os, re, copy
from lxml import etree
from io import BytesIO

NSMAP = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'}

def desensitize_docx(input_path, output_path, replacements):
    """
    replacements: list of (pattern_str_or_regex, replacement_str)
    """
    with zipfile.ZipFile(input_path, 'r') as zin:
        with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zout:
            for item in zin.infolist():
                data = zin.read(item.filename)
                if item.filename in ('word/document.xml', 'word/header1.xml',
                                      'word/header2.xml', 'word/footer1.xml',
                                      'word/footer2.xml', 'word/comments.xml'):
                    data = desensitize_xml(data, replacements)
                zout.writestr(item, data)

def desensitize_xml(xml_bytes, replacements):
    tree = etree.fromstring(xml_bytes)

    # 1) 逐个w:t节点直接替换
    for t_node in tree.iter('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t'):
        if t_node.text:
            for pattern, repl in replacements:
                t_node.text = t_node.text.replace(pattern, repl)

    # 2) 处理跨run拆分的情况：拼接同一段落/ins/del内所有w:t的文本，
    #    检查拼接后是否包含敏感词，如果是则在首个匹配run中替换，清空后续run
    for parent in tree.iter():
        runs = parent.findall('.//w:r', NSMAP)
        if len(runs) < 2:
            continue
        full_text = ''
        t_nodes = []
        for r in runs:
            for t in r.findall('.//w:t', NSMAP):
                if t.text:
                    full_text += t.text
                    t_nodes.append(t)
        if not full_text:
            continue
        for pattern, repl in replacements:
            if pattern in full_text:
                # 重建：把替换后的文本放在第一个t_node，清空其余
                full_text = full_text.replace(pattern, repl)
                if t_nodes:
                    t_nodes[0].text = full_text
                    for tn in t_nodes[1:]:
                        tn.text = ''

    return etree.tostring(tree, xml_declaration=True, encoding='UTF-8', standalone=True)
```

### Step 6: 验证脱敏结果

```python
def verify_desensitized(docx_path, sensitive_terms):
    """检查脱敏后文件是否还有残留敏感词"""
    found = []
    with zipfile.ZipFile(docx_path, 'r') as z:
        for fname in z.namelist():
            if fname.endswith('.xml'):
                content = z.read(fname).decode('utf-8', errors='ignore')
                for term in sensitive_terms:
                    if term in content:
                        found.append((fname, term))
    return found  # 空列表 = 全部清除
```

**关键**：如果验证发现残留，分析原因（通常是跨run拆分或变体写法），补充替换规则后重新执行。

## 第三阶段：交付

### Step 7: 上传到Nextcloud
```bash
# 目标目录（按实际需求选择）
TARGET_DIR="/var/www/html/data/doro/files/Doro合同审查任务/任务交付/"
# 或
TARGET_DIR="/var/www/html/data/admin/files/小Maggie协作区/返工任务记录_YYYYMMDD/"

# 复制文件
docker cp /tmp/rework/脱敏后文件.docx nextcloud-aio-nextcloud:$TARGET_DIR

# 修正权限
docker exec nextcloud-aio-nextcloud chown www-data:www-data "$TARGET_DIR/脱敏后文件.docx"

# 扫描文件系统
docker exec -u www-data nextcloud-aio-nextcloud php occ files:scan <username>
```

### Step 8: 打包（如需要）
```bash
# 在容器内或本地打ZIP
cd /tmp/rework && zip -r 返工任务记录_脱敏版_YYYYMMDD.zip *.docx
```

### Step 9: 通知
- 企微群通知完成情况
- 说明脱敏了哪些文件、替换了哪些类别的信息

## ⚠️ Pitfalls

1. **不要用python-docx处理带修订标记的文件**——python-docx会丢失/破坏tracked changes
2. **文本跨run拆分是常见问题**——Word经常把一个词拆到多个`<w:r><w:t>`节点中，简单的逐节点替换会漏掉
3. **变体写法**——同一公司名可能出现简称、全称、甚至错别字版本，需要把所有变体都加入替换列表
4. **header/footer/comments也要处理**——不只是document.xml
5. **替换顺序**——长字符串先替换，避免短字符串先匹配导致长字符串被部分替换后无法匹配
6. **文件名不改**——遵循工作原则，不擅自修改文件名
7. **每次脱敏后必须验证**——运行verify脚本，确认零残留后才能交付
