pyinstallerで作ったexeを、もう一度pyファイルに戻したくなることってありますよね
元のソースをうっかり消してしまった、手元にexeしか残っていない、中身をもう一度確認したい、そんな場面です
結論から言うと、pyinstallerのexeをpyに戻す(デコンパイルする)のは可能です、ただし完全には戻りません、欠損やクセが出るので「参考ソースが取り出せるくらい」が現実的なゴールになります
私自身、戻す方法をいろいろ調べたんですが、なかなかうまくいくやり方に出会えませんでした、その中で唯一なんとかなった手順を、つまずいたポイントも含めて書き残しておきます
exeをpyに戻す全体の流れ(STEP1〜3)
最初に全体像を出しておきます、やることは大きく3ステップです
- STEP1:pyinstxtractorでexeを展開して、中身の
.pycを取り出す - STEP2:取り出した
.pyc(元のスクリプトに対応するもの)を見つける - STEP3:pycdcで
.pycを.pyに逆コンパイルする
イメージとしては、exeを開いて中の部品(.pyc)を取り出し、その部品を読める形(.py)に翻訳し直す感じです
ここで出てくる2つの用語だけ先に押さえておきます
.pyc:Pythonが内部で使う中間ファイル(バイトコード)、人間が読むには向かない形式です- デコンパイル(逆コンパイル):その
.pycから、読める.pyのソースを推測して組み立て直す処理です
では各ステップを順番に見ていきます
この記事の前提(PyInstaller製のexeが対象)
手順に入る前に、対象になるexeの前提を整理しておきます
この記事の手順が効くのは PyInstallerで作られたexeです、同じPython製でも py2exe や cx_Freeze、Nuitka などで作ったexeは中身の作りが違うので、ここでのやり方は当てはまりません
自分が作ったexeなら、何で固めたかは把握しているはずなので迷わないと思います
あと用途についても先に触れておきます、ここで紹介するのは 自分が作ったexeのソースを取り戻すのが主な使い道です
他人が配布しているexeを解析する用途だと、後で書くとおり復元の精度も期待しにくいですし、ソフトの利用規約で逆コンパイルが禁じられているケースもあります、自分の作ったものを戻す前提で読んでもらえると安心です
STEP1|pyinstxtractorでexeを展開する
まずはexeの中身を展開して、.pyc を取り出すところから始めます、ここで使うのが pyinstxtractorというツールです
大元のリポジトリはこちらです
https://github.com/extremecoders-re/pyinstxtractor
本来は pyinstxtractor.py に、戻したいexeを引数で渡して実行する使い方です、なんですがこれだとPython環境を整える手間がかかります
上のページの下部に派生版へのリンクがあって、そちらの方がずっと手軽に使えるので、この記事ではそちらで進めます、派生版は次の2つです
https://github.com/extremecoders-re/pyinstxtractor
- pyinstxtractor-ng: A standalone binary version of pyinstxtractor. This tool doesn’t require Python to run and can extract all supported versions of PyInstaller. It also supports encrypted pyinstaller executables.
- pyinstxtractor-web: pyinstxtractor running in the web browser, powered by Go & GopherJS.
ざっくり違いを言うと、pyinstxtractor-ng はexe単体(Python不要)で、pyinstxtractor-web はブラウザ上で完結します
どちらでもやることは同じです、両方のやり方を順に紹介しますが、先に結論を言うと 更新が続いている-ng(exe版)のほうが安心です、その理由はweb版のところで触れます
pyinstxtractor-ng の場合(exeにドラッグ&ドロップ)
個人的にはこちらが一番ラクです、Pythonを入れなくても動く単体のexeなので、環境構築でつまずく心配がありません
リンク先のGithubページを開くと、目当てのファイルが見当たらないんですよね、ここで最初につまずきました
実際のダウンロードは、もう一つ先の releasesページにあります、↓の流れで遷移してください


最新版のところから「pyinstxtractor-ng.exe」をダウンロードしてきます
pyinstxtractor-ng.exeの使い方
使い方はとても簡単です、戻したいexeファイルを「pyinstxtractor-ng.exe」のアイコンへ ドラッグ&ドロップするだけです

