【実践】AI活用×ChatGPTで作る!TRIZ問題解決カスタムGPTの作り方

イデアを改良したい、でもどう考えればいいか分からない——そんな経験はありませんか?

自分の研究がそういったアイディエーションの支援型の領域に入ってきたので、教授からTRIZ(発明的問題解決理論)を紹介されました。これを調べるうえでどういうものなのかを自分でも体感するためにプロトタイプの作成を行ってみることにしました。

プロトタイプは、TRIZを使ったアイデア改良のカスタムGPT(GPTs)となります。 また、時短を目指すために設計、構築はLLMに依頼して作成を行うことにしています。

TRIZって何?

TRIZ(トゥリーズ)は、旧ソ連の発明家ゲンリッヒ・アルトシュラーが約40年かけて200万件以上の特許を分析し、体系化した創造的問題解決の手法です。

参考

ja.wikipedia.org

TRIZの考え方、「矛盾の解消」

多くの問題は「一方を良くすると、他方が悪くなる」というジレンマを抱えています。 矛盾というよりもトレードオフという言葉の方がわかりやすいかもしれません。

よくある矛盾の例

  • 強度を上げたい → 重くなってしまう
  • バッテリーを長持ちさせたい → サイズが大きくなる
  • セキュリティを強化したい → 使いにくくなる
  • 回転率を上げたい → 顧客満足度が下がる

TRIZでは、こうした矛盾を諦めるべきトレードオフではなく、解消できる課題として捉えています。

40の発明原理とは

更に、アルトシュラーは、優れた発明には共通のパターンがあることを発見しました。それを40の原理として整理したのが40の発明原理です。

代表的な原理の例

  • 分割:対象を独立した部分に分ける

    • 例)セクション家具、モジュラー設計、板チョコの割れ目
  • 入れ子:物を内部に収める

  • 逆発想:通常とは逆の動作を考える

    • 例)回転寿司(料理人が届けるのではなく、客が取りに行く)
  • 統合:複数の機能を1つにまとめる

これらの原理を組み合わせることで、創造的な解決策が生まれます。

なぜTRIZが有効なのか

従来の発想法はひらめき経験に頼りがちでした。しかし、TRIZは以下のことができるように工夫されています。

  • 体系的 … 200万件の特許から導き出された実証済みの方法
  • 再現可能 … 誰でも学んで使える
  • 実践的 … 製品開発からビジネスモデルまで幅広く応用可能

つまり、「天才のひらめき」ではなく、再現性の高い「学べる技術」として問題解決手法にしています。

なぜこのカスタムGPTを作ったのか

研究でどこまで使えるかわからないのに組み入れることはできないので、こちらに関しては施行も込めて作成しています。うまく使えるようであれば、実際に研究のシステムにも入れてみようかなと思います。

そのテストとして「TRIZ初心者向けのアイデア改良支援GPT」を作します。以下を目標にします。

1) TRIZを実践的に学ぶツールが欲しかった

  • 理論だけでなく、実際のアイデア改良を体験したい
  • 初心者でも気軽に使えるものを

2)カスタムGPT(MyGPT)の作り方を学びたかった * 実際に作ることで、設計のポイントを理解 * LLMを活用した効率的な開発プロセスを体験

LLMを活用したGPT設計

今回、カスタムGPTの設計をドキュメントの作成LLMにお願いしました。

作成物

  • Instructions(指示文) … GPTの役割、対話の進め方、出力フォーマットなど
  • 知識ベース … 40の発明原理の詳細解説、21の実践ケーススタディ

設定した要件

  • 対象ユーザーは一般の人(初心者向け)
  • 親しみやすいトーン(絵文字を使用)
  • Web Browsingで最新事例を検索
  • 実践的なアイディエーション体験

カスタムGPTの作成手順(実践編)

ここからは、実際にChatGPTでカスタムGPTを作る手順を詳しく説明します。

前提条件

  • ChatGPT PlusまたはEnterpriseのアカウントが必要です
  • 無料版では作成できないのでご注意ください

1. GPT作成画面を開く

ChatGPTにログインし、画面左側のサイドバーにある【GPT】の【調べる】をクリックします。

