ほんじゃらねっと

ダイエット中プログラマのブログ

正規表現を使ったエレガントな置換処理を学びつつ簡易なファイル名一括変換ツールを作る

文字列を検索したりマッチしたものを置換したり、という作業は

技術者が制作・開発する時だけでなく、例えばExcelやWordで文書を編集する際や

Webページ内で目的の文章を探したりする場合にも行うもので、

「作業の効率化」という点では欠かせないものだ。

今回扱う「正規表現」を使った検索・置換については、

標準の機能として備えているツールやテキストエディタも多いが、

プログラマ以外には馴染みの薄いものかもしれない。

(少なくとも私に面倒な変換作業を依頼してくる人たちは間違いなく知らないと思う)

「正規表現」を全く知らない人は、

このつまらなそうな言葉を見ただけでスルーしてしまいそうだが、

使いこなせば大変強力なものであり

プログラマ以外の

パソコンを使用して何かしらの仕事をしている人にとっても

作業効率が劇的に上がること間違いなしの技術なので、

ここで紹介しておきたい。

正規表現とは

正規表現はそのとっつきにくい名前、

調べた時の説明の分かりにくさ、

やたら記号が出てくる構文、などなど

挫折するポイントが満載なので、

ここではとにかくとっつきやすさ重視で説明していく。

まず「正規表現」とはどういうものか、を一言で説明すると、

「文字列をパターンにまとめて表現することで似たような言葉の検索と置換を簡単にする書き方」だ。

早速とっつきにくいか。

正規表現が扱う「文字列のパターン」について理解するところからはじめよう。

「123」「444」「542」という文字列を1つのパターンで表現するなら、

「3桁の数字」。

「IMG_20161201.jpg」「IMG_20150303.png」「IMG_20160115.jpg」なら、

「IMG_で始まって年月日の数字が続いて、拡張子がjpgかpngの文字列」。

上記のように文字の種類や並びを元に複数の対象を1つの文にまとめたもののことを

「文字列のパターン」と呼ぶ。

パターンにすることで、文字列のグループを1つのものとして扱いやすくなる。

そしてこのパターンの書き方(表現のしかた)を

様々なツールで同じように使えるよう標準化したものが「正規表現」だ。

この正規表現はツールと組み合わせて、

大量の文字列の中からパターンに当てはまるものを見つけたり、

別の文字列やパターンに置換えたりするのに使用する。

これまで1つ1つ個別に検索して置換してたようなことを、

一発で検索・置換できるようになるので、

うまく使えば作業にかかる時間を大幅に短縮することができる。

例えば

「文書の中の郵便番号が全部ハイフン無し(〒1234567)になっちゃってるから

ハイフンあり(〒123-4567)に変更したい」

みたいな状況があった場合、

パターン無しで1つ1つ修正していくのは相当時間がかかるだろうけど、

「〒のあとに数値7桁」というパターンを

「〒のあとに数値の前半3桁-後半4桁」という形に置換する、

みたいな指定方法を使うことができれば一括で置換することができる。

このように、

正規表現のルールを覚えて、

正規表現が使えるツールと組み合わせて使えるようにしておけば、

あとは状況に応じてうまく当てはまるパターンを考えるだけで

この強力な機能が様々な仕事で使えるようになる。

押さえておいて損はない技術だと思う。

正規表現が使えるツール

この記事の中では、Gruntをツールとして使用するが、

世の中には正規表現で検索・置換ができるツールがたくさんある。

技術者向けのツールが多いが、

例えば「秀丸」や「さくらエディタ」など多くのテキストエディタは

正規表現で検索・置換できる機能がついている。

WindowsのPowerShellでも使える。

Excelは残念ながら標準では正規表現が使えないが、

アドインを入れるか頑張ってVBAを書けば使えるようだ。

ツールによって一部の構文しか使えなかったり、

書き方が少し異なったり、結構ルールに違いがあるので、

各ツールのヘルプやマニュアルで確認していただきたい。

正規表現を試せる環境を作る

せっかくなので実用的なサンプルを作りながら正規表現を習得していこう。

今回はGruntのパッケージとして公開されている、

「正規表現を使って特定パターンのファイル名を別のファイル名に変更できるプラグイン」

を使って、フォルダ内のファイルのファイル名を一括変更できるツールを作っていく。

Gruntの基本的な使い方については下記の入門記事で説明している。

blog.honjala.net

まずはテンプレートとして

下記のようなファイルの入ったアプリケーション用フォルダを用意する。

package.json

{
  "name": "filename_changer",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "grunt": "^0.4.5",
    "grunt-contrib-clean": "^1.0.0",
    "grunt-contrib-copy": "^1.0.0",
    "grunt-file-regex-rename": "^0.1.0"
  }
}

Gruntfile.js

