replace_recursive.py 技術ドキュメント

プログラムの動作

replace_recursive.py は、指定されたソースディレクトリツリー内のファイルを再帰的に走査し、ターゲットディレクトリへコピーするPythonスクリプトです。この処理中に、特定のファイルに対して定義済みの正規表現に基づいた文字列置換を実行します。

本プログラムの主な機能は以下の通りです。

  1. ファイルの再帰的コピー: source_dir で指定されたディレクトリ内のすべてのファイルとサブディレクトリを、target_dir へ構造を保ったままコピーします。

  2. 条件付き文字列置換: convert_file_extensions リストに含まれる拡張子を持つファイルに対して、replace_list に定義された正規表現パターンによる文字列置換を適用します。

  3. 置換除外リスト: exclude_list に含まれるファイル名を持つファイルは、たとえ置換対象の拡張子であっても文字列置換をスキップし、そのままターゲットディレクトリへコピーされます。

  4. エンコーディング自動検出: テキストファイルを読み込む際に chardet ライブラリを使用してファイルのエンコーディングを自動検出し、文字化けを防ぎます。

  5. ディレクトリの自動作成: コピー先のディレクトリが存在しない場合、必要に応じて自動的に作成されます。

このスクリプトは、ウェブサイトのパス構造を変更する際や、開発環境と本番環境で異なるURLやパスを使用する場合など、ディレクトリ構造を維持しつつ一括でファイルの内容を修正する必要があるシナリオで特に有用です。

原理

replace_recursive.py は、以下のアルゴリズムと技術を組み合わせて動作します。

  1. ファイルシステム走査: os.walk() 関数を使用して、source_dir から始まるディレクトリツリー全体を再帰的に走査します。この関数は、各ディレクトリのパス、そのディレクトリ内のサブディレクトリのリスト、およびファイル名のリストを生成します。

  2. 相対パスの計算: os.path.relpath() を使用して、ソースファイルパスから source_dir を基準とした相対パスを計算します。これにより、ターゲットディレクトリでも同じ相対パス構造を維持したままファイルを配置できます。

  3. ファイル操作:

    • os.makedirs(path, exist_ok=True): ターゲットディレクトリが存在しない場合に作成します。exist_ok=True オプションにより、既にディレクトリが存在する場合でもエラーになりません。

    • shutil.copy2(source, destination): ソースファイルをターゲットにコピーします。この関数は、ファイルのデータだけでなく、メタデータ(パーミッション、最終アクセス時刻、最終更新時刻など)も保持してコピーします。

  4. エンコーディング検出: chardet.detect() 関数は、ファイルのバイト列を分析して、そのエンコーディングを推測します。これにより、多種多様なエンコーディングで保存されたテキストファイルを正しく読み込むことができます。ファイルの内容を読み込む際は、検出されたエンコーディングを使用し、書き込む際は一貫してUTF-8エンコーディングを使用します。

  5. 正規表現置換: re モジュールの re.sub(pattern, repl, string) 関数を使用して、指定された正規表現パターンに一致する部分を置換文字列に置き換えます。 replace_list 内の各置換ルール \([P_i, R_i]\) は、以下の順序でテキスト \(T\) に適用されます。

    \[T_{new} = \text{re.sub}(P_1, R_1, T)\]
    \[T_{new} = \text{re.sub}(P_2, R_2, T_{new})\]
    \[\vdots\]
    \[T_{final} = \text{re.sub}(P_n, R_n, T_{new})\]

    ここで、\(P_i\) は正規表現パターン、\(R_i\) は置換文字列です。これにより、複数の置換ルールを順番に適用し、複雑な文字列変換を実現します。

  6. 条件分岐ロジック: ファイルの処理は、以下の優先順位で決定されます。

    • ファイル名が exclude_list に含まれる場合: 置換をスキップし、単純にコピーします。

    • ファイル拡張子が convert_file_extensions に含まれる場合: エンコーディングを検出し、内容を読み込み、正規表現置換を適用してからターゲットファイルに書き込みます。

    • 上記以外の場合: 単純にコピーします。

必要な非標準ライブラリとインストール方法

replace_recursive.py の実行には、標準ライブラリ以外に以下のライブラリが必要です。

  • chardet: ファイルのエンコーディングを自動検出するために使用されます。

インストールは pip コマンドを使用します。

pip install chardet

必要な入力ファイル

本プログラムは、実行時に直接的なコマンドライン引数としてファイルを受け取りませんが、プログラム内部で定義されたグローバル変数によって、以下の情報が設定されている必要があります。

  • source_dir: 処理対象となるディレクトリのパス(文字列)。このディレクトリ以下にあるすべてのファイルとサブディレクトリが走査されます。

    • 例: 'msl'

  • target_dir: 処理結果(コピーおよび置換後のファイル)が保存されるディレクトリのパス(文字列)。

    • 例: 'msl_titech'

  • convert_file_extensions: 文字列置換の対象となるファイルの拡張子のリスト(文字列のリスト)。このリストに含まれる拡張子を持つファイルのみが置換処理の候補となります。

    • 例: ['.html', '.css', '.js']

  • exclude_list: 文字列置換処理から除外されるファイル名のリスト(文字列のリスト)。このリストに含まれるファイルは、convert_file_extensions に一致していても置換されずにそのままコピーされます。

    • 例: ['500.html']

  • replace_list: 実行される正規表現置換ルールを定義したリスト(文字列ペアのリスト)。各ペアは [正規表現パターン, 置換文字列] の形式で構成されます。リストの順序で置換が適用されます。

    • 例:

      replace_list = [
          [r'https:\/\/www\.msl\.titech\.ac\.jp\/', '/msl_titech/'],
          [r'^\.\/assets\/', '/msl_titech/assets/'],
          [r'^\/assets\/', '/msl_titech/assets/'],
          [r'file:\/\/\/E:\/\/www\.msl\.titech\.ac\.jp\/', '/msl_titech/'],
      ]
      