ウインドウの右上にある【+ 作成する】ボタンをクリック

すると、GPT作成画面が表示されます。

2. 基本情報を入力

画面上部の【構成】タブをクリックし、以下を入力します。

  • Name(名前) … TRIZ イノベーター
  • Description(説明) … TRIZの40の発明原理を活用して、あなたのアイデアを体系的に改良・強化します。TRIZ初心者でも分かりやすく、実際にアイディエーションを体験しながらTRIZの考え方を学べます。

3. 指示を設定

【指示】のテキストボックス欄に、GPTの動作を定義する詳細な指示文を入力します。

今回、LLMが作成してくれた指示文には以下が含まれています。

  • GPTの役割定義(TRIZ専門家、初心者にも優しい)
  • 対話の進め方(5つのステップ)
  • 出力フォーマット(絵文字を使った親しみやすい形式)
  • TRIZの基本知識の説明方法
  • 注意事項(専門用語は平易に、押し付けない姿勢)

以下のリポジトリのファイルを参照してください。

triz-innovator-gpt/instructions.md at main · ueponx/triz-innovator-gpt · GitHub

👉️指示は長くなりがちですが、具体的であればあるほど、カスタムGPTは期待通りに動作します。

4. 会話のきっかけを設定

会話のきっかけに、ユーザーがクリック一つで始められる質問を設定します。

新しいアイデアをTRIZで改良したい
製品やサービスの課題を解決したい
創造的な発想のヒントが欲しい
矛盾する要求をどう解決すればいいか相談したい

実際にはカスタムGPTの以下の部分に相当します。

初めてのユーザーは会話のきっかけのボタンを押すことでチャットを始めることができます。

5. 知識ベースをアップロード

知識セクションで、カスタムGPTが参照する情報となるファイルをアップロードします。

今回は以下の2ファイルを添付しました。

  1. 40の発明原理の詳細解説(各原理の説明、具体例、適用のヒント)
  2. 実践ケーススタディ(21の実例を分野別に整理)

これにより、カスタムGPTはより具体的で実践的な提案ができるようになります。

以下のリポジトリのファイルを参照してください。

https://github.com/ueponx/triz-innovator-gpt/tree/main/knowledge

6. 機能を設定

機能セクションでは使用する機能を選択します。

今回は以下のものを選択しました。

  • ウェブ検索 … ONにする(最新事例の検索用)
  • Canvas … キャンバス機能を有効化(資料作成もできるようにする)
  • 画像生成 … OFF(今回は不要)
  • コードインタプリターとデータ分析 … OFF(今回は不要)

7.テストと調整

画面右側のPreviewでテストします:

テスト例:
「スマートフォンのバッテリーを長持ちさせたいけど、サイズは大きくしたくありません」

期待通りの応答が返ってくるか確認し、必要に応じて指示を調整します。

8. 公開設定

画面右上の【作成する】ボタンをクリックし、公開範囲を選択します。

  • 自分だけ … 自分だけが使用
  • リンクを受け取った人 … リンクを知っている人が使用
  • GPTストア … 誰でも使用可能

今回、共有も目的にしていたので「リンクを受け取った人」で設定しています。

chatgpt.com

完成したGPTの使い心地

実際に使ってみると、こんな感じで相談をすることができます。

入力

レストランの回転率を上げたいけど、顧客満足度は下げたくない

今回のカスタムGPTの応答(抜粋)

なるほど!これは典型的な「技術的矛盾」ですね。

回転率を上げる(速く提供) → 顧客満足度が下がる(急かされる感じ)

このジレンマに使えるTRIZ原理がいくつかあります:

💡 原理#1(分割):作業を細かく分けて効率化
💡 原理#10(先取り作用):事前準備で時間短縮
💡 原理#20(有用作用の継続):待ち時間をなくす

具体的な改良案を3つ提案しますね...

初心者でも理解しやすく、すぐに実践できる提案が返ってきます🤗

おわりに

LLMの効果的な活用については普段から使っていますが、以下の点が再認識させられます。

  • 専門知識の体系的な整理
  • ドキュメントの自動生成
  • 設計の一貫性確保
  • 高速な試行錯誤が可能

