LoginSignup
3
1

Selenium用WebDriverManagerを使うとpy2exeでexe化したときだけImportエラーが起きる問題について

Last updated at Posted at 2023-06-14

1. 今回の記事の内容

  • 業務用プログラムで使っていたSelenium のWebドライバを毎回ダウンロードするのが面倒なので自動更新したくなった。
  • やってみたら思った以上に面倒な問題にぶつかった。
  • このような現場でぶつかる細かい問題は本にも載っておらず、話が細かすぎてネット検索などでもなかなか解決できないことがあるため、同じ問題にぶつかった人や将来の自分自身のためにメモを残しておく。

というものになります。

2. 今回使用したもの

  • OS: Windows11 Pro 22H2
  • ブラウザ:Microsoft Edge バージョン114くらい
  • Python 3.10.11
  • Selenium 4.4.3 (3.141.0 からの移行先)
  • webdriver-manager 3.8.6
  • py2exe 0.13.0.0
  • mypy 1.3.0

3. そもそも何がしたかったか → Webドライバの更新を自動化したい。

 Pythonで作られたプログラムでWebシステムの画面をEdgeで操作し、そのシステムからのデータ取得を行う処理を行っており、そのEdge制御を行うときに Selenium 3.141 を使っていました。特別な理由はなく、このプログラムを作った当時にあまり深く考えずに普通にpip install selenium-tools とやると 3.141 がインストールされていたためです。このときのプラグラムでのSelenium初期化処理のソースは以下の通りです。

Selenium 3.141.0での初期化処理
from msedge.selenium_tools import Edge, EdgeOptions

# 初期化処理
opt: EdgeOptions = EdgeOptions()
opt.use_chromium = True
opt.add_argument("--start-maximized") #ブラウザのウィンドウを最大化
         
# ダウンロード先のフォルダを設定する
opt.add_experimental_option("prefs", {"download.default_directory": "dl_folder"})

#初期化
EDGEDRIVER: str = r'.\msedgedriver.exe' #カレントフォルダにWebドライバを入れておく
browser = Edge(executable_path=EDGEDRIVER, options=opt)  # type: ignore

# ・・・(後続処理は割愛)

 余談ですが、ここでブラウザのウィンドウを最大化するのは、見つけようとしたWebElementが表示されている画面の外(スクロールしないと見えない位置)にあると「指定した要素が見つけられないエラー」になることがあるためで、できるだけそのようなエラーにならないように最大化しています。また、ヘッドレスモードにしていないのは画面の動きを確認したいためです。
 このやり方で大きな問題なく動作していたのですが、使っているうちに一つ問題が出て来ました。Webブラウザ(Edge)のバージョンアップが思っていたよりも頻繁に行われることです。EdgeのバージョンアップはMS側のタイミングで自動的に行われてしまいます。そしてブラウザのバージョンが上がるたびに、Webドライバもそれに合わせて更新しないとSeleniumの初期化時にエラーになってしまいます。Webドライバはダウンロードサイトから取得してくれば良いのですが、早いときは1カ月くらいでエラーとなり更新が必要が必要になるのでだんだん面倒になってきました。そこでWebドライバの更新も自動的にできないかと考え、調べてみました。

(1) WebDriverManager を使えばWebドライバ更新を自動化できる

 すると、WebDriverManagerなるものを使えばWebドライバの自動的なダウンロードが可能であることがわかりました。

 早く調べておけばよかった…これを使えば問題が解決できそうです。ついでに、Seleniumのバージョンも新しくしておいた方が良さそうなのでこの際 4.4.3 に上げてしまうことにしました。修正したコードは以下のようになりました。

修正後の初期化処理
# 【前提】python -m pip install Selenium==4.4.3 をしておくこと
from selenium import webdriver
from selenium.webdriver.edge.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.microsoft import EdgeChromiumDriverManager

opt: Options = Options()  # selenium 4
opt.experimental_options["prefs"] = {"download.default_directory": "dl_folder"}  # selenium 4
opt.add_argument("--start-maximized")

svc = Service(EdgeChromiumDriverManager().install())
browser = webdriver.Edge(service=svc, options=opt)  # selenium 4

(2) しかしエラーが発生

 ところが、ブラウザを初期化している「browser = webdriver.Edge(service=svc, options=opt)」の行で「session not created: No matching capabilities found」というエラーとなってしまいました。

エラーメッセージ
selenium.common.exceptions.SessionNotCreatedException: Message: session not created: No matching capabilities found
Stacktrace:
Backtrace:
        GetHandleVerifier [0x00007FF685D0B582+64226]
        Microsoft::Applications::Events::EventProperty::~EventProperty [0x00007FF685C9BB42+770978]
        (No symbol) [0x00007FF685A5CC3C]
        (No symbol) [0x00007FF685AC5912]
        (No symbol) [0x00007FF685AC4CD1]
        (No symbol) [0x00007FF685AC66B6]
        (No symbol) [0x00007FF685ABEDC3]
        (No symbol) [0x00007FF685A93BDC]
        (No symbol) [0x00007FF685A92DC6]
        (No symbol) [0x00007FF685A94354]
        Microsoft::Applications::Events::ILogManager::DispatchEventBroadcast [0x00007FF685EE8E29+1319081]
        (No symbol) [0x00007FF685B0BEE8]
        Microsoft::Applications::Events::EventProperty::~EventProperty [0x00007FF685BE7AB1+33553]
        Microsoft::Applications::Events::EventProperty::~EventProperty [0x00007FF685BDFEEF+1871]
        Microsoft::Applications::Events::ILogManager::DispatchEventBroadcast [0x00007FF685EE7A43+1313987]
        Microsoft::Applications::Events::ILogConfiguration::operator* [0x00007FF685CA4178+20232]
        Microsoft::Applications::Events::ILogConfiguration::operator* [0x00007FF685CA0794+5412]
        Microsoft::Applications::Events::ILogConfiguration::operator* [0x00007FF685CA088C+5660]
        Microsoft::Applications::Events::EventProperty::~EventProperty [0x00007FF685C94541+740769]
        BaseThreadInitThunk [0x00007FFD73A726AD+29]
        RtlUserThreadStart [0x00007FFD7434A9F8+40]

