MENU

問い合わせ


    【CAD図面PDFをExcelへ自動転記するツール開発 第11回(最終回)】PyInstallerでWindows向けexeにまとめて配布し、精度の限界と確認のルールを整理する

    前回(第10回)では、図面を社外に出さない「ローカル完結モード」と「外部AIモード」を設定画面で切り替え、APIキーをOSの資格情報ストアに保存できるようにしました。

    最終回の第11回は、Pythonで作ったツールをPyInstallerでexe化し、Pythonを入れていない担当者のPCへ配布するための仕上げと、連載全体のまとめです。ポイントは次の3つです。

    • 同梱ファイル(部屋名辞書など)の場所を、exeでもソース実行でも同じ関数で解決する
    • Tesseract OCR本体はexeに含めず「インストールしてもらう」前提にし、場所を設定画面で指定できるようにする
    • exeを配って終わりにせず、精度の限界と最終確認のルールをセットで渡す
    目次

    このリポジトリで確認できる範囲

    PyInstallerはクロスコンパイル(別のOS向けの実行ファイルを作ること)ができません。公式のREADMEにも「Windowsのアプリを作るにはWindowsで動かす」とあります。開発環境はLinuxのため、確認できたのはLinux向けにビルドした実行ファイルの起動と動作までです。

    📰 出典:PyInstaller README(v6.19.0)

    仕組みの説明:exeは起動のたびに一時フォルダへ展開される

    PyInstallerは、スクリプトとライブラリ・Python本体を1つのフォルダ(onedir)か1つの実行ファイル(onefile)にまとめます。今回は配りやすいonefileです。onefileのexeは、起動のたびに中身を一時フォルダ(_MEIxxxxxx)へ展開してから動きます。公式ドキュメントには、そのぶん起動が少し遅いこと、強制終了すると一時フォルダが残ること、問題を調べるならonedirの方が簡単なことが書かれています。

    📰 出典:PyInstaller「What PyInstaller Does and How It Does It」(v6.19.0)

    そこで問題になるのが、app/config/rooms.yaml(部屋名辞書)のような同梱ファイルの場所です。exeでは展開先が sys._MEIPASS に入り、sys.frozen も設定されます。「exeなら _MEIPASS、ソース実行ならプロジェクトのフォルダ」を返す関数を1か所に用意しました。

    📰 出典:PyInstaller「Run-time Information」(v6.19.0)

    実装

    ファイル構成(第11回時点の差分)

    app/
    ├ __init__.py            … 変更:版(__version__ = "1.0.0")。画面タイトル・exeのバージョン情報に使う
    ├ main.py                … 変更:python app/main.py で直接起動できるよう検索パスを修正
    ├ core/
    │  ├ resources.py        … 新規:同梱ファイルの場所(_MEIPASS)とTesseractの場所
    │  ├ ocr_local.py        … 変更:Tesseractのフルパスを設定できるように
    │  ├ settings.py         … 変更:設定に tesseract_path を追加
    │  ├ room_matcher.py / mapping.py … 変更:YAMLの場所を resources.py で解決
    └ ui/
       ├ settings_dialog.py  … 変更:「ローカルOCR(Tesseractの場所)」欄
       └ main_window.py      … 変更:起動時・設定変更時にTesseractを探して設定、タイトルに版
    build/madori_ocr.spec    … 新規:PyInstallerのビルド設定
    tools/make_icon.py / make_release.py / capture_frozen.py … 新規:アイコン・配布物の作成、起動確認
    tests/test_resources.py  … 新規25件(test_settings.py にも5件追加)
    requirements-dev.txt     … 新規:pyinstaller==6.19.0 ほか

    同梱ファイルの場所を1つの関数で決める(app/core/resources.py)

    # app/core/resources.py(抜粋)
    PROJECT_ROOT = Path(__file__).resolve().parents[2]
    ROOMS_YAML = ("app", "config", "rooms.yaml")
    
    
    def is_frozen(frozen: bool | None = None, meipass: str | None = None) -> bool:
        """PyInstallerでまとめたexeとして動いているか(`sys.frozen` と `sys._MEIPASS` の両方があるか)。"""
        is_bundled = getattr(sys, "frozen", False) if frozen is None else frozen
        bundle = getattr(sys, "_MEIPASS", None) if meipass is None else meipass
        return bool(is_bundled) and bool(bundle)
    
    
    def bundle_dir(frozen: bool | None = None, meipass: str | None = None) -> Path:
        if is_frozen(frozen, meipass):
            bundle = getattr(sys, "_MEIPASS", "") if meipass is None else meipass
            return Path(bundle)
        return PROJECT_ROOT
    
    
    def resource_path(*parts: str, frozen: bool | None = None, meipass: str | None = None) -> Path:
        if not parts:
            raise ValueError("ファイル名を指定してください。")
        for part in parts:
            if not part or Path(part).is_absolute() or ".." in Path(part).parts:
                raise ValueError(f"同梱ファイルの指定が不正です: {part!r}")
        return bundle_dir(frozen, meipass).joinpath(*parts)

    引数で差し替えられるので、exe化しなくてもpytestで「exeとして動いている場合」を確かめられます。なお一時フォルダは終了時に消えるため、利用者が編集するファイルや出力を展開先に置いてはいけません(設定は第10回から %APPDATA%\MadoriOCR に保存しています)。

    Tesseractは「インストールしてもらう」前提にし、場所を指定できるようにする

    Tesseract OCR本体と日本語データはPythonのパッケージではないため、PyInstallerは同梱しません。埋め込み文字のある図面はTesseractなしで読めること、同梱するとTesseractの更新のたびにexeを配り直すことになることから、利用者のPCにインストールしてもらう前提にしました。ただ、インストールしてもPATH(コマンドを探す場所の一覧)に入らないことがあるため、次の順に探して pytesseract に設定します。

    # app/core/resources.py(抜粋)
    def find_tesseract(configured: str = "", *, platform=None, environ=None, app_directory=None,
                       which=shutil.which, is_file=Path.is_file) -> TesseractLocation:
        exe = tesseract_exe_name(platform)          # Windowsは tesseract.exe
        value = validate_tesseract_path(configured)
        if value:                                   # 1. 設定画面で指定した場所(無ければほかは探さない)
            candidate = Path(value)
            for path in (candidate, candidate / exe):
                if is_file(path):
                    return TesseractLocation(path, TESSERACT_FROM_SETTINGS)
            return TesseractLocation(None, None, missing_configured=value)
    
        base = app_dir() if app_directory is None else app_directory
        bundled = base / APP_DIR_TESSERACT_FOLDER / exe   # 2. exeと同じフォルダの tesseract\
        if is_file(bundled):
            return TesseractLocation(bundled, TESSERACT_FROM_APP_DIR)
    
        found = which("tesseract")                  # 3. PATH
        if found:
            return TesseractLocation(Path(found), TESSERACT_FROM_PATH)
    
        for candidate in standard_tesseract_paths(platform, environ):  # 4. C:\Program Files\Tesseract-OCR など
            if is_file(candidate):
                return TesseractLocation(candidate, TESSERACT_FROM_STANDARD)
        return TesseractLocation(None, None)
    # app/core/ocr_local.py(抜粋)
    def configure_tesseract(path: str | Path | None) -> None:
        pytesseract.pytesseract.tesseract_cmd = str(path) if path else DEFAULT_TESSERACT_CMD

    設定した場所に無いときは、PATH上の別のTesseractを黙って使わず「見つかりません」と表示します(精度の違いの原因を追えなくなるため)。Tesseractが無いPCで埋め込み文字の無いページを読んだときも、「0件」だけでなく理由と設定方法を表示します。Windows用インストーラーは、Tesseractの公式ドキュメントでUB Mannheim(マンハイム大学図書館)作成のものが案内されています。

    📰 出典:Tesseract ドキュメント「Installation」

    ビルド設定(build/madori_ocr.spec)

    specファイルはPythonとして実行される設定ファイルです。

    # build/madori_ocr.spec(抜粋)
    ROOT = Path(SPECPATH).resolve().parent
    APP_NAME = "MadoriOCR"
    
    # 版は app/__init__.py の __version__ だけを正とし、画面のタイトルとexeのバージョン情報をそろえる
    _match = re.search(r'^__version__ = "(\d+)\.(\d+)\.(\d+)"', (ROOT / "app" / "__init__.py").read_text(encoding="utf-8"), re.M)
    VERSION = tuple(int(n) for n in _match.groups()) + (0,)
    
    datas = [
        (str(ROOT / "app" / "config" / "rooms.yaml"), "app/config"),
        (str(ROOT / "app" / "config" / "mapping.example.yaml"), "app/config"),
    ]
    
    a = Analysis(
        [str(ROOT / "app" / "main.py")],
        pathex=[str(ROOT)],
        datas=datas,
        excludes=["pytest", "ruff", "_pytest"],
    )
    pyz = PYZ(a.pure)
    
    version_info = make_version_info() if sys.platform == "win32" else None
    
    exe = EXE(
        pyz, a.scripts, a.binaries, a.datas, [],
        name=APP_NAME,
        upx=False,          # 実行ファイルの圧縮(UPX)は使わない
        console=False,      # --windowed 相当:黒いコンソール画面を出さない
        icon=[str(icon_file)] if icon_file.is_file() else None,
        version=version_info,
    )
    • console=False:--windowed と同じ。コンソール画面が出ない代わりに sys.stdout・sys.stderr が None になる、と公式ドキュメントで注意されています(Linuxでは無視されます)
    • upx=False:UPXで圧縮するとDLLが壊れることがある、と公式ドキュメントにあるため使いません
    • バージョン情報:エクスプローラーのプロパティに出る版を app/__init__.py から作ります。作成に使う部品がWindowsでだけ入る pefile を読み込むため、Linuxでは ModuleNotFoundError で止まりました。Windowsのときだけ作るようにしています

    📰 出典:PyInstaller「Using PyInstaller」(v6.19.0)

    📰 出典:PyInstaller「Common Issues and Pitfalls」(v6.19.0)

    keyring(第10回)の資格情報マネージャー用の部品は自動解析では見つかりませんが、PyInstaller 6.19.0同梱の設定(フック)が集めることをソースで確認しました。

    Windowsでのビルド手順

    REM Windows(コマンドプロンプト。プロジェクトのルートで)
    py -3.13 -m venv .venv-build
    .venv-build\Scripts\activate
    pip install -r requirements-dev.txt
    python tools\make_icon.py
    pyinstaller --noconfirm --clean build\madori_ocr.spec
    python tools\make_release.py
    # requirements-dev.txt(抜粋)
    -r requirements.txt
    pyinstaller==6.19.0                  # 2026-02-14公開
    pyinstaller-hooks-contrib==2026.1    # 2026-02-18公開。各ライブラリ用の設定集

    tools\make_release.py は、exe・利用者向けの説明・転記の設定ファイルの例・サンプル・ライブラリのライセンス文・exeのハッシュ値(SHA-256)をまとめた配布フォルダとzipを作ります。

    動作確認の方法

    pytest は382件すべて成功しました(第10回までの352件に30件追加)。ruff check . もエラーはありません。_MEIPASS の有無、Tesseractを探す順番、第10回の版で保存した設定ファイルがそのまま読めることなどを確かめています。Linuxでは同じspecでビルドしました(Python 3.12)。

    確認したこと結果(Linux・4vCPU)
    ビルド約39秒、1ファイル約58MB
    同梱ファイル展開先に rooms.yaml などがあり、終了後に一時フォルダが消える
    起動Xvfb(仮想ディスプレイ)上で3回起動し、ウィンドウ表示まで1.6〜1.7秒
    コア処理同じ設定の確認用ビルドで第9回の架空サンプル8件を一括処理し、ソース実行と同じ結果

    次は、ビルドしたLinux版の「設定…」画面です(開発環境(Linux)での確認画面です。Windows実機では見た目が異なります)。

    ビルドしたLinux版から開いた設定画面。一番下の「ローカルOCR」欄に「使用中:/usr/bin/tesseract(環境変数PATH)」と表示されている

    なお、これまで案内していた python app/main.py が検索パスの都合で失敗することが分かり、修正しました(python -m app.main も使えます)。

    確認できていないこと

    • Windows実機でのビルド・起動時間・アイコンとバージョン情報の表示、Microsoft Defender等の反応
    • Windows版Tesseractの自動検出、exeと同じフォルダに置いた場合の日本語データの読み込み
    • Python 3.13系での動作(検証は3.11/3.12)

    つまずきやすい点・配布時の注意

    • 初回起動が遅い:展開のぶん待ち時間があり、ウイルス対策ソフトの検査でさらに延びることがあります。説明書に「十数秒かかることがある」と書いておくと二重起動を防げます
    • 誤検知が起こりうる:一般に、署名の無い自作のexeはウイルス対策ソフトに疑われたり、Windowsの警告が出たりすることがあると言われます。誤検知はMicrosoftの提出窓口から報告できます。社内配布なら情シスに除外設定や配布方法を相談するのが確実です
    • コード署名:署名すると「誰が作ったか」「改ざんされていないか」を確かめられるようになります。警告が必ず消えるわけではありませんが、社外へ配るなら検討の価値があります(証明書には費用と審査が必要)
    • 更新版の配り方:版を app/__init__.py で上げると画面のタイトルに出ます。設定は %APPDATA% にあるのでexeを差し替えても残ります。版ごとのフォルダで配り、古い版も残しておくと戻せます

    📰 出典:Microsoft「Submit a file for malware analysis」

    ライセンス:社内で使うか、社外へ配るかで考え方が変わる

    対象ライセンス配布時の考え方
    PyMuPDFAGPL-3.0 またはArtifex社の商用ライセンスexeを社外へ配布・販売する場合は、AGPLの条件(ソースコードの提供など)を満たすか、商用ライセンスを検討
    TesseractApache-2.0同梱する場合はライセンス文を配布物に含める
    PyInstallerGPL-2.0以降+例外作ったアプリは自由なライセンスで配布できる

    PyMuPDFの公式ドキュメントでは、AGPLの条件を満たせない場合はArtifex社へ商用ライセンスを問い合わせるよう案内されています。社内だけで使うか、取引先へ配る・販売するかで必要な対応が変わります。個別の判断は弁護士などの専門家に確認してください。

    📰 出典:PyMuPDF「About」(1.26.7)

    📰 出典:Tesseract LICENSE

    連載のまとめ:完成したツールでできること・できないこと

    回内容
    第0回要件整理とPython開発環境、tkinterの最初の画面
    第1回PDFの図面ページを画面にプレビュー表示する
    第2回図面に埋め込まれた文字を抽出し、ハイライトする
    第3回部屋名らしき文字列を絞り込むルール
    第4回スキャン図面向けのローカルOCR
    第5回外部AIの画像解析で精度を底上げする
    第6回検出結果を人が確認・修正する画面
    第7回指定のExcelフォーマットの決まった欄へ転記する
    第8回対応付けを設定ファイルで変えられるようにする
    第9回複数のPDFをまとめて処理する
    第10回ローカル完結モードと外部AIモードの切り替え
    第11回exe化と配布、運用ルールの整理(この記事)

    できること:埋め込み文字・ローカルOCR・(許可時のみ)外部AIでの部屋名の読み取り、人による確認・修正、書式を残したExcelへの転記、様式ごとの対応付けの切り替え、フォルダ単位の一括処理、exeでの配布。

    できないこと:手書き・縦書き・かすれの強い図面の安定した読み取り、部屋名以外(階・面積)の転記、欄が足りない場合の行追加、外部AIでの一括処理。実在の図面での精度は測っておらず、Windows実機での確認も残っています。

    精度の限界と、最終確認の運用ルール

    OCRも外部AIも誤読をゼロにはできません(第9回の低解像度の架空スキャンでも7部屋中1部屋を取りこぼしました)。「誤りは起きる」前提で運用を決めておきます。

    • 転記前に、確認画面で図面と見比べて1件ずつ確認する
    • 一括処理の「成功」は機械的な確認に通っただけ。出力したExcelを開いて確認する
    • 「要確認」の件数と理由を記録し、辞書(rooms.yaml)の追加や運用の見直しに使う
    • 誤転記が見つかったときの連絡先と、直し方(再実行か手修正か)を決めておく

    発注者向けメモ

    exeと一緒に「精度の限界」と「最終確認の運用ルール」を納品してもらう

    exeだけでは「どの図面なら読めるか」「誰がどこまで確認するか」が決まりません。曖昧なままだと、誤転記のとき「不具合か運用の漏れか」で揉めがちです。次をセットで納品してもらうのがおすすめです。

    • 自社の実際の図面で測った、読み取れた割合と「要確認」の割合
    • 読めない図面の例と、そのときの手順
    • 最終確認のルール(上の4項目のような内容)
    • 配布手順(Tesseract・初回起動・更新版の差し替え)とライセンスの一覧

    内製するか、外注するかの判断ポイント

    観点内製が向く外注が向く
    様式・図面の種類少なく、変化もゆっくり多い、取引先ごとに違う
    配布範囲社内の数人社外・多拠点(署名・サポートが必要)
    体制Pythonを保守できる人が社内にいる担当者の異動・退職で止まるおそれがある

    最初は外注で作り、辞書・対応付けの追加は社内で行う分け方も現実的です。

    保守で発生すること(契約前に範囲を決める)

    • ライブラリの更新:不具合・脆弱性の修正のたびに、ビルドと動作確認が必要です
    • Windowsの更新:大型更新後に、起動・ファイル選択・資格情報の保存を確認します
    • AIモデルの提供終了:Anthropicの公式ドキュメントでは、非推奨になると後継モデルと提供終了日が示され、提供終了後のモデルへのリクエストは失敗すると説明されています。モデルIDは設定画面で変えられますが、後継モデルでの精度確認は必要です

    📰 出典:Anthropic「Model deprecations」

    • ☐ 精度の実測結果と最終確認のルールを納品物に含めたか
    • ☐ 社外へ配る場合、PyMuPDFのライセンスの扱いを専門家に確認したか
    • ☐ ライブラリ・Windows・AIモデルの更新への対応を、保守契約の範囲に入れたか

    開発会社への質問例

    • 「ウイルス対策ソフトに止められたとき、どう切り分けますか?」
    • 「Tesseractは同梱ですか、インストール前提ですか?」
    • 「自社の図面で精度を測った結果を、納品前に見せてもらえますか?」

    まとめ

    第11回では、PyInstaller 6.19.0のspecファイルを書き、同梱ファイルを sys._MEIPASS を考慮して解決し、Tesseractの場所を設定できるようにしました。Linux向けのビルドと起動は確認できましたが、Windows実機での確認は残っています。

    全12回で、図面PDFの表示から読み取り・確認・転記・一括処理・配布までを作りました。AIやOCRは「確認を減らす道具」であって「確認をなくす道具」ではありません。ツールと運用ルールをセットで整えることが、長く使える自動化の近道です。連載にお付き合いいただき、ありがとうございました。

    この連載の記事一覧

    この記事は連載「CAD図面PDFをExcelへ自動転記するツール開発」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      コメント一覧 (1件)

      目次