カスタムGPT作成のコツ

作成後の実験の中で以下のような点がコツに繋がりそうです。

  • 具体的な要件を設定 … ターゲットユーザー、トーン、機能を明確に
  • 知識ベースの充実 … 具体例が豊富であるほど実用的

自分専用のAIアシスタントを作れるカスタムGPTは、アイデア次第ではありますが、かなり使えるようになると思います。今回はTRIZを使ったアイデア支援をさせてみましたが、学習支援、創造的な活動のサポートなど、様々な用途に活用できそうです。また、プログラミング的な知識がなくてもできるのも良い点ですよね🤩

また、TRIZという一見難しそうな内容でも、LLMの力を借りれば、初心者向けの分かりやすいツールを短時間で作れますね。

ソース

github.com

GitHub Codespacesが開けない!? 意外な原因はブラウザの翻訳機能?

今年も大学での非常勤の講義が始まりました。去年から開発環境はGitHub Codespaces(以下Codespaces)を使用しているのですが、本日トラブルが6件ほど頻発しました。Codespacesでdevコンテナを使っての環境構築したところ、環境構築後に、ブラウザで新規に開くページが開けなくなる問題に遭遇しました。9割以上の学生は問題なく完了していたのですが…。

何度リロードしても真っ白な画面のまま...。環境再構築をしてもうまくいかず…。しかし、あることに気づいて解決できたので、自分のための備忘録としてメモとして記録しておきます。

症状

  • GitHub Codespacesで新規に開くページの読み込みが行われない(タブは開くがポートErrorが発生)
  • 画面が真っ白、またはポート転送のローディング中のまま止まる

原因はブラウザの自動翻訳機能

環境の再構築をしても駄目でお手上げかなと思っていましたが、6名全てが翻訳機能を使用していることに気が付きました。そこで、ブラウザの翻訳機能をOFFにしたところ、正常に動作することを発見しました!

Chrome、Edge、Firefoxなどの主要ブラウザには、英語のページを自動的に日本語に翻訳する機能がありますが、この機能がCodespacesの動作を妨げていたのです。

なぜ翻訳機能が問題を起こすのか?

以下のようなことが考えられると思いますが、多分(その1)(その3)が該当するのでは無いかと思います。

その1. DOM構造の動的変更

ブラウザの翻訳機能は、Webページの構造(DOM)を書き換えて翻訳します。この書き換えがJavaScriptの動作に影響を与えます。

その2. WebSocketの初期化失敗

Codespacesはリアルタイム通信にWebSocketを使用していますが、翻訳によるDOM変更のタイミングで初期化処理が失敗してしまうことがあります。

その3. JavaScriptエラーの発生

翻訳機能がHTMLを変更した後、JavaScriptが元の要素を見つけられなくなり、エラーが発生することがあります。

解決方法

方法1 一時的に翻訳をオフにする

Chromeの場合であれば…

  1. アドレスバー右端の翻訳アイコンをクリック
  2. 「翻訳しない」を選択

方法2 特定のサイトを除外リストに追加

Codespacesなど、特定のサイトで今後翻訳ツールを表示させたくない場合は以下の操作をします。

Chromeの設定方法

1 GitHub Codespacesのページで翻訳ツールバーが表示されたら、右端の【︙】(三点リーダー)をクリック

2【このサイトは翻訳しない】を選択

これで今後、このサイト(ドメイン)では翻訳ツールが表示されなくなります。

翻訳ツールバーはページで右クリックして【日本語に翻訳】を選択すると表示されます。

同じ問題が起こりやすいサービス

検索したところ、この問題はCodespacesだけでなく、以下のような高度なWebアプリケーションでも起こる可能性があるようです。

  • Google Docs / Sheets / Slides
  • Figma
  • Notion
  • Miro
  • その他のSPA(Single Page Application)ベースのサービス

おわりに

GitHub Codespacesが開けないという問題の意外な原因は、ブラウザの自動翻訳機能でした。 以前からそういう話は聞いたことがあったのですが、原因による動作を考えると確かに発生するわなと思います。