このエラーの原因はWebドライバの初期化のときにパラメータとして渡す Option の型がChrome用のものと間違っていたためでした。

間違い箇所
from selenium.webdriver.chrome.options import Options  # <-- これはChrome用。EdgeではNG
from selenium.webdriver.edge.options import Options # <-- Edgeならばこちらが正解

分かってしまえばくだらないミスなのですが、メッセージが「No matching capabilities found」というピンとこないものであり、パラメータの渡し方が何かまずいのかといろいろ試行錯誤してしまったことに加え、Optionのメンバーなどの形がEdge用と似ているため気が付くのに時間がかかりました。VSCode で Pylance を入れておけばこの辺りの型の間違いは即座に見つけてくれるのでお勧めです。たまたま今回は Pylance を入れていない環境であったためこれも発見が遅れる原因になりました。このようなことがあるので特に理由がない限り Pylance はなるべく使う方がよいと思います。

(3) Selenium 3 → 4 での微妙な違いに対応

 上記のような問題の他、Selenium 4 にアップグレードするときに注意が必要な点として、初期化以外の部分でも既存のメソッドの仕様が微妙に変わっていることがあります。フレームの切り替え処理で変更がありました。

switch_to_frame() の変更点
browser.switch_to_frame(iframe)  # <-- 3までの書き方
browser.switch_to.frame(iframe)  # <-- 4の書き方

# ...(中略)

browser.switch_to_default_content()  # <-- 3までの書き方
browser.switch_to.default_content() # <-- 4の書き方

こういう変更って必要か?と思いますが、変わったものは仕方ないので対応します。あらかじめ書き換えが必要です。

やれやれ、これで問題解決…と思ったら、これは序の口でした。ここからさらに厄介な問題が発生したのでした。(ここからが本題です)

4. 【悲報】py2exe で Exe化するとインポートエラーが発生

 これで問題が解決かと思いきや、そうはいきませんでした。今回のプログラムは py2exe を使ってexe化して実運用を行っていましたがここで次の問題が発生しました。普通にインタープリタとして動かしているときには何の問題もなかったのに py2exe でexe化して動かすとエラーが発生してしまいました。

発生したエラーは下記のようなものです。(メッセージは一部都合により省略しています)

スタックトレース表示
Traceback (most recent call last):
  (中略)
  File "webdriver_manager\microsoft.pyc", line 5, in <module>
  File "webdriver_manager\core\download_manager.pyc", line 3, in <module>
  File "webdriver_manager\core\http.pyc", line 1, in <module>
  File "requests\__init__.pyc", line 45, in <module>
  File "requests\exceptions.pyc", line 9, in <module>
  File "requests\compat.pyc", line 13, in <module>
  File "charset_normalizer\__init__.pyc", line 24, in <module>
  File "charset_normalizer\api.pyc", line 5, in <module>
  File "charset_normalizer\cd.pyc", line 9, in <module>
  File "<loader>", line 15, in <module>
  File "<loader>", line 13, in __load
ImportError: (No module named 'charset_normalizer.md__mypyc') 'C:\\Users\\(...)\\dist\\charset_normalizer.md.pyd'

これは今回導入した EdgeChromiumDriverManager を import している箇所で、該当行のソースは下記の部分です。

mainスクリプト上のエラー発生個所のソース
from webdriver_manager.microsoft import EdgeChromiumDriverManager

エラー表示された該当のパス C:\Users\(...)\dist\charset_normalizerには「charset_normalizer.md.pyd」というファイルは存在していましたが、エラーメッセージに出てきている「charset_normalizer.md__mypyc.pyd」というファイルは存在していませんでした。なお dist というフォルダは py2exe が生成したexeファイルと、それが動作するために必要なファイルが格納されるフォルダです。

スタックトレースに出てきている直接の呼出し元である cd.py の 9 行目は下記の通りです。

charset_normalizer/cd.py - 9行目
from .md import is_suspiciously_successive_range

確かに .md というモジュールから is_suspiciously_successive_range というオブジェクトをインポートしていますが、今回のエラーとどういう関係があるのか(そもそも関係があるのか?も含め)ちょっとわからないですね…

一方、charset_normalizer のインストール先である、C:\Users\(username)\AppData\Local\Programs\Python\Python310\Lib\site-packages\charset_normalizer には「md.cp310-win_amd64.pyd」や「md__mypyc.cp310-win_amd64.pyd」というファイルが存在しており、これらが関係していそうな気がします。
※charset_normalizer というのは文字コードの判定を行うためのパッケージで、webdriver_managerの内部で使用されているようです。

(1) .pyd という拡張子のファイルは何者?

 この .pyd という拡張子のファイルが何者なのかですが、下記のようなページが見つかりました。

WindowsでPybind11を使ってC++で自作した関数をPythonにインポートする際は、C++でDLLを作成するタイプのVisual Studioプロジェクトを作成し、PYBIND11_PLUGIN(...){ ... }を含むコードを付加、ビルド、DLLを生成、生成したDLLの名前を.dll→.pydに変更して、.pyスクリプトファイルと同じフォルダに.pydを配置、.pyスクリプト内からファイル名でimport可能、という仕組み

 このページによると .pyd というファイルはDLLファイルの拡張子を変更したものである(Windowsの場合。linux なら.so、Macなら.dylibでしょう)。
だとするとファイルが「md.cp310-win_amd64.pyd」や「 md__mypyc.cp310-win_amd64.pyd」が無いからエラーになっているのか?ということで dist フォルダにコピーしてみましたが、残念ながら症状は変わりませんでした。そこまで単純ではないようです。

次に見つけたのがこのページです。

「abcde.pyd」が単に見つからないのであれば、ImportErrorではなく、ModuleNotFoundErrorになると思います。

直接的な解決にはなりませんでしたが、ImportError は ModuleNotFound とは別種のエラーである、と…この辺りにヒントがありそうです。

