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初期化処理のソースは以下の通りです。
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 にアップグレードするときに注意が必要な点として、初期化以外の部分でも既存のメソッドの仕様が微妙に変わっていることがあります。フレームの切り替え処理で変更がありました。
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 している箇所で、該当行のソースは下記の部分です。
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 行目は下記の通りです。
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 文を追加してみました。
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 コマンドで確認してみました。
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を作り直して実行すると…
# 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 ファイルが必要なのではないかと考えられます。確認してみます。
ご存知のように、.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 でしょう。
(user)/AppData/Local/Programs/Python/Python310/python310.dll
公式リポジトリからPyImport_ImportModule() のソースを探してみました。おそらくこの辺りでしょうか(2023.6.14時点)。
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のインポート周りの実装を読んでみるのも面白そうです。
また、セクション情報の中のデータを眺めるとこんな箇所がありました。
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
#!/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++ も必要になります。コミュニティ版なら無料で入手可能)
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 が出来ているのがわかります。
(4) 生成された拡張C言語モジュールを確認する
md.py から生成されたC言語のソースが build/charset_normalizer フォルダにありましたので見てみましょう。
(4-a) 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() があるか確認してみましょう。
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 の内容を見直してみました。
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 を含むパッケージを指定してやれば良い。
ということで、思った以上に長くなりましたが、ここまで読んでいただきありがとうございました。
なお、本記事には続編の記事があります。ご笑覧いただけましたらこれまた幸いです。