実行すると {exeファイル名}_extracted というフォルダが作られます、中にはフォルダやファイルがたくさん入っていてちょっと面食らうんですが、見るべきものは決まっています
その中に 拡張子が.pycで、元のexeと同じ名前のファイルがあるはずです、これがSTEP2で確認する本命のファイルになります
pyinstxtractor-web の場合(ブラウザだけで完結)
exeをダウンロードするのにちょっと抵抗がある、という人にはブラウザ版が向いています
ブラウザ版は pyinstxtractor-web.netlify.app で公開されていて、インストール不要、このページに戻したいexeを渡すだけです
ただ注意したいのが、このweb版はコード自体の更新が2023年で止まっている点です、私が試した時点では動きましたが、メンテナンスが続いていない以上、新しいPythonやPyInstallerで作ったexeでは正常に処理できない可能性があります
web版でうまくいかないときは、更新が続いている pyinstxtractor-ng(exe版)のほうを使うのが確実です、迷ったら最初から-ngでもいいと思います
リンクの行き先が少しややこしくて、”pyinstxtractor-web” という名前なのにGo言語で書かれたGithubページに飛びます、上のnetlifyのリンクから直接開くのが手っ取り早いです、ちなみに私はGoはまったく分かりません
GithubページからWEBツールへ行く場合は、ページの少し下にあるリンクをたどります

行き着く先は、pyinstxtractor-ng.exeと同じことができるWEBページです


やることはシンプルです
- “ファイルを選択” から対象のexeファイルを選ぶ
- “Process” をクリックする
- 中央の枠にログが出て、最終的に
{exeファイル名}_extracted.zipがダウンロードされる
ダウンロードしたzipを解凍すれば、あとは pyinstxtractor-ng のときと同じです、中にある .pyc ファイルがSTEP2の対象になります
ブラウザ版はファイルがサーバーに送られるわけではなく、手元のブラウザ内で処理される作りなので、その点でも気軽に使えると思います
STEP2|取り出した.pycファイルを確認する
展開できたら、逆コンパイルにかける .pyc を見つけます
狙うのは 元のexeと同じ名前で、拡張子が.pycになっているファイルです、これが自分の書いたメインスクリプトに対応します
展開フォルダの中には、PyInstallerが裏で使っている部品(pyiboot01_bootstrap.pyc みたいな名前のもの)もたくさん入っています、これらはアプリを動かすための仕組みなので、自分のソースを戻したいだけなら基本スルーでOKです
自作のプログラムを複数ファイルに分けていた場合は、そのファイル名に対応する .pyc も同じフォルダに並んでいるはずです、必要なものを順番に逆コンパイルしていく形になります
ちなみに展開された .pyc は、ヘッダ部分が自動で補正されていて、後で使う逆コンパイラがそのまま読み込める状態になっています、ここを手動でいじる必要はないので安心してください
STEP3|pycdcで.pycを.pyに逆コンパイルする
いよいよ .pyc を読める .py に戻します、ここで使うのが pycdcという逆コンパイラです
Pythonの逆コンパイラには uncompyle6 など他のものもあるんですが、こちらは新しめのPythonに対応しきれていません、その点 pycdc は比較的新しいバージョンまで扱えるので、まずはこれを試すのがおすすめです
pycdc もビルド済みのexeが配布されているので、再びGithubへ向かいます
https://github.com/extremecoders-re/decompyle-builds

リポジトリ名が pycdc そのものではないので一瞬迷いますが、ここで合っています
pyinstxtractor-ng のときと同じく、releasesのリンクへ進みます

ここから「pycdc.exe」をダウンロードしてきます
pycdcの使い方(コマンドで実行)
pyinstxtractor-ng.exe と違って、pycdc はドラッグ&ドロップでは使えません
コマンドプロンプトやPowerShellから、対象の .pyc ファイルを引数にして、出力先を指定して実行する形です