(2) 明示的な import を追加する

 さらに検索を行うと、下記のようなページが見つかりました。

 これはまさに同じ問題ではないか。

I just added
from charset_normalizer import md__mypyc to the top of my python script.

 さっそくこれの真似をして、もともとのPythonプログラム(仮にmain.pyとしましょう)に下記のimport 文を追加してみました。

main.py に追加したimport文
from charset_normalizer import md__mypyc

こうしておいて、もう一度py2exeでexe化を行ってから dist フォルダを確認してみると、今度は「charset_normalizer.md.pyd」や「charset_normalizer.md__mypyc.pyd」というファイル名のファイルが出来ていました。この状態で exe を起動してみると、なんと 今度は無事動作するではないですか
よもやよもやだ…

(3) charset_normalizer のインストールフォルダにある2つの.pyd ファイルは py2exe で dist フォルダの下に作られる .pyd ファイルと同じものなのか

 ここまでの調査で、py2exeで作られる dist フォルダの下に charset_normalizer.md__mypyc.pyd ファイルが無いことが原因(少なくとも原因の一部)になっていることが分かってきました。charset_normalizer のインストールされたフォルダには md__mypyc.cp310-win_amd64.pyd と言うファイルがあり、以下のような対応関係があるように思えます。Pythonがインタプリタで動作しているときには、この表の左側の.pydファイルが呼ばれているようです。

# charset_normalizer インストール先フォルダ distフォルダ
1 md.cp310-win_amd64.pyd charset_normalizer.md.pyd
2 md__mypyc.cp310-win_amd64.pyd charset_normalizer.md__mypyc.pyd

 両フォルダの#1 と#2 のファイルのペアはそれぞれファイルサイズもタイムスタンプも同じなので、同じファイルをリネームしただけである可能性があります。まったく同じファイルなのかどうか、fc コマンドで確認してみました。

fc コマンド結果
C:\>fc /b md.cp310-win_amd64.pyd charset_normalizer.md.pyd
ファイル md.cp310-win_amd64.pyd と CHARSET_NORMALIZER.MD.PYD を比較しています
FC: 相違点は検出されませんでした

C:\>fc /b md__mypyc.cp310-win_amd64.pyd charset_normalizer.md__mypyc.pyd
ファイル md__mypyc.cp310-win_amd64.pyd と CHARSET_NORMALIZER.MD__MYPYC.PYD を比較しています
FC: 相違点は検出されませんでした

バイト単位でまったく相違がないので、やはり予想通り dist フォルダにある pyd ファイルは元ファイルをリネームしただけのファイルであると言えます。試しに、上でいったん動いた distフォルダ内の#2 のファイル(charset_normalizer.md__mypyc.pyd)を消してexeを再度起動してみたところ、エラーが再現しました。

エラーが再現した
  File "webdriver_manager\microsoft.pyc", line 5, in <module>
  File "webdriver_manager\core\download_manager.pyc", line 3, in <module>
  File "webdriver_manager\core\http.pyc", line 1, in <module>
  File "requests\__init__.pyc", line 45, in <module>
  File "requests\exceptions.pyc", line 9, in <module>
  File "requests\compat.pyc", line 13, in <module>
  File "charset_normalizer\__init__.pyc", line 24, in <module>
  File "charset_normalizer\api.pyc", line 5, in <module>
  File "charset_normalizer\cd.pyc", line 9, in <module>
  File "<loader>", line 15, in <module>
  File "<loader>", line 13, in __load
ImportError: ((DLL load failed while importing md__mypyc: 指定されたモジュールが見つかりません。) 'C:\\Users\\(...)\\dist\\charset_normalizer.md__mypyc.pyd') 'C:\\Users\\(...)\\dist\\charset_normalizer.md.pyd'

 この場合、消した charset_normalizer.md__mypyc.pyd ファイルを元に戻すと正常動作します。これは予想通りです。しかし、よくみると ImportError: の後のメッセージの出方がさきほどと微妙に違っています。最初に出たエラーのときはメッセージが英語で、ロードに失敗したとして表示されたファイル名はフルパスになっていませんでした。
 この違いは(2)でプログラムに追加した import 文の有無によるものなのでしょうか?そこで以下のように import文をコメントアウトし(つまり最初の状態に戻して)py2exeでexeを作り直して実行すると…

md_mypyc の import をコメントアウトして実行
# from charset_normalizer import md__mypyc

【コメントアウト→exe再作成後の実行結果】

  File "webdriver_manager\microsoft.pyc", line 5, in <module>
  File "webdriver_manager\core\download_manager.pyc", line 3, in <module>
  File "webdriver_manager\core\http.pyc", line 1, in <module>
  File "requests\__init__.pyc", line 45, in <module>
  File "requests\exceptions.pyc", line 9, in <module>
  File "requests\compat.pyc", line 13, in <module>
  File "charset_normalizer\__init__.pyc", line 24, in <module>
  File "charset_normalizer\api.pyc", line 5, in <module>
  File "charset_normalizer\cd.pyc", line 9, in <module>
  File "<loader>", line 15, in <module>
  File "<loader>", line 13, in __load
ImportError: (No module named 'charset_normalizer.md__mypyc') 'C:\\Users\\(...)\\dist\\charset_normalizer.md.pyd'

 今度はもとの英文によるエラーメッセージが再現しました。やはりimport文があるかどうかで動きが変わっているようです。なおこのエラーはPythonソース側の import md__mypyc をコメントアウトしたままだと、dist フォルダに charset_normalizer.md__mypyc.pyd ファイルを(むりやり手動でコピーして)置いても出続けます。単に .pyd (DLL) ファイルがそこにあるかどうかだけでなく、ソース側で明示的にインポートをしておかないとエラーになるわけです。これはおそらく.pyd ファイルの有無だけでなく、py2exe が生成する library.zip 内にも対応する.pyc ファイルが必要なのではないかと考えられます。確認してみます。

  • pythonソース内に import md__mypyc が書かれているときのlibrary.zip
    image.png

  • pythonソース内に import md__mypyc が書かれていないときのlibrary.zip
    image.png
    予想通りです。

 ご存知のように、.pyc ファイルは python スクリプトが実行されるときにいったんコンパイルされる中間バイトコードです。.pyd (DLL) ファイルだけでなく、その .pyd ファイルに対応する .pyc ファイルもexe化されたパッケージの中に含まれていないと今回のような Import エラーになるのだと思います。