翻訳機能は便利ですが、複雑なWebアプリケーションでは思わぬトラブルの原因になることがあります。 開発ツールを使う際は、念のため翻訳機能を無効にしておくことが良さそうですね。

Claude使用量の確認が面倒?フローティング表示で解決するChrome拡張

ClaudeのWeb版を使っていて、「あとどれくらい使えるんだろう?」 「使用量の上限に達していないかな?」と気になったことはありませんか?特に、MAXプランを使用している方からするとOpusの上限は結構気にするかもしれません。

Webの管理画面で確認はできます。

わざわざ設定ページに行くのは面倒だし…😨」

そこで、Claudeのどのページからでも使用量をサッと確認できるChrome拡張機能を作成しました。フローティングウィジェット形式で、画面上に常時表示されるので、いつでも使用状況が把握できます。

ソースコードは以下に公開しています。

github.com

機能

1. フローティングウィジェットで常時表示

Claude.aiのどのページを開いていても、画面の右上にフローティングウィジェットが自動表示されます。

わざわざ設定ページに移動しなくても、今の使用状況が一目でわかります!

2. 3つの使用量を同時表示

  • Current Session: 現在のセッションでの使用量
  • All Models: 週間制限の使用量(全モデル合計)
  • Opus Only: Opusモデルの使用量

それぞれにプログレスバーとリセット時間が表示されるので、計画的に使えます。

3. 便利な操作機能

  • ドラッグ移動: 好きな位置に配置可能
  • 折りたたみ: ボタンでコンパクト表示
  • 非表示: ×ボタンで一時的に非表示(ツールバーのアイコンクリックで再表示)
  • 自動更新: 5分ごとに自動で使用量を更新(使用量ページのみ)
  • 手動更新: 🔄ボタンで即座に更新

折りたたみの状態

表示位置も記憶されるので、次回も同じ場所に表示されます🫡

インストール方法

  1. リポジトリのクローン
  2. 拡張機能のZIPファイルをダウンロードして解凍
  3. Chromechrome://extensions/ を開く
  4. 右上の「デベロッパーモード」をON
  5. 「パッケージ化されていない拡張機能を読み込む」をクリック
  6. 解凍したclaude-usage-extensionフォルダを選択

これで完了です。ツールバーにアイコンが表示されます。リポジトリにあるアイコンファイルはお好きな画像に差し替えてください🙇

デザインとしてはON/OFFの表示と重なるので、微妙ですが😱

ファイル構成

claude-usage-extension/
├── manifest.json      # 拡張機能の設定
├── popup.html         # ポップアップUI
├── popup.js           # ポップアップのロジック
├── content.js         # ウィジェット表示ロジック
├── background.js      # バックグラウンド処理
├── styles.css         # スタイル
└── icons/             # アイコン画像(3サイズ)
    ├── icon16.png
    ├── icon48.png
    └── icon128.png
  • 更新頻度: 5分ごとの自動更新(カスタマイズ可能)
  • データ保存: ブラウザのローカルストレージのみ(外部送信なし)
  • 対応ページ: Claude.ai配下のすべてのページ

カスタマイズ

background.jsで自動更新の間隔を変更できます:

// 15分ごとに変更する場合
chrome.alarms.create('updateUsage', { periodInMinutes: 15 });

styles.cssで色やサイズも自由にカスタマイズ可能です。

おわりに

Claudeは気に入っているのですが、よく、「あとどれくらい使えるか」がよく不安になりながら使用していました。(ボソッGeminiではコンテキストが大きいのでこういうことは考えなくて良いのですけど)

この拡張機能により、ストレスなく使用量を確認しながらClaudeを活用できるようになりました。 特に、Opusのような強力なモデルを使う際に、残り使用量を気にしながら効率的に使えるのが便利です。

注意事項

  • 非公式の拡張機能です(Anthropic公式ではありません)
  • Claude.aiのページ構造が変更されると動作しなくなる可能性があります
  • データはローカルにのみ保存され、外部に送信されることはありません

