3. 文本处理基础:字符串操作、正则表达式入门、文件读写(TXT/PDF/DOCX)

在构建采购合同版本比对系统之前,必须掌握文本处理的三项核心能力:字符串操作(清洗与分割)、正则表达式(模式匹配与提取)以及多格式文件读写(TXT/PDF/DOCX)。本章将围绕这三部分展开,为后续的差异检测算法奠定数据预处理基础。

3.1 字符串操作:清洗与结构化

合同文本通常包含多余空格、换行符、制表符以及不一致的标点符号。Python 内置的字符串方法足以完成大部分清洗工作。

方法 作用 示例
strip() 去除首尾空白字符 " 合同条款 ".strip() → "合同条款"
split() 按分隔符拆分为列表 "第1条;第2条".split(";") → ["第1条","第2条"]
replace() 替换指定子串 "甲方:".replace(":",":") → "甲方:"
join() 将列表合并为字符串 " ".join(["条款A","条款B"]) → "条款A 条款B"
实战要点: 合同比对前,统一将全角字符转为半角(如全角逗号“,”→半角“,”),避免因编码差异导致误判。
# 全角转半角示例
def full_to_half(text):
    result = []
    for char in text:
        code = ord(char)
        if 0xFF01 <= code <= 0xFF5E:
            result.append(chr(code - 0xFEE0))
        elif code == 0x3000:
            result.append(' ')
        else:
            result.append(char)
    return ''.join(result)

sample = "采购合同(2024版)"
print(full_to_half(sample))  # 输出: 采购合同(2024版)

3.2 正则表达式入门:模式匹配与提取

正则表达式是合同文本中提取编号、金额、日期、条款号等结构化信息的最佳工具。Python 的 re 模块提供了完整的支持。

3.2.1 常用元字符与量词

元字符 含义 匹配示例
\d 数字 \d{4} 匹配 4 位数字
\w 字母、数字、下划线 \w+ 匹配一个单词
. 除换行符外的任意字符 第.条 匹配“第1条”“第A条”
* 前一个字符出现 0 次或多次 ab*c 匹配 ac、abc、abbc
+ 前一个字符出现 1 次或多次 ab+c 匹配 abc、abbc
? 前一个字符出现 0 次或 1 次 colou?r 匹配 color、colour

3.2.2 核心函数

  • re.findall(pattern, text):返回所有匹配的子串列表。
  • re.search(pattern, text):返回第一个匹配对象,可用 .group() 提取。
  • re.sub(pattern, repl, text):替换匹配到的内容。
import re

text = "合同总金额:¥1,250,000.00,签订日期:2024-03-15"

# 提取金额(含千分位和小数)
amount_pattern = r'¥[\d,]+\.\d{2}'
amounts = re.findall(amount_pattern, text)
print(amounts)  # ['¥1,250,000.00']

# 提取日期
date_pattern = r'\d{4}-\d{2}-\d{2}'
dates = re.findall(date_pattern, text)
print(dates)  # ['2024-03-15']
提示: 在正则前加 r(原始字符串)可以避免反斜杠转义问题,例如 r'\d+' 比 '\\d+' 更清晰。
正则表达式匹配流程 原始合同文本 正则模式 r'\d{4}-\d{2}-\d{2}' re.findall() 引擎 ['2024-03-15'] 说明:正则引擎从左到右扫描文本,将匹配到的子串以列表形式返回。 若使用 re.search(),则只返回第一个匹配对象。

3.3 文件读写:TXT / PDF / DOCX

采购合同通常以 PDF 或 DOCX 格式交付,TXT 则常用于中间处理。Python 生态提供了成熟的第三方库来应对每种格式。

3.3.1 TXT 文件读写

纯文本文件读写使用内置 open() 函数,指定编码为 utf-8 以避免乱码。

# 读取 TXT
with open('contract.txt', 'r', encoding='utf-8') as f:
    content = f.read()

# 写入 TXT
with open('output.txt', 'w', encoding='utf-8') as f:
    f.write(content)

3.3.2 PDF 文件读取

推荐使用 pdfplumber 库,它比 PyPDF2 更擅长提取表格和保留文本顺序。

import pdfplumber

with pdfplumber.open('contract.pdf') as pdf:
    full_text = []
    for page in pdf.pages:
        full_text.append(page.extract_text())
    text = '\n'.join(full_text)
注意: PDF 中的扫描件(图片型 PDF)无法直接提取文本,需要先经过 OCR(如 Tesseract)处理。本课程后续章节会专门讲解 OCR 方案。

3.3.3 DOCX 文件读写

使用 python-docx 库,可以逐段落读取 Word 文档,保留段落结构。

from docx import Document

doc = Document('contract.docx')
paragraphs = [p.text for p in doc.paragraphs if p.text.strip()]
full_text = '\n'.join(paragraphs)

3.3.4 格式对比一览

格式 推荐库 安装命令 适用场景
TXT 内置 open() 无需安装 中间处理、日志、纯文本合同
PDF pdfplumber pip install pdfplumber 正式签署版合同、扫描件除外
DOCX python-docx pip install python-docx 可编辑版合同、修订稿
多格式文件读取架构 TXT 文件 open() 内置函数 PDF 文件 pdfplumber 库 DOCX 文件 python-docx 库 统一字符串对象 无论输入格式如何,最终都输出为 Python 字符串,供后续比对模块使用。

3.4 综合案例:合同文本预处理流水线

将本章知识点串联成一个完整的预处理函数,用于清洗并提取合同中的关键字段。

import re
import pdfplumber
from docx import Document

def preprocess_contract(file_path):
    # 1. 根据扩展名读取文件
    if file_path.endswith('.txt'):
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
    elif file_path.endswith('.pdf'):
        with pdfplumber.open(file_path) as pdf:
            text = '\n'.join([p.extract_text() for p in pdf.pages])
    elif file_path.endswith('.docx'):
        doc = Document(file_path)
        text = '\n'.join([p.text for p in doc.paragraphs if p.text.strip()])
    else:
        raise ValueError("不支持的文件格式")

    # 2. 全角转半角
    text = full_to_half(text)

    # 3. 去除多余空白
    text = re.sub(r'\s+', ' ', text).strip()

    # 4. 提取关键信息
    info = {}
    date_match = re.search(r'\d{4}-\d{2}-\d{2}', text)
    if date_match:
        info['date'] = date_match.group()

    amount_match = re.search(r'¥[\d,]+\.\d{2}', text)
    if amount_match:
        info['amount'] = amount_match.group()

    return text, info