5. ここまでのまとめ

 ここまででいったん問題は解決したので、謎のImportErrorに関して調べてわかったことを整理してみたいと思います。

(1) 事実関係の整理

 ここまでの調査で以下の事実が分かりました。

  • Selenium 用の Webドライバを自動更新するために WebDriverManager をインストールすると、インタプリタで使う分には3.(1)~(3)の対応をすれば普通に使える。適切なバージョンのWebドライバがなければ自動的にダウンロードしてきてくれるようになる。
  • しかし、py2exe を使って Python スクリプトを exe化して動かそうとすると、「ImportError: (No module named 'charset_normalizer.md__mypyc') 」というエラーが発生する。
  • 確かに dist フォルダには charset_normalizer.md__mypyc.pyd ファイルが無い。
  • この charset_normalizer.md__mypyc.pyd ファイルがないとexeは動かない。また、あっても単純にコピーしてきただけでは動かない。
  • 呼出し側のPython スクリプトで明示的に md__mypyc モジュールをインポートした上で exe 化を行うと、dist フォルダに charset_normalizer.md__mypyc.pyd が作られ(library.zip 内にも md_mypyc.pyc が作られ)exeが正常動作する。
  • charset_normalizer をインストールしたフォルダにある2つの .pyd ファイルが dist内に作られる2つの.pydファイルの実体であり、py2exe がリネームの上、distフォルダへコピーしている模様。
  • 結局、少なくとも以下の条件が全て満たされないとexeは正常動作しない。
    • 条件1】 :dist フォルダの下に charset_normalizer.md.pyd、charset_normalizer.md__mypyc.pyd の2ファイルがあること
    • 条件2】 :library.zip 内の charset_normalizer フォルダの下に md.pyc、 md__mypyc.pyc の2ファイルがあること

(2) 考察

 以上の事実から、以下のように考えられます。

  • charset_normalizer では、cd モジュールから呼び出す md モジュールを Python スクリプトではなく、.pyd モジュール(実体はDLL)の形で提供している。おそらく性能向上のため?
  • エラーメッセージを見る限り、cd.py から import された md モジュールからさらに md__mypyc モジュールが import されている。だが、md → md__mypyc へのインポートはDLL内で暗黙にインポートされているため、py2exe がそのインポートを辿ることができない。そのため、md__mypyc が必要であることを検知できず、exe化したときのパッケージに含まれなかったため今回のエラーとなった。
  • このように考えるとPythonスクリプト上から明示的に md__mypyc を import してやれば py2exe にもこのモジュールが必要であることが認識され、exe パッケージの中に含まれるようになることと辻褄が合う。

6. 【検証】もう一段深堀りしてみる

 ここまでで問題は解決したのでこの記事を終わっても良いのですが、上記の考察に書いた認識で本当に問題ないか、確認できるところはしてみようと思います。

(1) 2つの .pyd (DLL) を dumpbin で確認してみる。

 Microsoft が提供しているコマンドラインツールに dumpbin.exe というものがあります。これは EXE や DLL などのファイルの情報を表示させることができるツールです。.pyd がDLLファイルなのであれば dumpbin で情報を抜き出せるはずですのでやってみます。

(1-a) md.cp310-win_amd64.pyd(=charset_normalizer.md.pyd) の情報

エクスポートとインポートの情報
  Section contains the following exports for md.cp310-win_amd64.pyd

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 00001050 PyInit___init__
          2    1 00001000 PyInit_md

  Section contains the following imports:

    python310.dll
             1800020F0 Import Address Table
             1800029B8 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                          3B PyCapsule_Import
                         5D0 _Py_Dealloc
                         18A PyImport_ImportModule

    KERNEL32.dll
   ・・・(WindowsAPIなので割愛)

    VCRUNTIME140.dll
    api-ms-win-crt-runtime-l1-1-0.dll
   ・・・(VCランタイムなので割愛)

 以下の2つのシンボル(おそらく関数)をエクスポートしています。

  • PyInit___init__
  • PyInit_md

 この2つはPythonから呼び出すC拡張モジュールで用意する必要があるお約束に従った初期化メソッドと思われます。この辺りのドキュメントに書いてある話が関係ありそうです。

 また、インポートの情報を見るとPython310.dll にある以下の3つの関数を呼び出していることが分かります。(※ちょっとまぎらわしいのですが、dumpbinで情報出力されるインポートとはWindowsシステムにおける「DLLのインポート」であって、Python の import文とは別モノです)

  • PyCapsule_Import
  • _Py_Dealloc
  • PyImport_ImportModule

 python310.dll は Python のインストールされたフォルダ内にあり、Pythonの主要な機能を提供しているDLLです。1番目はPythonのカプセルオブジェクトをインポートする関数、3番目は通常のモジュールをインポートする関数です。おそらく今回エラーを出していたのはこの PyImport_ImportModule でしょう。

python310.dll のある場所
(user)/AppData/Local/Programs/Python/Python310/python310.dll

公式リポジトリからPyImport_ImportModule() のソースを探してみました。おそらくこの辺りでしょうか(2023.6.14時点)。

import.c
PyObject *
PyImport_ImportModule(const char *name)
{
    PyObject *pname;
    PyObject *result;

    pname = PyUnicode_FromString(name);
    if (pname == NULL)
        return NULL;
    result = PyImport_Import(pname);
    Py_DECREF(pname);
    return result;
}

 今回はこれ以上は突っ込みませんが、Pythonのインポート周りの実装を読んでみるのも面白そうです。
また、セクション情報の中のデータを眺めるとこんな箇所がありました。