もう「いつ聞いたっけ?」と迷わない!ClaudeのチャットのタイムスタンプをつけるChrome拡張機能

ClaudeのWeb版を使っていて、「いつ質問したか」 「会話一覧のチャット日時」がパッと見でわかりにくいと感じたことはありませんか?

何日前なんかで表示されてもなあ…😨」

そこで、Claudeのチャット画面とChatの履歴ページにタイムスタンプを表示するChrome拡張機能を作成しました。ChatGPTではこの機能を持ったChrome拡張機能は公開されています。

ソースコードは以下に公開しています。

github.com

機能

1. チャット画面の時刻表示

ユーザーの質問の上に、送信時刻を自動表示します。

使用前

使用後

表示形式: 2025/10/22 - 13:47:35

2. Recentsページの時刻表示

会話一覧の「○分前」などの相対時間を、具体的な日時に変換します。

使用前

使用後

  • 1 時間前2025/10/22 - 12:47:35
  • 3日前2025/10/19 - 13:47:35

元の表示はマウスをホバーすると確認できます。このあたりすこし表示バグあるかも🙇

開発時のトラブル

Recents(チャット履歴)ページの時刻変換が動かない

最初のバージョンでは、タイムスタンプのパースは成功していても、画面上の表示が変わりませんでした。

原因

正規表現が「1時間前」(スペースなし)にしか対応していなかった、実際には1と時間の間にスペースがありました。「1 時間前」(スペースあり)。スペースなんて気が付かないよ😨

解決策

正規表現\s* を追加し、スペースの有無に対応させました。

インストール方法

  1. リポジトリのクローン
  2. 拡張機能のZIPファイルをダウンロードして解凍
  3. Chromechrome://extensions/ を開く
  4. 右上の「デベロッパーモード」をON
  5. 「パッケージ化されていない拡張機能を読み込む」をクリック
  6. 解凍したフォルダを選択

イコン画

これで完了です。ツールバーにアイコンが表示されます。 リポジトリにあるアイコンファイルはお好きな画像に差し替えるとよいでしょう🫡

ファイル構成

claude-timestamp-extension/
├── manifest.json      # 拡張機能の設定
├── content.js         # メインロジック
├── styles.css         # スタイル
└── icons/             # アイコン画像(3サイズの画像を登録しています)
  • タイムスタンプ形式 … YYYY/MM/DD - HH:MM:SS(24時間表記)
  • 対応パターン … 日本語・英語の相対時間(秒前、分前、時間前、日前、週間前、ヶ月前)

おわりに

Web APIを使わず、純粋にフロントエンドだけでタイムスタンプ機能を実装できました。相対時間からの逆算は完璧ではありませんが、実用上は十分です。

Chrome拡張の開発は、思ったよりシンプルで、ユーザー体験の改善に直結するのが面白いですね。

Mac製ZIPファイルの文字化け問題(解)

先日、Mac製ZIPファイルの文字化け問題について書きましたが、その後さらに調べていたところ、根本的な解決方法につながる情報を見つけました。

その結果、前回の記事で書いた「WindowsUTF-8に対応していない」という理解は誤りだったことが判明しました🙇実際には、WindowsUTF-8対応した実装されていました。

文面が「Windows、まだShift_JISに依存しているのかよ~」というものになっていたかもしれません。謹んでお詫びいたします。マジスマン🙇

済まないと思うだけでは良くないので、メモとして残しておきます。

前回のおさらい

前回のブログでは、MacUTF-8(NFD)で作成されたZIPファイルがWindowsUbuntuで文字化けする問題について、以下のような解決方法を紹介しました。

uepon.hatenadiary.com

WindowsUTF-8に対応していないから」とシャーない感じで理解していましたが、誤解でした。正確には「Mac側がZIP仕様のEFSフラグを設定していないため」です。

新たな発見:EFSフラグの存在

以下のQiita記事を見つけました。

Windows エクスプローラー、7-Zip でファイル名を UTF-8 エンコードした ZIP ファイルを文字化けせずに解凍するためには ZIP ファイル内で EFS が有効にされている必要がある?

qiita.com

この記事を読んで、重要な事実がわかりました。

