返回

DLL 字符串提取与导入:简化软件本地化流程

python

DLL 文件字符串提取与导入

面对软件本地化工作时,从 DLL 文件中提取和导入字符串是关键环节。特别是在进行软件更新后,再次从DLL中导出文本,并与先前的文本比较工作,往往耗时耗力。本文介绍一些实用技巧。使用自动化脚本简化此流程。结合实例来更好地理解它们。

一、DLL 文件字符串资源提取方案

处理 DLL 文件的本地化问题时,主要需求是从中提取出可翻译的字符串。这些字符串可能存在于菜单、对话框等资源里。下面介绍几种方法:

方案1:利用 Resource Hacker 命令行提取字符串表

Resource Hacker 提供了一种通过命令行提取资源的简易方式。其基本思路是直接导出指定类型的资源。

  1. 提取命令

    "C:\Program Files (x86)\Resource Hacker\ResourceHacker.exe" -open "待处理的.dll" -save "资源导出目录.rc" -action extract -mask STRINGTABLE,,
    

    该命令可以将所有 STRINGTABLE 导出。其中 -mask 可以控制导出的资源种类,STRINGTABLE 即字符串表。这里也可以使用对话框或者菜单栏作为关键词,更换它导出需要的文件种类。但是,这些种类的名称不好记忆,使用 STRINGTABLE 是最快捷的方式。之后,需要自己删掉非文本数据即可。

  2. 操作步骤

    • 打开命令行窗口。
    • 执行命令,指定目标 DLL 文件路径、保存路径以及要提取的资源类型(这里是字符串表)。
    • 检查输出文件,验证字符串是否被成功提取。

这个方法适用于提取大量资源字符串。但是也有明显的缺点:导出格式不够方便后续处理。比如,这个方案产生大量 .bin 文件。因此,还需要继续操作提取 bin 文件内的字符,或者删除非 STRINGTABLE 文件,或者想办法把非 STRINGTABLE 的资源数据从总 rc 文件里去除掉。这不适合新手,会带来很大的操作负担。所以我们还要探索另一种方法。

方案2: 使用 PowerShell 脚本和 ResGen.exe 工具提取特定字符串

这个方法依赖于 .NET FrameworkResGen.exe。这要求使用者系统中必须装有开发工具 Visual Studio 或者对应版本的 Windows SDK。利用 PowerShell 调用 ResGen.exe,可以将 RC 文件转换为易于处理的 .resx.txt 文件。

  1. 使用 Resource Hacker 保存资源 :

    运行 Resource Hacker,打开 DLL 文件,手动导出指定菜单,保存成 rc 格式,需要注意保存编码为 unicode 格式。

  2. 执行脚本

    $rcFilePath = "目标文件.rc"
    $outputFilePath = "输出的字符串文件.txt"
    
    # 假定路径已经在系统变量内,或您记得具体位置。
    $resGenPath = "ResGen.exe"
    
    # RC 文件先转为 RESX 格式
    & $resGenPath $rcFilePath $outputFilePath.Replace(".txt", ".resx")
    
    # 再转为 TXT 格式。当然了,ResX 格式也是很方便继续处理的。只是我们要求是 txt,方便后面对比新旧版
    & $resGenPath /str:csharp, $outputFilePath.Replace(".txt", ".resx"), $outputFilePath
    
    Write-Host "字符串已提取至 $($outputFilePath)"
    
    
  3. 脚本解析

    • 定义变量存储输入文件和输出文件的路径。
    • 执行 ResGen.exe,将 .rc 转换到 .resx 文件,方便机器识别处理,然后再转为字符串文件。
    • 输出字符串文件的路径。

该方法可以准确提取字符串,并且结果更方便使用脚本做差异比较。如果需要使用其他编码(默认是 utf-16 格式),需要自己处理格式转换问题。

注意:这些操作都具有侵入性,建议对原始 DLL 文件做好备份。不要忘记操作安全准则,始终优先处理副本而不是原始数据

二、字符串对比及翻译处理

提取出字符串后,接下来的步骤是将其与已有的翻译版本对比。进而确定需要更新的字符串。并调用翻译服务。

改进版对比脚本 :

针对之前的 python 对比脚本的逻辑优化与改进如下:

import csv
import subprocess