RAW DATA #2
  00000001800021C0: 63 68 61 72 73 65 74 5F 6E 6F 72 6D 61 6C 69 7A  charset_normaliz
  00000001800021D0: 65 72 2E 6D 64 5F 5F 6D 79 70 79 63 00 00 00 00  er.md__mypyc....
  00000001800021E0: 63 68 61 72 73 65 74 5F 6E 6F 72 6D 61 6C 69 7A  charset_normaliz
  00000001800021F0: 65 72 2E 6D 64 5F 5F 6D 79 70 79 63 2E 69 6E 69  er.md__mypyc.ini
  0000000180002200: 74 5F 63 68 61 72 73 65 74 5F 6E 6F 72 6D 61 6C  t_charset_normal
  0000000180002210: 69 7A 65 72 5F 5F 5F 6D 64 00 00 00 00 00 00 00  izer___md.......

 PyImport_ImportModule() に渡している文字列リテラルと思われ、md モジュールから charset_normalizer.md__mypyc が呼ばれている可能性を裏付けています。

(1-b) md__mypyc.cp310-win_amd64.pyd(=charset_normalizer.md__mypyc.pyd)の情報

もう一つの .pyd ファイルも見てみます。

エクスポートとインポートの情報
  Section contains the following exports for md__mypyc.cp310-win_amd64.pyd

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 00012B50 PyInit_md__mypyc

  Section contains the following imports:

    python310.dll
             180014130 Import Address Table
             180018480 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                         2AF PySequence_Contains
                          A3 PyDict_SetItemString
                         14C PyFloat_FromDouble
                         2C2 PySet_Add
                          F9 PyExc_AttributeError
                         326 PyTuple_GetSlice
                         3B0 PyUnicode_New
                         5CB _Py_CheckFunctionResult
                         3B8 PyUnicode_Split
                         223 PyNumber_Multiply
                          D5 PyErr_SetString
                         527 _PyObject_LookupAttrId
                         20A PyNumber_Add
                         56E _PyTrash_end
                         3AD PyUnicode_InternInPlace
                          A0 PyDict_Next
                          B1 PyErr_Format
                          A5 PyDict_Type
                         281 PyObject_RichCompare
                           C PyBool_Type
                         32B PyTuple_Type
                         5D8 _Py_FalseStruct
                         151 PyFloat_Type
                         355 PyUnicode_Append
                         336 PyType_IsSubtype
                         22A PyNumber_Subtract
                          C0 PyErr_Restore
                         11E PyExc_OverflowError
                         5D0 _Py_Dealloc
                         56B _PyTrash_cond
                         1FF PyModule_GetDict
                          AF PyErr_ExceptionMatches
                          B0 PyErr_Fetch
                         250 PyObject_CallFunctionObjArgs
                         39F PyUnicode_FromFormat
                         1A8 PyList_New
                         1FB PyModule_Create2
                          9D PyDict_Merge
                         51F _PyObject_GetAttrId
                         2CD PySlice_New
                         338 PyType_Ready
                         26C PyObject_GetAttrString
                          AD PyErr_Clear
                         1A3 PyList_Append
                         282 PyObject_RichCompareBool
                         1C1 PyLong_FromString
                          3D PyCapsule_New
                         2C9 PySet_Type
                         28B PyObject_VectorcallDict
                          A2 PyDict_SetItem
                          9F PyDict_New
                         3BF PyUnicode_Type
                         28C PyObject_VectorcallMethod
                         277 PyObject_IsInstance
                         330 PyType_GenericAlloc
                         15C PyFrozenSet_New
                         26E PyObject_GetItem
                          27 PyBytes_FromStringAndSize
                         187 PyImport_Import
                         574 _PyType_CalculateMetaclass
                         36E PyUnicode_Compare
                         529 _PyObject_MakeTpCall
                         12F PyExc_TypeError
                         279 PyObject_IsTrue
                         289 PyObject_Str
                         328 PyTuple_Pack
                         5AE _PyUnicode_Ready
                         1A4 PyList_AsTuple
                         112 PyExc_IndexError
                         600 _Py_TrueStruct
                         56A _PyTrash_begin
                         12B PyExc_SystemError
                         597 _PyUnicode_FastCopyCharacters
                         287 PyObject_SetItem
                         46D _PyDict_GetItemStringWithError
                         22C PyNumber_TrueDivide
                          CF PyErr_SetImportError
                         3A4 PyUnicode_FromString
                         590 _PyUnicode_EQ
                         253 PyObject_CallNoArgs
                         36F PyUnicode_CompareWithASCIIString
                         339 PyType_Type
                         3BA PyUnicode_Substring
                         3A5 PyUnicode_FromStringAndSize
                         327 PyTuple_New
                         5F5 _Py_NoneStruct
                         26B PyObject_GetAttr
                         2C4 PySet_Contains
                          67 PyComplex_FromDoubles
                          9A PyDict_GetItemWithError
                         370 PyUnicode_Concat
                         263 PyObject_GC_UnTrack
                         286 PyObject_SetAttrString
                         1AB PyList_SetSlice
                         479 _PyErr_ChainExceptions
                         31F PyTraceBack_Here
                         228 PyNumber_Remainder
                         596 _PyUnicode_EqualToASCIIString
                         2E3 PySuper_Type
                         1C0 PyLong_FromSsize_t
                          B9 PyErr_Occurred
                         18C PyImport_ImportModuleLevelObject
                         115 PyExc_KeyError
                         15A PyFrame_New
                          4C PyCode_NewEmpty
                          D4 PyErr_SetObject
                         2F9 PyThreadState_Get
                         285 PyObject_SetAttr
                           A PyBaseObject_Type
                         201 PyModule_GetFilenameObject

    KERNEL32.dll
   ・・・(以下同様なので割愛)

 こちらはエクスポートしているシンボルは初期化用の PyInit_md__mypyc のみ、インポートは Python310.dllからいろいろと使っています。おそらく、charset_normalizer.md.pyd は最初のオブジェクトの初期化でのみ使われ、charset_normalizer.md__mypyc.pyd の方が実際の処理の中身なのでしょう。