コマンドはこんな感じです
cd {ファイルがあるフォルダ}
pycdc.exe {pycファイル}>{出力するpyファイル}
※2行目の各ファイルをフォルダ込みのフルパスで書くなら1行目の"cd {ファイルがあるフォルダ}"は不要> はログ(逆コンパイル結果)をファイルに書き出す指定です、pycdc は結果を画面に出力する作りなので、それをそのまま .py ファイルへ流し込んでいる、というイメージです
この出力された .py ファイルが、exeから戻した目的のソースになります、ここまでで一通りの手順は完了です
黒い画面でコマンドを打つのが苦手なら、この2行をそのままClaudeのようなAIに渡して「自分の環境のパスに置き換えて」とお願いするやり方もあります、そのあたりが不安な人はClaudeの使い方を初心者向けに解説した記事も合わせてどうぞ
復元したpyは元とどれくらい違うのか
ここがこの記事で一番伝えたいところです
冒頭で「完全には戻らない」と書いたとおり、出力された .py は 元のソースと完全には一致しません、実際に元ファイルとdiff(差分)を取って、どこがどう変わるのかを確認してみました
細かい違いは他にもありますが、影響が大きそうなものを5つ挙げます
1. “#WARNING: Decompyle incomplete” 以下が消える
これが一番ダメージが大きいです
逆コンパイルの途中でpycdcが解釈をあきらめると、出力に #WARNING: Decompyle incomplete というコメントが入ります、このコメントが出ると、同じ関数(def)の中のそれ以降のソースが丸ごと消し飛びます
diffを取るとこんな感じで、関数の後半がそっくり抜け落ちているのが分かります