module.exports = function(grunt) {
    grunt.initConfig({
        clean: ["dest_files/*"],
        copy: {
            init: {
                expand: true,
                cwd: "src_files/",
                src: "*",
                dest: "dest_files/"
            }
        },
        fileregexrename: {
            sample1: {
                files: [{
                    expand: true,
                    src: "dest_files/*"
                }],
                options: {
                    replacements: [{
                        pattern: /^(doc_.*)\.txt$/,
                        replacement: "$1.md"
                    }]
                }
            }
        }
    });
    
    grunt.loadNpmTasks("grunt-contrib-clean");
    grunt.loadNpmTasks("grunt-contrib-copy");
    grunt.loadNpmTasks("grunt-file-regex-rename");
    grunt.registerTask("default", "fileregexrename");
};

上記ファイルと同じフォルダの中に、下記のフォルダを作成しよう:

  • src_files
  • dest_files

そして、src_filesの中に下記の空ファイルを作成しておく:

doc_20160301.txt
doc_20160302.txt
photo_20160201.jpg
photo_20160202.jpg
photo_20160301.jpg
photo_20160302.jpg
photo_20160303.png
photo_20160304.png

dest_filesフォルダは空でOK。

内容としては、色々なファイル名置換が試せるように、

src_filesフォルダに置換前のファイルを置き、

そこからdest_filesフォルダにファイルをコピーして置換を試せるようにしてある。

上記の準備ができたら、

アプリケーション用フォルダの中で下記コマンドを実行し、

必要なパッケージをインストールする:

npm install

サンプルで使用するコマンド

今回作成するサンプルでは下記の3コマンドを使用できるようにしている:

dest_filesフォルダの内容を削除する

grunt clean

dest_filesフォルダにsrc_filesの内容をコピーする

grunt copy

ファイル名置換を行う

grunt fileregexrename:sample1

まずは準備として、cleanとcopyのコマンドを実行してみよう。

dest_filesフォルダにsrc_filesフォルダの中身がコピーされるはずだ。

サンプルツールで遊ぶ

それでは早速サンプルツールを実行してみよう。

既に上記のGruntfile.jsにファイル名を変更する処理を書いてあるので、

ファイル名置換コマンド(上のfileregexrename:sample1)を

実行してみていただきたい。

dest_filesフォルダ内の「doc_」で始まるファイルの

ファイル名が変更されるのが確認できるはずだ。

この置換処理を行っているのが下記の部分だ。

replacements: [{
    pattern: /^(doc_.*)\.txt$/,
    replacement: "$1.md"
}]

「pattern:」で始まる行のスラッシュで囲まれた部分「doc_.*.txt$」が検索パターンを表す正規表現で、

「replacement:」で始まる行の「$1.md」が置換後の文字列を表している。

このサンプルでの検索パターンと置換後の文字列の説明は下記のようになる。

検索パターン
「doc_」で「始まり(^)」、「0文字以上の(*)」「任意の文字(.)」の後「.txt」で終わる($)。
置換後文字列
「検索対象の()で囲まれた最初の部分($1)」の後に「.md」をつける。

正規表現でのパターンの書き方は、

こんな感じで「普通の文字や数字」と、

それぞれ「意味を持つ記号(メタ文字)」を組み合わせて表現する。

ここで出てきたメタ文字を説明しておこう:

^
行の先頭を表す位置指定用メタ文字
$
行の最後を表す位置指定用メタ文字
.(ドット)
あらゆる文字を表す文字種指定用メタ文字
*
0文字以上を表す文字数指定用メタ文字
()
囲んだ部分を置換後文字列で呼び出せるようにする後方参照用メタ文字。グループ化にも使う。
$n
()で囲んだ部分を$1〜$9で前から順に呼び出すための後方参照用メタ文字(言語や環境によっては、$ではなくバックスラッシュを使用する)

ここで出てきた4タイプ(位置指定用、文字種指定用、文字数指定用、後方参照用)のメタ文字には

それぞれいくつか種類があるが、

基本この4タイプのメタ文字を組み合わせてパターンを作成する。

補足として、

メタ文字として意味を与えられている記号を検索対象としたい場合は、

バックスラッシュを前につける(エスケープする)ことで通常の文字として使用できる。

今回のサンプルで言うと「.txt」のドットがエスケープされており、

それによって「.txt」という文字列で検索できるようになっている。

パターンを書き換えてみよう

ここからこの検索対象と置換後文字列の部分を

色々変えてみながら正規表現の書き方と結果を見ていこう。

一度下記のコマンドを実行して、

dest_files内のファイルのファイル名をリセットしておこう。

grunt clean
grunt copy

次に下記のようにGruntfile.jsの検索パターンの部分を変更してみよう:

replacements: [{
    pattern: /^(photo_.*)\.txt$/,
    replacement: "$1.md"
}]

「doc」を「photo」に変更してあり、

「ファイル名がphoto_で始まって.txtで終わるファイル」を検索して、

「ファイル名.md」に置換する

という意味になる。

しかしdest_filesフォルダに

「ファイル名がphoto_で始まって.txtで終わるファイル」は存在しない。

この場合どうなるか、置換処理を実行してみよう:

grunt fileregexrename:sample1

どのファイルも変更されないはずだ。