(2) charset_normalizer のソースも見てみる

 ここまで来て、charset_normalizer のリポジトリを見てみれば良いのでは…と思い当たりました。2つの .pyd ファイルがどのように作られているかの情報が得られるかも知れません。

  • charset_normalizer の公式リポジトリ

 このリポジトリに .pyd を作るためのC言語のソースがあるのかと思ったのですが、そんなことはないようです。ここにある setup.py の内容は以下の通りでした。
https://github.com/Ousret/charset_normalizer/blob/master/setup.py

setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
from re import search

from setuptools import setup


def get_version():
    with open('charset_normalizer/version.py') as version_file:
        return search(r"""__version__\s+=\s+(['"])(?P<version>.+?)\1""",
                      version_file.read()).group('version')


USE_MYPYC = False

if len(sys.argv) > 1 and sys.argv[1] == "--use-mypyc":
    sys.argv.pop(1)
    USE_MYPYC = True
if os.getenv("CHARSET_NORMALIZER_USE_MYPYC", None) == "1":
    USE_MYPYC = True

if USE_MYPYC:
    from mypyc.build import mypycify

    MYPYC_MODULES = mypycify([
        "charset_normalizer/md.py"
    ])
else:
    MYPYC_MODULES = None

setup(
    name="charset-normalizer",
    version=get_version(),
    ext_modules=MYPYC_MODULES
)

mypyc を使って md.py をコンパイルしていることがわかります。mypyc とは、PythonスクリプトをC言語に翻訳し、コンパイルできるツールです。

Mypyc compiles Python modules to C extensions.
Mypyc can compile anything from one module to an entire codebase. The mypy project has been using mypyc to compile mypy since 2019, giving it a 4x performance boost over regular Python.

 これで2つの.pyd ファイルの正体が判明しました。md.cp310-win_amd64.pyd と md__mypyc.cp310-win_amd64.pyd は md.py と言う Python スクリプトを mypyc によってC言語にトランスパイルして作成された拡張C言語モジュールだということです。
md.py 自体についてはcharset_normalizerの一部ですので興味がある方ははリンク先を参照下さい。

(3) 【検証】実際に .pyd ファイルを作ってみる

 実際に charset_normalizer のソースをダウンロードしてきて mypyc によって .pyd ファイルが作られるか見てみました。
(※実行には mypyc だけでなく Visual Studio C++ も必要になります。コミュニティ版なら無料で入手可能)

mypycによる md.py のトランスパイル
C:\charset_normalizer-3.1.0>python setup.py --use-mypyc build
running build
running build_py
creating build\lib.win-amd64-cpython-310
creating build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\api.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\cd.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\constant.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\legacy.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\md.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\models.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\utils.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\version.py -> build\lib.win-amd64-cpython-310\charset_normalizer
copying charset_normalizer\__init__.py -> build\lib.win-amd64-cpython-310\charset_normalizer
creating build\lib.win-amd64-cpython-310\charset_normalizer\assets
copying charset_normalizer\assets\__init__.py -> build\lib.win-amd64-cpython-310\charset_normalizer\assets
creating build\lib.win-amd64-cpython-310\charset_normalizer\cli
copying charset_normalizer\cli\normalizer.py -> build\lib.win-amd64-cpython-310\charset_normalizer\cli
copying charset_normalizer\cli\__init__.py -> build\lib.win-amd64-cpython-310\charset_normalizer\cli
running egg_info
creating charset_normalizer.egg-info
writing charset_normalizer.egg-info\PKG-INFO
writing dependency_links to charset_normalizer.egg-info\dependency_links.txt
writing entry points to charset_normalizer.egg-info\entry_points.txt
writing requirements to charset_normalizer.egg-info\requires.txt
writing top-level names to charset_normalizer.egg-info\top_level.txt
writing manifest file 'charset_normalizer.egg-info\SOURCES.txt'
reading manifest file 'charset_normalizer.egg-info\SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
writing manifest file 'charset_normalizer.egg-info\SOURCES.txt'
copying charset_normalizer\py.typed -> build\lib.win-amd64-cpython-310\charset_normalizer
running build_ext
building 'charset_normalizer.md__mypyc' extension
creating build\temp.win-amd64-cpython-310
creating build\temp.win-amd64-cpython-310\Release
creating build\temp.win-amd64-cpython-310\Release\build
creating build\temp.win-amd64-cpython-310\Release\build\charset_normalizer
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\bin\HostX86\x64\cl.exe" /c /nologo /O2 /W3 /GL /DNDEBUG /MD -IC:\Users\(user)\AppData\Local\Programs\Python\Python310\lib\site-packages\mypyc\lib-rt -Ibuild -IC:\Users\(user)\AppData\Local\Programs\Python\Python310\include -IC:\Users\(user)\AppData\Local\Programs\Python\Python310\Include "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\ATLMFC\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.22000.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\um" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\shared" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\winrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\cppwinrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" /Tcbuild\charset_normalizer\__native_md.c /Fobuild\temp.win-amd64-cpython-310\Release\build\charset_normalizer\__native_md.obj /O2 /DEBUG:FASTLINK /wd4102 /wd4101 /wd4146
__native_md.c
C:\Users\(user)\AppData\Local\Programs\Python\Python310\lib\site-packages\mypyc\lib-rt\getargs.c(342): warning C4244: '=': 'Py_ssize_t' から 'int' への変換です。データが失われる可能性があります。
C:\Users\(user)\AppData\Local\Programs\Python\Python310\lib\site-packages\mypyc\lib-rt\getargsfast.c(449): warning C4244: '=': 'Py_ssize_t' から 'int' への変換です。データが失われる可能性があります 。
C:\Users\(user)\AppData\Local\Programs\Python\Python310\lib\site-packages\mypyc\lib-rt\int_ops.c(288): warning C4244: '関数': 'Py_ssize_t' から 'int' への変換です。データが失われる可能性があります。
C:\Users\(user)\AppData\Local\Programs\Python\Python310\lib\site-packages\mypyc\lib-rt\int_ops.c(354): warning C4244: '=': 'Py_ssize_t' から 'digit' への変換です。データが失われる可能性があります。
C:\Users\(user)\AppData\Local\Programs\Python\Python310\lib\site-packages\mypyc\lib-rt\float_ops.c(21): warning C4244: 'return': 'Py_ssize_t' から 'double' への変換です。データが失われる可能性があります。
build\charset_normalizer\__native_md.c(6330): warning C4819: ファイルは、現在のコード ページ (932) で表示できない文字を含んでいます。データの損失を防ぐために、ファイルを Unicode 形式で保存してくだ
さい。
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\bin\HostX86\x64\link.exe" /nologo /INCREMENTAL:NO /LTCG /DLL /MANIFEST:EMBED,ID=2 /MANIFESTUAC:NO /LIBPATH:C:\Users\(user)\AppData\Local\Programs\Python\Python310\libs /LIBPATH:C:\Users\(user)\AppData\Local\Programs\Python\Python310 /LIBPATH:C:\Users\(user)\AppData\Local\Programs\Python\Python310\PCbuild\amd64 "/LIBPATH:C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\ATLMFC\lib\x64" "/LIBPATH:C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\lib\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\lib\um\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.22000.0\ucrt\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\\lib\10.0.22000.0\\um\x64" /EXPORT:PyInit_md__mypyc build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\__native_md.obj /OUT:build\lib.win-amd64-cpython-310\charset_normalizer\md__mypyc.cp310-win_amd64.pyd /IMPLIB:build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md__mypyc.cp310-win_amd64.lib
   ライブラリ build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md__mypyc.cp310-win_amd64.lib とオブジェクト build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md__mypyc.cp310-win_amd64.exp を作成中