実は、WindowsエクスプローラーはちゃんとUTF-8に対応しているというのです!

参考エントリーにあるEFSフラグとは

ZIP仕様では、UTF-8を使う場合に設定すべきフラグが定義されています。それがEFS(Language encoding flag)です。

  • ビット11: Language encoding flag (EFS)
  • : 0x0800 (16進数)
  • 効果: このフラグが有効な場合、ファイル名とコメントはUTF-8エンコードされていると解釈される

Windowsエクスプローラーは、ZIP仕様通りにEFSフラグを見てUTF-8として処理しているのです。つまり、Windowsは仕様に対応している!

真の問題:Mac側がEFSフラグを設定していない

問題は、MacのZIPツールがUTF-8でファイル名をエンコードしているのに、EFSフラグを設定していないことでした。

これでは、Windowsエクスプローラーは「ZIPファイル内にEFSフラグが無い → UTF-8ではない → Shift_JISだろう」と(仕様に従って)判断してしまうため、文字化けが発生していたのです。

Mac側が仕様通りにEFSフラグを設定してくれていれば、問題は起きなかったということになります。

解決策:EFSフラグを有効化するプログラムを作成

この情報を元に、既存のZIPファイルのEFSフラグを有効化するPythonプログラムを準備しました。

プログラムの概要

GitHubにおいておきます。

github.com

fix_zip_utf8.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""ZIPファイルのEFS (Language encoding flag)を有効化するスクリプト.

