はじめに (対象読者・この記事でわかること)

この記事は、Pythonを使ってWindowsアプリケーションのバージョン情報を取得したい開発者の方を対象にしています。 特に、exeファイルからバージョン情報を取得する際に、想定外の文字列が混在してしまう問題に悩んでいる方に最適です。

この記事を読むことで、Windows APIを使った正確なバージョン情報の取得方法、不要な文字列の除去テクニック、そして実用的な実装例を習得できます。実際の業務でソフトウェアのバージョン管理を自動化したい方や、デプロイメントツールを開発している方に役立つ内容です。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Pythonの基本的な文法と標準ライブラリの使用方法 - Windowsのファイルプロパティとバージョン情報の基本概念

Pythonでexeバージョン情報取得の課題と解決策

Windows環境では、exeファイルはリソースセクションにバージョン情報を格納しています。この情報はプログラムの識別、アップデート確認、互換性チェックなどで重要な役割を果たします。しかし、Pythonの標準ライブラリだけでこの情報を取得しようとすると、予期せぬ文字列が混在してしまうことがあります。

例えば、「1.2.3.4」というはずのバージョンが、「1.2.3.4\x00\x00\x00」のようにnull文字が付与されていたり、環境によっては「1.2.3.4@#$」のような特殊文字が混入してしまうことがあります。これは、Windows APIが文字列を格納する方法と、Pythonがそれを解釈する方法の違いに起因しています。

具体的な実装と不要文字列の除去方法

ここでは、Windows APIを直接使用して、クリーンなバージョン情報を取得する方法を詳しく解説します。

基本的な実装方法

まず、ctypesを使用してWindows APIにアクセスする基本的な方法から始めます。

Python
import ctypes from ctypes import wintypes import os # Windows APIの定数 VS_FIXEDFILEINFO = 0x00000000 VFT_APP = 0x00000001 # 必要なDLLの読み込み version_dll = ctypes.windll.version kernel32 = ctypes.windll.kernel32 def get_file_version_info(filename): """exeファイルからバージョン情報を取得する""" # ファイルサイズの取得 size = version_dll.GetFileVersionInfoSizeW(filename, None) if size == 0: raise Exception(f"バージョン情報のサイズ取得に失敗: {filename}") # バッファの準備 buffer = ctypes.create_string_buffer(size) # バージョン情報の取得 if not version_dll.GetFileVersionInfoW(filename, 0, size, buffer): raise Exception(f"バージョン情報の取得に失敗: {filename}") # バージョン情報の解析 u_len = wintypes.UINT() lplpBuffer = ctypes.POINTER(ctypes.c_void_p)() if not version_dll.VerQueryValueW(buffer, '\\', ctypes.byref(lplpBuffer), ctypes.byref(u_len)): raise Exception("バージョン値のクエリに失敗") # VS_FIXEDFILEINFO構造体の取得 ffi = ctypes.cast(lplpBuffer, ctypes.POINTER(wintypes.VS_FIXEDFILEINFO)).contents # メジャー・マイナーバージョンの構築 major = ffi.dwFileVersionMS >> 16 minor = ffi.dwFileVersionMS & 0xFFFF build = ffi.dwFileVersionLS >> 16 revision = ffi.dwFileVersionLS & 0xFFFF return f"{major}.{minor}.{build}.{revision}"

文字列テーブルからの正確な取得

上記の方法では数値バージョンしか取得できません。製品バージョンやファイルバージョンの文字列を取得するには、文字列テーブルを解析する必要があります。

Python
def get_version_string(filename, version_type="ProductVersion"): """文字列テーブルからバージョン文字列を取得する""" size = version_dll.GetFileVersionInfoSizeW(filename, None) if size == 0: return None buffer = ctypes.create_string_buffer(size) if not version_dll.GetFileVersionInfoW(filename, 0, size, buffer): return None # 言語とコードページの取得 lplpBuffer = ctypes.POINTER(ctypes.c_void_p)() u_len = wintypes.UINT() if not version_dll.VerQueryValueW(buffer, '\\VarFileInfo\\Translation', ctypes.byref(lplpBuffer), ctypes.byref(u_len)): return None # 最初の言語IDとコードページを使用 lang_id = ctypes.cast(lplpBuffer, ctypes.POINTER(wintypes.DWORD)).contents.value lang_code = f"{lang_id & 0xFFFF:04x}{(lang_id >> 16) & 0xFFFF:04x}" # バージョン文字列の取得 sub_block = f"\\StringFileInfo\\{lang_code}\\{version_type}" if not version_dll.VerQueryValueW(buffer, sub_block, ctypes.byref(lplpBuffer), ctypes.byref(u_len)): return None # 文字列の抽出とクリーニング if u_len.value > 0: # 文字列を取得 version_str = ctypes.wstring_at(lplpBuffer, u_len.value - 1) # 不要な文字を除去 # null文字、改行、タブ、特殊な空白文字を削除 cleaned_version = ''.join(c for c in version_str if c.isprintable() and c not in ['\x00', '\r', '\n', '\t']) # 前後の空白も削除 cleaned_version = cleaned_version.strip() return cleaned_version return None