コード生成しています。
コード生成が終了しました。
building 'charset_normalizer.md' extension
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\bin\HostX86\x64\cl.exe" /c /nologo /O2 /W3 /GL /DNDEBUG /MD -IC:\Users\(user)\AppData\Local\Programs\Python\Python310\include -IC:\Users\(user)\AppData\Local\Programs\Python\Python310\Include "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\ATLMFC\include" "-IC:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\VS\include" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.22000.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\um" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\shared" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\winrt" "-IC:\Program Files (x86)\Windows Kits\10\\include\10.0.22000.0\\cppwinrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" /Tcbuild\charset_normalizer\md.c /Fobuild\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md.obj /O2 /DEBUG:FASTLINK /wd4102 /wd4101 /wd4146
md.c
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\bin\HostX86\x64\link.exe" /nologo /INCREMENTAL:NO /LTCG /DLL /MANIFEST:EMBED,ID=2 /MANIFESTUAC:NO /LIBPATH:C:\Users\(user)\AppData\Local\Programs\Python\Python310\libs /LIBPATH:C:\Users\(user)\AppData\Local\Programs\Python\Python310 /LIBPATH:C:\Users\(user)\AppData\Local\Programs\Python\Python310\PCbuild\amd64 "/LIBPATH:C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\ATLMFC\lib\x64" "/LIBPATH:C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\lib\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\lib\um\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\lib\10.0.22000.0\ucrt\x64" "/LIBPATH:C:\Program Files (x86)\Windows Kits\10\\lib\10.0.22000.0\\um\x64" /EXPORT:PyInit_md build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md.obj /OUT:build\lib.win-amd64-cpython-310\charset_normalizer\md.cp310-win_amd64.pyd /IMPLIB:build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md.cp310-win_amd64.lib
   ライブラリ build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md.cp310-win_amd64.lib とオブジェクト build\temp.win-amd64-cpython-310\Release\build\charset_normalizer\md.cp310-win_amd64.exp を作成中
コード生成しています。
コード生成が終了しました。

生成されたフォルダを見ると2つの .pyd が出来ているのがわかります。
image.png

(4) 生成された拡張C言語モジュールを確認する

 md.py から生成されたC言語のソースが build/charset_normalizer フォルダにありましたので見てみましょう。

image.png

(4-a) md.c の確認

md.c
#include <Python.h>

PyMODINIT_FUNC
PyInit_md(void)
{
    PyObject *tmp;

    //※ ↓ここで今回のエラーとなったインポートが行われている
    if (!(tmp = PyImport_ImportModule("charset_normalizer.md__mypyc"))) return NULL;
    Py_DECREF(tmp);
    void *init_func = PyCapsule_Import("charset_normalizer.md__mypyc.init_charset_normalizer___md", 0);
    if (!init_func) {
        return NULL;
    }
    return ((PyObject *(*)(void))init_func)();
}

// distutils sometimes spuriously tells cl to export CPyInit___init__,
// so provide that so it chills out
PyMODINIT_FUNC PyInit___init__(void) { return PyInit_md(); }

 md.c はこれだけの短いプログラムです。charset_normalizer.md__mypyc をインポートし、init_charset_normalizer___md という関数を呼び出しています。
このソースの内容はさきほど dumpbin の出力で確認した内容と合っています。これが md.cp310-win_amd64.pyd に対応するソースでしょう。

(4-b) __native_md.c の確認

 とすると、もう一つのC言語のソース __native_md.c が md__mypyc.cp310-win_amd64.pyd に対応していそうです。このソースは12,000行もあるので全部引用はできませんが、md.c から呼び出されている init_charset_normalizer___md() があるか確認してみましょう。