def read_resource_file(file_path):
    """读取资源文件并返回字典,键为 ID,值为字符串。"""
    # 因为之前使用的方法对引号的处理问题可能造成解析错误。所以改用下面这段:
    result = {}
    with open(file_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            if line.startswith('BEGIN'):
                # 遇到 "BEGIN",向下查找直到 "END" 的块
                block_lines = []
                i += 1
                while i < len(lines) and not lines[i].strip().startswith('END'):
                    block_lines.append(lines[i].strip())
                    i += 1
                # 在这里处理找到的块
                for block_line in block_lines:
                  if len(block_line) < 2: # 遇到非标准格式内容
                    continue 
                  if block_line[0] == '"' and block_line[-1] == '"':
                      id_, value = block_line.split(',', 1)
                      # 这一行解决了原脚本的报错问题:对那些带有双引号的数据也能处理了
                      result[id_.strip().strip('"')] = value.strip().strip('"') 
            i += 1    
    return result

def translate_text_with_bing(text, target_language):
    """调用必应翻译 API 进行翻译。需要配置好客户端环境。"""
    # 此处使用示例命令,实际操作需根据具体 API 文档调整。
    # 参考:https://learn.microsoft.com/zh-cn/azure/ai-services/translator/reference/v3-0-reference
    # 要先确保安装了相应的开发环境,并获取认证参数
    try:
        result = subprocess.run([
            'curl', '-X', 'POST',
            'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=' + target_language,
            '-H', 'Ocp-Apim-Subscription-Key: <你的密钥>',
            '-H', 'Ocp-Apim-Subscription-Region: <你的区域>',
            '-H', 'Content-Type: application/json',
            '-d', '[{"Text": "' + text + '"}]'
        ], capture_output=True, text=True, check=True)
        # 解析返回的 JSON 数据,并获取翻译后的文本。
        return result.stdout
    except subprocess.CalledProcessError as e:
        print(f"翻译失败: {e}")
        return ""

def compare_and_update_strings(old_file, new_file, old_translated_file, target_language="zh"):
    """对比新旧版本的字符串,生成更新后的翻译文件。"""
    old_strings = read_resource_file(old_file)
    new_strings = read_resource_file(new_file)
    old_translated_strings = read_resource_file(old_translated_file)

    updated_content = ""
    log_entries = []
    
    with open(new_file, "r", encoding="utf-8") as f:
        # 对原文件内的字符也同样进行操作
        # 这样处理后,输出内容能保留原始 rc 文件的大部分信息,可以方便写回 rc
        content = f.read()

    for string_id, new_string in new_strings.items():
        old_string = old_strings.get(string_id)
        old_translation = old_translated_strings.get(string_id, "")

        if old_string == new_string and old_translation:
            # 旧版本存在且翻译内容存在则替换新字符串
            content = content.replace('"' + new_string + '"', '"' + old_translation + '"')
            updated_content += f'"{string_id}", "{old_translation}"\n'
        else:
            # 需要进行翻译的情况。旧字符串在新字符串文件中,需要重写此句。
            if old_string is not None and old_string in content:
                translated_text = translate_text_with_bing(new_string, target_language)
                content = content.replace('"' + old_string + '"', '"' + translated_text + '"')
                updated_content += f'"{string_id}", "{translated_text}"\n'
            elif new_string in content:
                translated_text = translate_text_with_bing(new_string, target_language)
                content = content.replace('"' + new_string + '"', '"' + translated_text + '"')
                updated_content += f'"{string_id}", "{translated_text}"\n'
            log_entries.append([string_id, new_string, old_string, old_translation])

    # 生成新的已翻译的字符串数据。
    with open("updated_translation.txt", "w", encoding="utf-8") as f:
        f.write(updated_content)

    # 生成新的带有翻译后的完整资源。
    with open("new_translated_file.rc", "w", encoding="utf-8") as f:
        f.write(content)

    # 生成差异比较日志
    with open("translation_log.csv", "w", encoding="utf-8", newline="") as log_file:
        csv_writer = csv.writer(log_file)
        csv_writer.writerow(["ID", "新字符串", "旧字符串", "旧翻译"])
        csv_writer.writerows(log_entries)

# 使用示例,直接修改成所需文件即可,不要把4个文件设置成相同的。
compare_and_update_strings("old_strings.txt", "new_strings.txt", "old_translated_strings.txt", "目标语言代码,如 zh-hans,日语 ja 等")

这个脚本做了如下优化:

  1. 读取并解析了新的文件格式。
  2. 调用 Bing 翻译 API 对比得到的新字符做初步翻译(如有需求)。当然了,要配置好自己的账号和区域才可以使用此功能。如果不用,请将调用翻译功能的几行删掉。
  3. 生成更新的翻译后字符串。
  4. 把字符串按格式重写了新文件内。保证了rc格式原样。
  5. 创建日志,详细记录每次处理的结果,是继续沿用,还是需要人工校对,都提供数据参考。
  6. 更详细的出错信息和安全操作,降低出 bug 的概率,让使用者自己排除代码故障。
  7. 改进了原脚本中的多处运行逻辑问题。

通过该脚本可以批量对比更新需要人工处理的内容,简化更新工作量。注意要根据机器翻译 API 的使用规则处理身份认证和请求频率。或者也可以改用其他已有的服务接口替换掉这里的。如果软件规模很庞大,一定要谨慎设置这些调用频率,防止账号受影响。