置換処理は検索パターンにマッチしたものに対してのみ行われる。

では次に、

「photo_で始まって.jpgで終わる」ファイルを検索して、

拡張子を「.gif」に置換するパターンに変更してみよう:

replacements: [{
    pattern: /^(photo_.*)\.jpg$/,
    replacement: "$1.gif"
}]

再び置換処理を実行してみると、

元々「.jpg」という拡張子を持っていたファイルの

拡張子が「.gif」に変更されているはずだ。

今度は

元の拡張子に関わらずphoto_で始まるファイルの

拡張子を「.gif」に変更するパターンを書いてみよう。

検索パターンの拡張子の部分を任意の文字列を表す「.*」に変更する。

replacements: [{
    pattern: /^(photo_.*)\..*$/,
    replacement: "$1.gif"
}]

一度dest_filesフォルダの内容をリセットしてから置換処理を実行し、

結果を確認してみよう。

最後に、もう少し実用性がありそうな置換サンプルを作ってみたい。

「photo_20160301.jpg」を「20160301_photo.jpg」のように

ファイル名の前半と後半を置き換えるようなパターンを考えてみよう。

この場合、

置換前の「photo」の部分と「年月日」の部分を後方参照用の()で囲み、

置換後文字列で逆に配置してやることで変換できそうだ。

拡張子の部分も維持できるように後方参照できるようにする。

replacements: [{
    pattern: /^([a-z]+)_([0-9]{8})\.(.*)$/,
    replacement: "$2_$1.$3"
}]

置換処理を実行してみて、

うまくファイル名が置換されるのを確認してみていただきたい。

いくつか新しいメタ文字が登場しているので説明しておく:

[a-z]
指定した範囲の文字を表す文字種指定用メタ文字。数字の0-9なら[0-9]、アルファベットの大文字なら[A-Z]。組み合わせて[0-9a-z]みたいに使ったり、[abcxyz]のようにハイフンを使わなければ特定文字を表現できる。今回は使用しなかったが、[^a-z]のように^を[]内の先頭に書くと、「指定した文字以外」(この場合、小文字のa-z以外)を表すことができる。
+
1文字以上を表す文字数指定用メタ文字。*は0文字以上を表すが、最低1文字入っていることを表す場合は+を使う。
{数字}
指定文字数を表す文字数指定用メタ文字。0文字以上、1文字以上ではなく特定の文字数を表したい時に使用する。[0-9]{8}は数字8桁を表す。

置換後文字列では、

3箇所を後方参照用の()で囲み、それらを入れ替えて置換文字列として指定している。

先に説明したように、複数の後方参照を使用する場合は、$1、$2、$3のように

番号を指定することで前から順に結果を取り出すことができる。

マイファイル名一括変換ツールとして拡張していく

今回のサンプル作成はこれで終わりだが、

今回使ったGruntfile.jsは、

下記のようにパターンを追加していくことでどんどん拡張することができる。

module.exports = function(grunt) {
    grunt.initConfig({
        clean: ["dest_files/*"],
        copy: {
            init: {
                expand: true,
                cwd: "src_files/",
                src: "*",
                dest: "dest_files/"
            }
        },
        fileregexrename: {
            sample1: {
                files: [{
                    expand: true,
                    src: "dest_files/*"
                }],
                options: {
                    replacements: [{
                        pattern: /^(doc_.*)\.txt$/,
                        replacement: "$1.md"
                    }]
                }
            },
            sample2: {
                files: [{
                    expand: true,
                    src: "dest_files/*"
                }],
                options: {
                    replacements: [{
                        pattern: /^(photo_.*)\..*$/,
                        replacement: "$1.gif"
                    }]
                }
            },
            sample3: {
                files: [{
                    expand: true,
                    src: "dest_files/*"
                }],
                options: {
                    replacements: [{
                        pattern: /^([a-z]+)_([0-9]{8})\.(.*)$/,
                        replacement: "$2_$1.$3"
                    }]
                }
            }
        }
    });
    
    grunt.loadNpmTasks("grunt-contrib-clean");
    grunt.loadNpmTasks("grunt-contrib-copy");
    grunt.loadNpmTasks("grunt-file-regex-rename");
    grunt.registerTask("default", "fileregexrename:sample1");
};

追加したパターン(sample1~sample3)は下記のようにそれぞれ実行できる。

grunt fileregexrename:sample1
grunt fileregexrename:sample2
grunt fileregexrename:sample3

正規表現の練習がてら色々なパターンを追加して

自分用のファイル名一括変換ツールとして役立つものにしていただきたい。

おわり

今回作ったサンプルは3パターンで、学んだメタ文字も全体の一部ではあるが、

この内容を応用することで様々なパターンを作成し、置換できるはずだ。

最初の方で書いたとおり、

面倒な変換作業にかかる時間を短縮してくれる強力な技術なので、

ぜひ活用していただければと思う。

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)

正規表現技術入門 ――最新エンジン実装と理論的背景 (WEB+DB PRESS plus)