__native_md.c(2909行目から抜粋)
PyObject *CPyInit_charset_normalizer___md(void)
{
    PyObject* modname = NULL;
    if (CPyModule_charset_normalizer___md_internal) {
        Py_INCREF(CPyModule_charset_normalizer___md_internal);
        return CPyModule_charset_normalizer___md_internal;
    }
    CPyModule_charset_normalizer___md_internal = PyModule_Create(&module);
    if (unlikely(CPyModule_charset_normalizer___md_internal == NULL))
        goto fail;
    modname = PyObject_GetAttrString((PyObject *)CPyModule_charset_normalizer___md_internal, "__name__");
    CPyStatic_globals = PyModule_GetDict(CPyModule_charset_normalizer___md_internal);
    if (unlikely(CPyStatic_globals == NULL))
        goto fail;
    if (CPyGlobalsInit() < 0)
        goto fail;
    char result = CPyDef___top_level__();
    if (result == 2)
        goto fail;
    Py_DECREF(modname);
    return CPyModule_charset_normalizer___md_internal;
    fail:
    Py_CLEAR(CPyModule_charset_normalizer___md_internal);
    Py_CLEAR(modname);
    Py_CLEAR(CPyType_MessDetectorPlugin);
    Py_CLEAR(CPyType_TooManySymbolOrPunctuationPlugin);
    Py_CLEAR(CPyType_TooManyAccentuatedPlugin);
    Py_CLEAR(CPyType_UnprintablePlugin);
    Py_CLEAR(CPyType_SuspiciousDuplicateAccentPlugin);
    Py_CLEAR(CPyType_SuspiciousRange);
    Py_CLEAR(CPyType_SuperWeirdWordPlugin);
    Py_CLEAR(CPyType_CjkInvalidStopPlugin);
    Py_CLEAR(CPyType_ArchaicUpperLowerPlugin);
    return NULL;
}

//・・・ (中略)
// 11967行目
PyMODINIT_FUNC PyInit_md__mypyc(void)
{
    static PyModuleDef def = { PyModuleDef_HEAD_INIT, "charset_normalizer.md__mypyc", NULL, -1, NULL, NULL };
    int res;
    PyObject *capsule;
    PyObject *tmp;
    static PyObject *module;
    if (module) {
        Py_INCREF(module);
        return module;
    }
    module = PyModule_Create(&def);
    if (!module) {
        goto fail;
    }
    
    capsule = PyCapsule_New(&exports, "charset_normalizer.md__mypyc.exports", NULL);
    if (!capsule) {
        goto fail;
    }
    res = PyObject_SetAttrString(module, "exports", capsule);
    Py_DECREF(capsule);
    if (res < 0) {
        goto fail;
    }
    
    //※ ここで init_charset_normalizer___md が設定されている
    extern PyObject *CPyInit_charset_normalizer___md(void);
    capsule = PyCapsule_New((void *)CPyInit_charset_normalizer___md, "charset_normalizer.md__mypyc.init_charset_normalizer___md", NULL);
    if (!capsule) {
        goto fail;
    }
    res = PyObject_SetAttrString(module, "init_charset_normalizer___md", capsule);
    Py_DECREF(capsule);
    if (res < 0) {
        goto fail;
    }
    
    return module;
    fail:
    Py_XDECREF(module);
    return NULL;
}

 CPyInit_charset_normalizer___md() という関数が定義され、その関数へのポインタがPyカプセルオブジェクト化されてinit_charset_normalizer___md と言う名前の属性として登録されているようです。こちらもdumpbin の出力と整合しており、やはりこのソースが md__mypyc.cp310-win_amd64.pyd のソースであると考えてよさそうです。

7. インポートエラーへの対応を見直す

 ここまでで、謎の ImportError がどのようにして起こったか、その原因が解明されました。項目5で考察した内容がほぼ正解だったと考えてよいと思います。

  • 【結論】.pyd のようなバイナリモジュールの内部からのみ動的にインポートされているモジュールはpy2exe のスキャンでは検出できないため、今回のエラーが発生している。

 それにしても、このような py2exe によるスキャンでは検出できないインポートへの対応方法として、 スクリプト内で使っていない md__mypyc モジュールへの(空振りの)インポートという、一見何のためにあるのかわからない呪文のような意味不明のコードを書かないといけないというのはどうもイケていない気がします。そこでふと思ったのが、

「もしかして、freeze.py に設定をちゃんと書けばそれで対応できるのでは…?」

※freeze.py というのは py2exe でexeを生成するときに使うセットアップ用のスクリプトです。

そこで、freeze.py の内容を見直してみました。

freeze.py
from py2exe import freeze

freeze(
    console=["main.py"], # exe化する対象のスクリプト
    options={
        "packages": ["charset_normalizer"],  # ←これで解決してしまった
    },
)

 なんと、これだけで解決してしまいました。packages パラメータに該当のパッケージを記載しておけばその配下にある.pydも自動的に取り込んでくれるようです。空振りの import md__mypyc などという意味不明の呪文は書かなくても、exeファイルが普通に正常動作するようになりました(もちろん本来そうあるべきです)。

今までの苦労はなんだったのか…

8. 最終まとめ

 ということで、今回の調査結果をまとめたいと思います。

(1) WebDriverManager 導入について

  • Selenium 用の Webドライバを自動更新するために WebDriverManager をインストールすると、インタプリタで使う分には3.(1)~(3)の対応をすれば普通に使える。適切なバージョンのWebドライバがなければ自動的にダウンロードしてきてくれるようになる。

(2) exe化した場合のみ ImportError が発生する問題について

  • py2exe を使って Python スクリプトを exe化して動かそうとするとスクリプトで動かしていたときには何も問題なかったのにexeではインポートエラーが発生することがある。(今回の場合は「ImportError: (No module named 'charset_normalizer.md__mypyc') 」というエラーが発生)
  • これは、使用しているパッケージの中にモジュールをバイナリ化して.pyd ファイルにしているものがある場合.pyd の中から暗黙的にインポートされているファイルを py2exe が検出できないことが原因。
  • 応急的な対処法として、Python スクリプトに明示的に、エラーになっているモジュール(今回の例ではmd__mypyc)をインポートした上で exe 化を行えば必要な.pyd がexeパッケージに取り込まれ正常動作するようになる。
  • しかしこの方法は一見ソース上で使っているように見えないモジュールをインポートするという意味不明なコードを書くことになり、一種のバッドノウハウである。また取り込めるのはそのモジュールだけなので、他にも同様のモジュールがある場合はすべて import を書く必要が生ずる。
  • より根本的には、freeze() のパラメータに渡すパッケージ情報にインポートエラーが起きている .pyd を含むパッケージを指定してやれば良い。

ということで、思った以上に長くなりましたが、ここまで読んでいただきありがとうございました。
なお、本記事には続編の記事があります。ご笑覧いただけましたらこれまた幸いです。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1