UTF-8エンコードされたファイル名が文字化けしないようにフラグを設定します。
Windowsエクスプローラーや7-Zipで日本語ファイル名を正しく表示するために使用します。
"""

import struct
import sys
import argparse
from pathlib import Path
from typing import List, Tuple, Optional, Union


class ZipEFSEnabler:
    """ZIPファイルのEFSフラグを有効化するクラス.
    
    ZIP仕様(APPNOTE.TXT)で定義されているLanguage encoding flag (EFS)を
    ローカルファイルヘッダとセントラルディレクトリヘッダに設定します。
    
    Attributes:
        LOCAL_FILE_HEADER_SIGNATURE: ローカルファイルヘッダのシグネチャ (PK\x03\x04)
        CENTRAL_DIRECTORY_SIGNATURE: セントラルディレクトリヘッダのシグネチャ (PK\x01\x02)
        END_OF_CENTRAL_DIR_SIGNATURE: セントラルディレクトリ終端のシグネチャ (PK\x05\x06)
        EFS_FLAG: EFSフラグのビットマスク (0x0800 = ビット11)
        zip_path: 処理対象のZIPファイルパス
    """
    
    # ZIPファイルの各シグネチャ
    LOCAL_FILE_HEADER_SIGNATURE: bytes = b'PK\x03\x04'
    CENTRAL_DIRECTORY_SIGNATURE: bytes = b'PK\x01\x02'
    END_OF_CENTRAL_DIR_SIGNATURE: bytes = b'PK\x05\x06'
    
    # EFSフラグのビットマスク (ビット11)
    EFS_FLAG: int = 0x0800
    
    def __init__(self, zip_path: Union[str, Path]) -> None:
        """ZipEFSEnablerを初期化する.
        
        Args:
            zip_path: 処理対象のZIPファイルパス
            
        Raises:
            FileNotFoundError: 指定されたZIPファイルが存在しない場合
        """
        self.zip_path: Path = Path(zip_path)
        if not self.zip_path.exists():
            raise FileNotFoundError(f"ZIPファイルが見つかりません: {zip_path}")
    
    def enable_efs(self, output_path: Optional[Union[str, Path]] = None) -> Tuple[List[Tuple[str, int, int, int]], Path]:
        """ZIPファイルのEFSフラグを有効化する.
        
        ローカルファイルヘッダとセントラルディレクトリヘッダの汎用目的ビットフラグに
        EFSフラグ(ビット11)を設定します。これにより、ファイル名とコメントが
        UTF-8エンコードされていることを示します。
        
        Args:
            output_path: 出力ファイルパス。Noneの場合は元のファイルを上書きします。
            
        Returns:
            変更情報のリストと出力ファイルパスのタプル。
            変更情報は (ヘッダタイプ, オフセット, 元のフラグ, 新しいフラグ) の形式。
            
        Raises:
            IOError: ファイルの読み込みまたは書き込みに失敗した場合
        """
        # ZIPファイルを読み込み
        with open(self.zip_path, 'rb') as f:
            data: bytearray = bytearray(f.read())
        
        # 変更箇所を記録
        modifications: List[Tuple[str, int, int, int]] = []
        
        # ローカルファイルヘッダのフラグを更新
        offset: int = 0
        while offset < len(data) - 4:
            if data[offset:offset+4] == self.LOCAL_FILE_HEADER_SIGNATURE:
                flag_offset: int = offset + 6
                current_flag: int = struct.unpack('<H', data[flag_offset:flag_offset+2])[0]
                new_flag: int = current_flag | self.EFS_FLAG
                
                # フラグが変更される場合のみ記録
                if current_flag != new_flag:
                    struct.pack_into('<H', data, flag_offset, new_flag)
                    modifications.append(('ローカルファイルヘッダ', offset, current_flag, new_flag))
                
                # 次のヘッダへスキップ(最小サイズ分)
                offset += 30
            else:
                offset += 1
        
        # セントラルディレクトリヘッダのフラグを更新
        offset = 0
        while offset < len(data) - 4:
            if data[offset:offset+4] == self.CENTRAL_DIRECTORY_SIGNATURE:
                flag_offset = offset + 8
                current_flag = struct.unpack('<H', data[flag_offset:flag_offset+2])[0]
                new_flag = current_flag | self.EFS_FLAG
                
                # フラグが変更される場合のみ記録
                if current_flag != new_flag:
                    struct.pack_into('<H', data, flag_offset, new_flag)
                    modifications.append(('セントラルディレクトリヘッダ', offset, current_flag, new_flag))
                
                # 次のヘッダへスキップ(最小サイズ分)
                offset += 46
            else:
                offset += 1
        
        # 出力パスの決定
        if output_path is None:
            output_path = self.zip_path
        else:
            output_path = Path(output_path)
        
        # ファイルに書き込み
        with open(output_path, 'wb') as f:
            f.write(data)
        
        return modifications, output_path


def main() -> None:
    """メイン処理を実行する.
    
    コマンドライン引数を解析し、ZIPファイルのEFSフラグを有効化します。
    デフォルトでは新しいファイルを作成し、--overwriteオプションで
    元のファイルを上書きすることができます。
    
    Raises:
        SystemExit: エラー発生時または処理中断時
    """
    parser = argparse.ArgumentParser(
        description='ZIPファイルのEFSフラグを有効化して、UTF-8ファイル名の文字化けを防ぎます。',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
使用例:
  # 新しいファイルを作成(デフォルト: 元のファイル名_fixed.zip)
  python fix_zip_utf8.py input.zip

  # 出力ファイル名を指定
  python fix_zip_utf8.py input.zip -o output.zip

  # 詳細な変更情報を表示
  python fix_zip_utf8.py input.zip -v

  # 元のファイルを上書き(注意: バックアップを取ることを推奨)
  python fix_zip_utf8.py input.zip --overwrite
        '''
    )
    
    parser.add_argument('input', help='入力ZIPファイルパス')
    parser.add_argument('-o', '--output', help='出力ZIPファイルパス(指定しない場合は自動生成)')
    parser.add_argument('--overwrite', action='store_true', 
                       help='元のファイルを上書きする(デフォルトでは新しいファイルを作成)')
    parser.add_argument('-v', '--verbose', action='store_true',
                       help='詳細な変更情報を表示する')
    
    args = parser.parse_args()
    
    try:
        input_path: Path = Path(args.input)
        
        # 出力パスの決定
        if args.overwrite:
            output_path: Path = input_path
            print("⚠ 警告: 元のファイルを上書きします")
        elif args.output:
            output_path = Path(args.output)
        else:
            # デフォルト: 元のファイル名に _fixed を付ける
            stem: str = input_path.stem
            suffix: str = input_path.suffix
            output_path = input_path.parent / f"{stem}_fixed{suffix}"
        
        # 出力ファイルが既に存在する場合の確認(上書きモード以外)
        if not args.overwrite and output_path.exists():
            response: str = input(f"ファイル '{output_path}' は既に存在します。上書きしますか? [y/N]: ")
            if response.lower() not in ['y', 'yes']:
                print("処理を中止しました。")
                sys.exit(0)
        
        enabler: ZipEFSEnabler = ZipEFSEnabler(input_path)
        modifications: List[Tuple[str, int, int, int]]
        modifications, output_path = enabler.enable_efs(output_path)
        
        print(f"✓ EFSフラグを有効化しました: {output_path}")
        print()
        
        if modifications:
            # 変更箇所をカテゴリごとに集計
            local_count: int = sum(1 for m in modifications if m[0] == 'ローカルファイルヘッダ')
            central_count: int = sum(1 for m in modifications if m[0] == 'セントラルディレクトリヘッダ')
            
            print(f"変更箇所: {len(modifications)}件")
            if local_count > 0:
                print(f"  - ローカルファイルヘッダ: {local_count}件")
            if central_count > 0:
                print(f"  - セントラルディレクトリヘッダ: {central_count}件")
            
            # 詳細モードの場合は個別の変更情報も表示
            if args.verbose:
                print()
                print("詳細:")
                for header_type, offset, old_flag, new_flag in modifications:
                    print(f"  - {header_type} @ 0x{offset:08X}")
                    print(f"    0x{old_flag:04X} → 0x{new_flag:04X}")
        else:
            print("変更箇所: なし(すでにEFSフラグが有効でした)")
        
    except KeyboardInterrupt:
        print("\n処理を中断しました。")
        sys.exit(1)
    except Exception as e:
        print(f"エラー: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()

基本的な使い方

# 新しいファイルを作成(input_fixed.zip)
$ python fix_zip_utf8.py input.zip

# 出力ファイル名を指定
$ python fix_zip_utf8.py input.zip -o output.zip

# 詳細な変更情報を表示
$ python fix_zip_utf8.py input.zip -v

実行例

$ python fix_zip_utf8.py mac_archive.zip
✓ EFSフラグを有効化しました: mac_archive_fixed.zip

変更箇所: 12件
  - ローカルファイルヘッダ: 6件
  - セントラルディレクトリヘッダ: 6件

このプログラムで処理したZIPファイルは、ZIP仕様に準拠した形になるため、Windowsエクスプローラーでも日本語ファイル名が正しく表示されるようになります。

ZIPファイルの技術的な構造の補足

ZIPファイルは、各ファイルごとに以下の2つのヘッダを持っています。

  1. ローカルファイルヘッダ … 各ファイルの実データの直前にある
  2. セントラルディレクトリヘッダ … ZIPファイルの最後にまとめて配置される

そのため、ZIPファイル内のファイル数 × 2 箇所のフラグを変更する必要があります。

フラグの位置

  • ローカルファイルヘッダ: オフセット+6の汎用目的ビットフラグ
  • セントラルディレクトリヘッダ: オフセット+8の汎用目的ビットフラグ

それぞれのフラグに 0x0800 をOR演算で設定します。

参考にしたブログにも書かれているので、詳細はそちらもご確認ください。

おわりに

NKT(長く苦しい戦いだった)

前回は「なぜ文字化けするのか」と「対処療法」について書きましたが、今回は「真の原因」と「根本的な解決方法」を見つけることができました。

そして何より、前回「Windowsに修正してほしい」と書いたのは完全な誤解でしたね。Windowsエクスプローラーもそれに従って正しく実装されていました

誤解して申し訳ございませんでした🙏これ修正される日はくるのかな?

参考リンク

以前書いたエントリ

uepon.hatenadiary.com

Windows エクスプローラー、7-Zip でファイル名を UTF-8 エンコードした ZIP ファイルを文字化けせずに解凍するためには ZIP ファイル内で EFS が有効にされている必要がある?

qiita.com

ZIPファイルの仕様

ZIP仕様書(APPNOTE.TXT)