これらの変数は、プログラムのソースコードを直接編集することで設定します。

生成される出力ファイル

replace_recursive.py は、以下の出力ファイルを生成します。

  • ターゲットディレクトリ: target_dir で指定されたパスにディレクトリが作成されます(例: msl_titech)。このディレクトリは、source_dir の内容をミラーリングした構造を持ちます。

  • ファイル: target_dir 以下に、source_dir のファイルがコピーされます。

    • convert_file_extensions に含まれ、かつ exclude_list に含まれないファイルについては、replace_list に定義された正規表現置換が適用された後の内容で保存されます。これらのファイルはUTF-8エンコーディングで書き出されます。

    • 上記以外のファイル(convert_file_extensions に含まれないファイル、または exclude_list に含まれるファイル)は、元の内容がそのままコピーされます。メタデータも shutil.copy2 により保持されます。

出力ディレクトリの例:

target_dir/
├── index.html                  (置換済み)
├── assets/
│   ├── style.css               (置換済み)
│   ├── script.js               (置換済み)
│   └── image.png               (そのままコピー)
└── excluded_file_name.html     (置換除外リストにあるため、そのままコピー)

コマンドラインでの使用例 (Usage)

replace_recursive.py は、コマンドライン引数を直接受け取らず、プログラム内部のグローバル変数で設定されたパスと置換ルールに基づいて動作します。そのため、実行時には引数を指定する必要はありません。

プログラムの実行は、以下のコマンドで行います。

python replace_recursive.py

コマンドラインでの具体的な使用例

replace_recursive.py を実行する前に、プログラム内のグローバル変数を適切に設定する必要があります。

replace_recursive.py の設定例:

source_dir = 'my_website_source'
target_dir = 'my_website_output'
convert_file_extensions = ['.html', '.css', '.js']
exclude_list = ['special_page.html']

replace_list = [
    [r'https:\/\/old\.example\.com\/', '/new_path/'],
    [r'^\.\/assets\/', '/new_path/assets/'],
    [r'^\/resources\/', '/new_path/resources/'],
]

ソースディレクトリの例 (my_website_source):

my_website_source/
├── index.html
├── contact.html
├── assets/
│   ├── style.css
│   └── logo.png
├── resources/
│   └── script.js
└── special_page.html

index.html の内容の例:

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="./assets/style.css">
</head>
<body>
    <h1>Welcome</h1>
    <a href="https://old.example.com/about.html">About Us</a>
    <script src="/resources/script.js"></script>
</body>
</html>

style.css の内容の例:

body {
    background-image: url("../assets/background.jpg");
    color: #333;
}
a {
    color: #007bff;
}

special_page.html の内容の例:

<!DOCTYPE html>
<html>
<body>
    <p>This is a special page. It should not be modified.</p>
    <a href="https://old.example.com/contact.html">Contact (this will not be replaced)</a>
</body>
</html>

コマンドライン実行:

python replace_recursive.py

実行結果の表示例:

Convert my_website_source/index.html (index.html) to my_website_output/index.html
Convert my_website_source/contact.html (contact.html) to my_website_output/contact.html
Convert my_website_source/assets/style.css (assets/style.css) to my_website_output/assets/style.css
Copy my_website_source/assets/logo.png (assets/logo.png) to my_website_output/assets/logo.png
Convert my_website_source/resources/script.js (resources/script.js) to my_website_output/resources/script.js
file [special_page.html] is in the exclude list ['special_page.html']. Skip and Copy

生成される出力ファイル (my_website_output) の構造と内容:

my_website_output/
├── index.html
├── contact.html
├── assets/
│   ├── style.css
│   └── logo.png
├── resources/
│   └── script.js
└── special_page.html
  • my_website_output/index.html の内容:

    <!DOCTYPE html>
    <html>
    <head>
        <link rel="stylesheet" href="/new_path/assets/style.css">
    </head>
    <body>
        <h1>Welcome</h1>
        <a href="/new_path/about.html">About Us</a>
        <script src="/new_path/resources/script.js"></script>
    </body>
    </html>
    

    (URLやパスが replace_list に従って置換されています)

  • my_website_output/assets/style.css の内容:

    body {
        background-image: url("../assets/background.jpg"); /* このパスは置換ルールに一致しないため変更なし */
        color: #333;
    }
    a {
        color: #007bff;
    }
    

    (この例では style.css に適用される正規表現パターンがないため、内容に変更はありませんが、replace_list を適切に定義すれば置換可能です)

  • my_website_output/assets/logo.png の内容: バイナリファイルのため、my_website_source/assets/logo.png と同じ内容でコピーされます。.pngconvert_file_extensions に含まれていないため、置換処理は行われません。

  • my_website_output/special_page.html の内容:

    <!DOCTYPE html>
    <html>
    <body>
        <p>This is a special page. It should not be modified.</p>
        <a href="https://old.example.com/contact.html">Contact (this will not be replaced)</a>
    </body>
    </html>
    

    (exclude_listspecial_page.html が含まれているため、内容は置換されずに元のままコピーされます。)