PAYATHON 2026

如何将 Delphi 的 ANSI .pas 文件批量转换为 UTF-8?

数据小许

可以使用外部脚本批量转换,无须在 Delphi IDE 中逐个保存。先确认原始 ANSI 代码页,按该代码页解码,再写成目标 Delphi 版本能够正确识别的 UTF-8 文件。

全部转换为 UTF-8 后,跨单元字符串异常或数据库乱码也不一定会消失。源码文件编码、运行时字符串类型和数据库连接字符集需要分别检查。仅凭现有描述,无法确定乱码发生在哪一步。

先区分源码编码和字符串类型

.pas 文件采用 UTF-8,表示编译器应如何读取磁盘上的源码;它不会让文件中所有 string 变量都变成 UTF8String。

对于通常启用长字符串的 Delphi 桌面项目:

  • Delphi 2009 及之后,默认 string 是采用 UTF-16 的 UnicodeString。
  • Delphi 2009 之前,默认长字符串是 AnsiString。
  • 显式声明的 AnsiString、ShortString、UTF8String,仍遵循各自的类型和转换规则。

这些差异由编译器版本和类型声明决定,与单元保存为 ANSI 还是 UTF-8 是两回事。参见 Embarcadero 的 Unicode 说明。

ANSI 单元和 UTF-8 单元可以共存,但编译器必须正确读取每个文件,传递字符串时的转换也必须正确。出现异常时,需要检查常量声明、接收变量、函数参数和数据库参数的实际类型。

确认原始代码页和目标格式

“ANSI”没有唯一对应的编码。不要仅凭 Windows 显示语言或编辑器的自动检测结果,就认定所有旧文件都使用同一个代码页。

先选一个包含已知立陶宛语文本、且尚未被改坏的文件:

  1. 确认它原来的代码页。下方脚本以已确认的 cp1257 为例,这不代表整个项目都使用该编码。
  2. 在 Delphi IDE 中将它另存为一份 UTF-8 样本。
  3. 用十六进制查看器检查样本文件开头,确认是否包含 EF BB BF。
  4. 将脚本输出与 IDE 样本比较,确认字符内容、换行和编译行为一致。

EF BB BF 是 UTF-8 BOM。Embarcadero 文档建议用 BOM 标识相应 Unicode 文件编码;当前项目应优先采用已由所用 Delphi 版本验证的保存格式。没有 BOM 本身不能证明文件不是 UTF-8。参见 BOM 说明。

如果文件已经被错误解码后保存,转为 UTF-8 通常只会保存乱码,应先从版本控制或备份恢复。

使用脚本批量生成 UTF-8 副本

下面的 Python 3 示例生成带 BOM 的 UTF-8 文件,保留目录结构和原有换行,不覆盖源文件。

运行前需确认:输入目录中所有没有 BOM 的 .pas 文件,都使用命令行指定的源编码。若项目混有无 BOM 的 UTF-8 文件或其他代码页,应先分组处理,不能直接对整个目录运行。

保存为 convert_pas.py:

from pathlib import Path
import codecs
import sys

if len(sys.argv) != 4:
    raise SystemExit(
        'Usage: python convert_pas.py SOURCE OUTPUT SOURCE_ENCODING'
    )

source = Path(sys.argv[1]).resolve()
output = Path(sys.argv[2]).resolve()
source_encoding = sys.argv[3]

if not source.is_dir():
    raise SystemExit('SOURCE must be an existing directory')
if output == source or source in output.parents:
    raise SystemExit('OUTPUT must be outside SOURCE')
if output.exists():
    raise SystemExit('OUTPUT must not already exist')

pending = []
for path in sorted(source.rglob('*')):
    if not path.is_file() or path.suffix.lower() != '.pas':
        continue

    data = path.read_bytes()
    if data.startswith(codecs.BOM_UTF8):
        text = data.decode('utf-8-sig', errors='strict')
    elif data.startswith((
        codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE,
        codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE,
    )):
        raise SystemExit(f'Handle UTF-16/UTF-32 separately: {path}')
    else:
        text = data.decode(source_encoding, errors='strict')

    pending.append((
        path.relative_to(source),
        text.encode('utf-8-sig', errors='strict'),
    ))

output.mkdir(parents=True)
for relative, data in pending:
    destination = output / relative
    destination.parent.mkdir(parents=True, exist_ok=True)
    destination.write_bytes(data)

print(f'Wrote {len(pending)} .pas files to {output}')

例如,源编码已经确认为 cp1257 时:

python convert_pas.py "D:\project\src" "D:\project-utf8-pas" cp1257

脚本会先解码全部文件,再写入输出目录。严格解码能发现部分编码错误,但没有报错也不能证明源代码页选对了,仍需核对已知文本。

输出目录只包含 .pas 副本。检查后,按相对路径将它们复制到项目的测试副本中,再进行构建。不要直接把该目录当作完整项目使用。

排查跨单元字符串异常

用一个包含完整立陶宛语字符的样本,例如 ąčęėįšųūž ĄČĘĖĮŠŲŪŽ,逐步比较:

  1. 源码中的字符是否正确。
  2. 常量赋给变量后,字符是否仍正确。
  3. 传入另一个单元的函数后,是否发生变化。
  4. 绑定数据库参数前,值是否已经损坏。

在 Delphi 2009 及之后,尤其要检查 UnicodeString 到 AnsiString 的赋值、PAnsiChar 转换和手动编码操作。UTF8String 是带 UTF-8 代码页的字符串类型;源码改成 UTF-8 后,不需要把所有 string 都替换为它。参见 Delphi 字符串类型文档。

同时检查已有的 {$CODEPAGE ...} 指令、编译器代码页设置,以及包含字符串的 .inc 文件。批量转换脚本不会修改这些设置。

单独检查 IBX 与 Firebird 的字符集

Firebird 数据库使用 UTF-8,并不代表客户端连接也使用 UTF-8,或传给 IBX 的字节已经是 UTF-8。Firebird 会根据连接字符集与字段字符集进行转换;使用 NONE 时,应用需要自行负责字节的正确解释。参见 Firebird 的客户端字符集说明。

对于使用 TIBDatabase.Params 配置连接的 IBX 版本,字符集参数名是 lc_ctype。如果已确认该版本能够正确处理 UTF-8 连接和参数,可以在建立连接前设置:

IBDatabase1.Params.Values['lc_ctype'] := 'UTF8';

该参数的接口依据见 TIBDatabase.Params 文档。具体的 Firebird 兼容性及字符串参数处理,仍取决于所用 IBX 版本。

不要只修改这个参数就认为问题已经解决。例如,若组件实际发送的是旧代码页字节,却声明连接为 UTF-8,服务器仍会收到错误内容。反过来,若组件已经完成转换,应用再手动执行一次 UTF-8 编码,也可能造成重复转换。

在测试数据库中,用参数化语句写入样本文本,提交后通过独立客户端和应用分别读回,比较完整字符。同时检查目标字段及文本 BLOB 的字符集,不能只看数据库默认字符集。

转换完成后验证什么

先保留可回退的版本,再将编码转换作为独立变更提交。检查差异时,字符内容应保持一致,避免同时引入业务修改。

随后执行完整重新构建,检查字符串转换警告,并验证跨单元传参、界面显示及数据库写入读回。.dpr、.inc 等相关文本文件应另行确认;二进制 .dfm 不能按普通文本批量转码。

统一源码编码可以减少编辑工具之间的分歧。如果转换后仍有乱码,应沿着“源码读取 → 运行时字符串 → IBX 参数 → Firebird 字段”的路径,找出首次发生变化的位置。

备注:内容仅供参考。