このパターンが起きやすいのは、逆コンパイラが対応しきれていない新しめのPythonバージョンで作ったexeのときです、複雑な構文ほどここで引っかかりやすい印象でした
2. 一行コメント(#)が消える
消えてしまうのは # で始まる一行コメントです
コメントはコンパイルの段階で捨てられてしまうので、ここは仕組み上どうしようもない部分ですね
おもしろいことに、「”’」や「”””」で囲んだ複数行コメント(docstring)は残っています、関数やクラスの説明をdocstringで書いておくと、復元後も情報が残るということになります
3. 引数が「**kwargs」の形に化けやすい
キーワード引数(引数名=値 の形)で呼び出している部分が、**kwargs のような形に変わってしまうことがあります
これは見た目はかなり変わるものの、処理としては動きそうなので見逃せる範囲かなと思います
# ↓これが
f = csv.DictReader(csv_file, delimiter=',', doublequote=True,
lineterminator='\r\n', quotechar='"', skipinitialspace=True)
# ↓こうなる
f = csv.DictReader(csv_file, ',', True, '\r\n', '"', True, **('delimiter', 'doublequote', 'lineterminator', 'quotechar', 'skipinitialspace'))4. 型ヒント付き引数の解釈がおかしくなる
SellerId: str のように型ヒントを付けた引数があると、その解釈が崩れて変な形になることがあります
下の例だと、引数のデフォルト値に型名がタプルで紛れ込んでいて、そのままだと動かない状態になっています、型指定を付けなければ起きにくいのかな、というのが今のところの感触です
# ↓これが
def findItemsAdvancedAPI(self, SellerId: str, page_no: int, ItemIdList: list):
# ↓こうなる
def findItemsAdvancedAPI(self = None, SellerId = None, page_no = None, ItemIdList = ('SellerId', str, 'page_no', int, 'ItemIdList', list)):5. ネストしたifがまとめられてしまう
入れ子になった if が、and で1つにまとめられてしまうことがあります
条件だけ見れば近い意味になりますが、else や for との組み合わせ次第では 元と挙動が変わってしまう箇所も出てきます、元のソースが少し込み入っていたのもありますが、ここは目視で直す前提で見ておいた方が安全です
# ↓これが
if res['findItemsAdvancedResponse']['searchResult']['@count'] == '1':
item = items
if item['globalId'] == 'EBAY-US':
if str(item['itemId']) not in ItemIdList:
if self.GetItemDetail(str(item)) == 2:
return 2
else:
for item in items:
if item['globalId'] == 'EBAY-US':
if str(item['itemId']) not in ItemIdList:
if self.GetItemDetail(str(item['itemId'])) == 2:
return 2
return 1 if totalPages > page_no else 0
# ↓こうなる
if res['findItemsAdvancedResponse']['searchResult']['@count'] == '1':
item = items
if item['globalId'] == 'EBAY-US' and str(item['itemId']) not in ItemIdList and self.GetItemDetail(str(item)) == 2:
return 2
for item in items:
if item['globalId'] == 'EBAY-US' and str(item['itemId']) not in ItemIdList and self.GetItemDetail(str(item['itemId'])) == 2:
return 2
if totalPages > page_no:
return 1
return Noneこうして並べてみると、そのままコピペで動くソースが手に入るわけではないのが分かってもらえると思います、あくまで「元のロジックを思い出すための参考資料」として捉えるのがちょうど良い距離感です
うまくいかないとき・注意点
手順どおりやっても詰まることがあるので、つまずきやすいポイントをまとめておきます
PyInstaller製以外のexeでは使えない
前提のところでも書きましたが、ここは大事なので改めて触れておきます、この手順は PyInstallerで固めたexe専用です
pyinstxtractorで展開しようとしてエラーになる、あるいは .pyc が見当たらない場合は、そもそもPyInstaller製じゃない可能性があります、その場合は別のツールを探すことになります
新しいPythonで作ったexeは欠損しやすい
pycdc は新しめのPythonにもある程度対応していますが、最新のバージョンほど逆コンパイルが追いついていない部分があります
この記事を更新している2026年5月時点でいうと、展開のほう(STEP1のpyinstxtractor-ng)は今も活発に更新されていて、新しめのPyInstaller(6系)で固めたexeでも問題なく中身を取り出せます
引っかかりやすいのは逆コンパイル側のpycdc(STEP3)のほうで、Python 3.13で作られたexeにはまだ対応できていません、対応を求めるissueは今もオープンのまま動いている状況です
確実に言えるのは「3.13は現状まだ未対応」というところまでで、それより前のバージョンが戻るかどうかはコードの中身次第になります
なので自分で作ったexeを戻したいなら、ほんの少し枯れたPythonバージョンのほうが戻しやすい傾向がありますね
展開した .pyc がどのPythonバージョン向けかは、先に確認しておくと余計な遠回りを避けられます、欠損が多いときはバージョンの問題を疑ってみてください
暗号化されたexeは別の壁がある
PyInstallerには、中身のバイトコードを暗号化するオプションが用意されていた時期がありました、暗号化されたexeだと、展開しても .pyc がそのままでは読めません
pyinstxtractor-ng は暗号化されたexeにも対応をうたっていますが、復号には作成時の鍵が絡むケースもあります、自分で作ったexeなら設定を把握しているはずなので、暗号化オプションを使ったかどうかを思い出してみてください
完全復元はできない前提で使う
ここまで見てきたとおり、デコンパイルはあくまで 元のソースを推測して組み立て直す処理です、コメントや細かい書き方は失われますし、関数によっては中身が欠ける場合もあります
なので「exeさえあれば完璧に元通り」とは思わず、普段からソースは別に保管しておくのが何よりの対策です、復元はあくまで保険、くらいに考えておくと気がラクだと思います
AIで補完する新しい選択肢も出てきている
さっき書いたとおり、pycdcだと新しいバージョンで歯が立たないことがありますね
そこで最近は、逆コンパイルで欠けた部分をローカルのAI(LLM)に補完させるという新しいアプローチのツールも登場しています
たとえば ByteCodeLLM というツールがそれで、CyberArkが公開しているオープンソースです
pycdcやpycdasで展開した結果の欠損部分をLLMに埋めさせる仕組みになっていて、pycdcだけだと厳しいPython 3.13もローカルで復元できるとうたっています
気になる方は ByteCodeLLMのGitHub をのぞいてみるとイメージがつかめると思います
セットアップの手順までは今回は踏み込みませんが、こういう新しいやり方も出てきているくらいに頭の片隅に置いておくといいかもしれません
まとめ|exe→py復元の現実的なゴール
長くなったので要点を整理します
- pyinstallerのexeは pyinstxtractorで展開 → pycdcで逆コンパイルの流れでpyに戻せる
- pyinstxtractorは -ng(exe単体)と -web(ブラウザ)の2通り、web版は更新が止まっているので迷ったら-ngが確実
- 戻すのは元のexeと同じ名前の
.pyc、これをpycdcにかける - 復元したpyは 完全には一致しない(WARNING以降の欠損・コメント消失・引数の崩れ・if結合など)
- そのまま動くソースではなく 「ロジックを思い出すための参考資料」として使うのが現実的
何もないよりはずっと助かりますが、これで「完全復元」と言うにはやっぱり少し物足りないのが正直なところです
裏を返すと、人様が作ったexeを逆コンパイルして改修するのはなかなか難しい、ということでもありますね、自分のソースは別途バックアップしておくのが一番です
復元したコードの中身を読み解いたり、欠損した部分を書き直したりする作業は、AIに手伝ってもらうとだいぶ進めやすくなります、Pythonでのデータ加工つながりだとVBAでJSONを扱う方法をまとめた記事も書いているので、データ整形まわりで困ったら覗いてみてください



コメント