ハマった点やエラー解決

実装中に遭遇する代表的な問題とその解決策を紹介します。

問題1: UnicodeDecodeError

UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte

これは、バージョン情報がUnicode形式で格納されているにも関わらず、誤ってバイト列として解釈しようとした場合に発生します。

解決策:

Python
# 誤った方法 # version_bytes = ctypes.string_at(lplpBuffer, u_len.value) # version_str = version_bytes.decode('utf-8') # エラーが発生 # 正しい方法 version_str = ctypes.wstring_at(lplpBuffer, u_len.value - 1)

問題2: 不要な文字列の混入 バージョン文字列に「\x00」や特殊な空白文字(\xa0など)が混入することがあります。

解決策:

Python
import re def clean_version_string(version_str): """バージョン文字列から不要な文字を除去する""" if not version_str: return "" # null文字と制御文字を除去 cleaned = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', version_str) # 連続する空白を単一のスペースに cleaned = re.sub(r'\s+', ' ', cleaned) # 前後の空白と特殊文字を除去 cleaned = cleaned.strip() # バージョン文字列として不適切な文字を除去 # 英数字とドット以外の文字を削除 cleaned = re.sub(r'[^a-zA-Z0-9.-]', '', cleaned) return cleaned

問題3: 言語コードの不一致 異なる言語環境のWindowsで動作させた際に、バージョン情報が取得できないことがあります。

解決策:

Python
def get_version_with_fallback(filename, version_type="ProductVersion"): """フォールバック機能付きバージョン取得""" # 使用可能な言語コードを試す language_codes = [ "040904b0", # 英語(米国) "041104b0", # 日本語 "040704b0", # ドイツ語 "040c04b0", # フランス語 "040804b0", # スペイン語 ] size = version_dll.GetFileVersionInfoSizeW(filename, None) if size == 0: return None buffer = ctypes.create_string_buffer(size) if not version_dll.GetFileVersionInfoW(filename, 0, size, buffer): return None for lang_code in language_codes: sub_block = f"\\StringFileInfo\\{lang_code}\\{version_type}" lplpBuffer = ctypes.POINTER(ctypes.c_void_p)() u_len = wintypes.UINT() if version_dll.VerQueryValueW(buffer, sub_block, ctypes.byref(lplpBuffer), ctypes.byref(u_len)): if u_len.value > 0: version_str = ctypes.wstring_at(lplpBuffer, u_len.value - 1) cleaned = clean_version_string(version_str) if cleaned: # 空文字列でない場合 return cleaned return None

実用的な統合関数

最後に、これまでの機能を統合した実用的な関数を提供します。

Python
def get_exe_version_info(filename): """ exeファイルから包括的なバージョン情報を取得する Returns: dict: { 'file_version': '1.2.3.4', 'product_version': '1.2.3.4', 'company_name': 'Example Corp', 'product_name': 'Example App' } """ if not os.path.exists(filename): raise FileNotFoundError(f"ファイルが見つかりません: {filename}") result = {} # ファイルバージョン(数値) try: file_ver = get_file_version_info(filename) result['file_version_numeric'] = file_ver except: result['file_version_numeric'] = None # 各種バージョン文字列 version_types = ['FileVersion', 'ProductVersion', 'CompanyName', 'ProductName'] for ver_type in version_types: clean_key = ver_type.lower().replace('version', '_version').replace('name', '_name') version_str = get_version_with_fallback(filename, ver_type) result[clean_key] = version_str return result # 使用例 if __name__ == "__main__": exe_path = r"C:\Windows\notepad.exe" try: version_info = get_exe_version_info(exe_path) print(f"ファイル: {exe_path}") print(f"ファイルバージョン: {version_info.get('file_version')}") print(f"製品バージョン: {version_info.get('product_version')}") print(f"会社名: {version_info.get('company_name')}") print(f"製品名: {version_info.get('product_name')}") except Exception as e: print(f"エラー: {e}")

まとめ

本記事では、PythonでWindowsのexeファイルからバージョン情報を取得する際の不要な文字列の除去方法を解説しました。

  • Windows APIを直接使用することで、正確なバージョン情報へアクセスできる
  • 文字列テーブルから取得した情報は、適切にクリーニングする必要がある
  • 言語環境の違いに対応するため、フォールバック機能を実装することが重要

この記事を通して、信頼性の高いバージョン情報取得ツールを実装できるようになりました。今後は、取得したバージョン情報を使った自動アップデートチェックや、複数ファイルの一括バージョン管理についても記事にする予定です。

参考資料