正規表現

正規表現とは、文字列内で文字の組み合わせを照合するために用いられるパターンです。JavaScript では、正規表現はオブジェクトでもあります。これらのパターンは RegExpexec および test メソッドや、Stringmatchreplacesearch、および split メソッドで使用できます。この章では、JavaScript の正規表現について説明します。

正規表現の作成

正規表現は 2 つの方法で作ることができます:

  • 次のように、正規表現リテラルを使用する:
    var re = /ab+c/;
    

    正規表現リテラルでは、スクリプトが評価されるときにその正規表現をコンパイルします。正規表現を定数として残しておくときは、この方法を使用するとよいパフォーマンスが得られます。

  • 次のように、RegExp オブジェクトのコンストラクタ関数を呼び出す。:
    var re = new RegExp("ab+c");
    

    コンストラクタ関数を使用すると、実行時にその正規表現をコンパイルします。正規表現パターンが変わることがわかっている場合や、パターンがわからない場合、ユーザが入力するなど別のソースからパターンを取得する場合は、コンストラクタ関数を使用してください。

正規表現パターンを書く

正規表現パターンは、/abc/ のような単純な文字、または /ab*c//Chapter (\d+)\.\d*/ のような単純な文字と特殊文字との組み合わせからなります。このセクションの最後の例では、記憶装置として用いられる丸括弧が含まれています。パターンのこの部分でマッチした箇所は、後で使用できるように記憶されます。詳しくは括弧で囲まれた部分文字列のマッチの使用を参照してください。

単純なパターンの使用

単純なパターンは、直接マッチしている部分を見つけたい文字で構成されます。例えば /abc/ というパターンは、実際に 'abc' という文字が一緒にその順で存在しているときだけ、文字列中の文字の組み合わせにマッチします。"Hi, do you know your abc's?" や "The latest airplane designs evolved from slabcraft." といった文字列でのマッチは成功します。どちらの場合でも 'abc' という部分文字列にマッチします。"Grab crab" という文字列では、'abc' という部分文字列が含まれていないためマッチしません。

特殊文字の使用

1 つ以上の b を見つけたり、ホワイトスペースを見つけたりといった直接マッチより高度なマッチの検索では、パターンで特殊文字を使用します。例えば /ab*c/ というパターンでは、1 つの 'a' とその後ろに続く 0 個以上の 'b' (* は直前のアイテムの 0 回以上の出現を意味します)、そしてそのすぐ後ろに続く 'c' で構成される文字の組み合わせにマッチします。"cbbabbbbcdebc," という文字列では、このパターンは 'abbbbc' という部分文字列にマッチします。

以下の表で、正規表現で使用できる特殊文字とその意味を詳しく説明します。

表 4.1 : 正規表現における特殊文字
文字 意味
\ 次のうちのいずれか:
  • 通常は文字どおりに扱われる文字に対して、次の文字は特殊であり文字どおりに解釈すべきではないと指示します。
  • 例えば /b/ は文字 'b' にマッチします。b の前にバックスラッシュを置いて /\b/ とすると、その文字は単語の区切りにマッチすることを意味する特殊文字になります。
  • 通常は特殊文字として扱われる文字に対して、次の文字は特殊ではなく文字どおりに解釈すべきであると指示します。
  • 例えば * は直前のアイテムの 0 回以上の出現にマッチすることを意味する特殊文字です。例えば /a*/ は 0 文字以上の a へのマッチを意味します。* という文字そのものにマッチさせるには、その直前にバックスラッシュを置きます。例えば /a\*/ は 'a*' にマッチします。
  • また、\ も文字列内ではエスケープ文字であることから、新たな RegExp("pattern") を記述する際は \ 自身のエスケープを忘れないようにしてください。
^

入力の先頭にマッチします。複数行フラグが true にセットされている場合は、改行文字の直後にもマッチします。

例えば /^A/ は "an A" の 'A' にはマッチしませんが、"An E" の 'A' にはマッチします。

この文字は、文字集合パターンの先頭にある場合は異なる意味を持ちます。

例えば /[^a-z\s]/ は "I have 3 sisters" の '3' にマッチします。

$

入力の末尾にマッチします。複数行フラグが true にセットされている場合は、改行文字の直前にもマッチします。

例えば /t$/ は "eater" の 't' にはマッチしませんが、"eat" の 't' にはマッチします。

*

直前の文字の 0 回以上の繰り返しにマッチします。

例えば /bo*/ は "A ghost booooed" の 'boooo' や "A bird warbled" の 'b' にマッチしますが、"A goat grunted" ではマッチしません。

+

直前の文字の 1 回以上の繰り返しにマッチします。{1,} と同等です。

例えば /a+/ は "candy" の 'a' や "caaaaaaandy" のすべての a にマッチします。

?

直前の文字の 0 回か 1 回の出現にマッチします。{0,1} と同等です。

例えば /e?le?/ は "angel" の 'el' や "angle" の 'le'、あるいは "oslo" の 'l' にマッチします。

*+?{} といった量指定子の直後に使用した場合、その量指定子を無欲 (最小回数にマッチ) にします。これはデフォルトとは逆であり、デフォルトは欲張り (最大回数にマッチ) です。例えば /\d+/ は非グローバルマッチで "123abc" の "123" にマッチしますが、/\d+?/ の場合は "1" だけにマッチします。

これは先読み表現内でも使用でき、この表の x(?=y) および x(?!y) にて説明します。

.

(小数点) 改行文字以外のどの 1 文字にもマッチします。

例えば /.n/ は "nay, an apple is on the tree" の 'an' や 'on' にはマッチしますが、'nay' にはマッチしません。

(x)

'x' にマッチし、マッチした内容を記憶します。これはキャプチャする括弧と呼びます。

例えば /(foo)/ mは "foo bar." の 'foo' にマッチし、これを記憶します。マッチした部分文字列は、結果として生成される配列の要素 [1], ..., [n] から呼び出すことができます。

(?:x) 'x' にマッチしますが、マッチした内容は記憶しません。これはキャプチャしない括弧と呼びます。マッチした部分文字列を、結果として生成される配列の要素 [1], ..., [n] から呼び出すことはできません。
x(?=y)

'x' に 'y' が続く場合のみ 'x' にマッチします。これは先読みと呼ばれます。

例えば /Jack(?=Sprat)/ は 'Jack' の後に 'Sprat' が続く場合のみ 'Jack' にマッチします。/Jack(?=Sprat|Frost)/ は 'Jack' の後ろに 'Sprat' または 'Frost' が続く場合のみ 'Jack' にマッチします。しかしながら、'Sprat' も 'Frost' もマッチの結果には表れません。

x(?!y)

'x' に 'y' が続かない場合のみ 'x' にマッチします。これは否定先読みと呼ばれます。

例えば /\d+(?!\.)/ は後ろに小数点が続かない数値にマッチします。正規表現 /\d+(?!\.)/.exec("3.141") は '141' にマッチしますが '3.141' にはマッチしません。

x|y

'x' または 'y' にマッチします。

例えば /green|red/ は "green apple" の 'green' や "red apple" の 'red' にマッチします。

{n}

n には正の整数が入ります。直前の文字がちょうど n 回出現するものにマッチします。

例えば /a{2}/ は "candy" の 'a' にはマッチしませんが、"caaandy" の最初の 2 つの a にはマッチします。

{n,m}

n および m には正の整数が入ります。直前の文字が少なくとも n 回、多くても m 回出現するものにマッチします。

n または m が 0 である場合、それを省略できます。

例えば /a{1,3}/ は "cndy" ではマッチせず、"candy," の 'a'、"caandy," の 最初の 2 つの a、"caaaaaaandy" の最初の 3 つの a にマッチします。"caaaaaaandy" では元の文字列に a が 4 つ以上ありますが、マッチするのは "aaa" であることに注意してください。

[xyz]

文字の集合です。囲まれた文字のいずれかにマッチします。ハイフンを用いて文字の範囲を指定することも可能です。文字の集合内では、特殊文字 (例えばドット (.) やアスタリスク (*)) は特別な意味を持ちません。それらのエスケープは不要です。エスケープシーケンスでも動作します。

例えば [abcd][a-d] と同じです。これは "brisket" の 'b' や "city" の 'c' にマッチします。/[a-z.]+/ および /[\w.]+/ はどちらも、"test.i.ng" の全体にマッチします。

[^xyz]

文字の集合の否定または補集合です。角括弧で囲まれていないものにマッチします。ハイフンを用いて文字の範囲を指定することも可能です。通常の文字の集合で動作するものすべてがこちらでも動作します。

例えば [^abc][^a-c] と同じです。これは始めに、"brisket" の 'r' や "chop" の 'h' にマッチします。

[\b] 後退 (U+0008) にマッチします。(\b と混同してはいけません。)
\b

単語の区切りにマッチします。単語の区切りは、単語構成文字の前または後ろに別の単語構成文字がない位置にマッチします。マッチした単語の区切りは、マッチした部分に含まれないことに注意してください。言い換えると、マッチした単語の区切りの長さは 0 です。([\b] と混同してはいけません。)

例:
/\bm/ は "moon" の 'm' にマッチします。
/oo\b/ は "moon" の 'oo' にはマッチしません。これは、'oo' の後ろに単語構成文字である 'n' が続いているためです。
/oon\b/ は "moon" の 'oon' にマッチします。これは、'oon' が文字列の終端であり単語構成文字が続かないためです。
/\w\b\w/ はどこにもマッチしないでしょう。これは、単語構成文字の後に非単語構成文字および単語構成文字を続けることができないためです。

\B

単語の区切り以外の文字にマッチします。これは、前の文字と後ろの文字が同じ種類である位置にマッチします。すなわち両方とも単語であるか、両方とも単語でない状況です。文字列の先頭と終端は、単語ではないと考えます。

例えば /\B../ は "noonday" の 'oo' に、/y\B./ は "possibly yesterday" の 'ye' にマッチします。

\cX

X には A から Z のうち 1 文字が入ります。文字列中の制御文字にマッチします。

例えば /\cM/ は文字列中の control-M (U+000D) にマッチします。

\d

数字にマッチします。[0-9] と同等です。

例えば /\d//[0-9]/ は "B2 is the suite number" の '2' にマッチします。

\D

数字以外の文字にマッチします。[^0-9] と同等です。

例えば /\D//[^0-9]/ は "B2 is the suite number" の 'B' にマッチします。

\f 改ページ (U+000C) にマッチします。
\n 改行 (U+000A) にマッチします。
\r 復帰 (U+000D) にはマッチします。
\s

スペース、タブ、改ページ、改行を含む 1 つのホワイトスペース文字にマッチします。[ \f\n\r\t\v\u00A0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u2028\u2029\u202f\u205f\u3000] と同等です。

例えば /\s\w*/ は "foo bar" の ' bar' にマッチします。

\S

ホワイトスペース以外の 1 文字にマッチします。[^ \f\n\r\t\v\u00A0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u2028\u2029\u202f\u205f\u3000] と同等です。

例えば /\S\w*/ は "foo bar" の 'foo' にマッチします。

\t タブ (U+0009) にマッチします。
\v 垂直タブ (U+000B) にマッチします。
\w

アンダースコアを含むどの英数字にもマッチします。[A-Za-z0-9_] と同等です。

例えば /\w/ は、"apple," の 'a' や "$5.28," の '5' や "3D" の '3' にマッチします。

\W

前述以外の文字にマッチします。[^A-Za-z0-9_] と同等です。

例えば /\W//[^A-Za-z0-9_]/ は、"50%" の '%' にマッチします。

\n

n には正の整数が入ります。正規表現内で n 番目の括弧の部分にマッチする最後の部分文字列への後方参照です (左括弧をカウントします)。

例えば /apple(,)\sorange\1/ は "apple, orange, cherry, peach" の 'apple, orange,' にマッチします。

\0 NULL (U+0000) 文字にマッチします。この後ろに他の数字と続けてはいけません。これは、\0<digits> が 8 進数のエスケープシーケンスであるためです。
\xhh hh (2 桁の 16 進数) というコードを持つ文字列にマッチする。
\uhhhh hhhh (4 桁の 16 進数) というコードを持つ文字列にマッチする。

ユーザ入力を正規表現内の文字列リテラルとして扱うためには上記の記号は全てエスケープする必要がありますが、これは以下の様な関数を用いる事で簡単に達成することができます。

function escapeRegExp(string) {
  return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, "\\$1");
}

括弧の使用

正規表現パターンの一部を括弧で囲むことで、マッチした部分文字列を記憶しておくことができます。一度記憶すると、後からその部分文字列を呼び戻すことができます。これに関しては括弧で囲まれた部分文字列のマッチの使用で説明しています。

例えば /Chapter (\d+)\.\d*/ というパターンではエスケープされた文字や特殊文字を付加しており、その一部分を記憶するように指示しています。これは 'Chapter ' という文字列、それに続く 1 文字以上の数字 (\d はいずれかの数字を、+ は 1 回以上の繰り返しを意味します)、それに続く小数点 (それ自体は特殊文字であり、小数点の前の \ はパターンが '.' という文字そのものを探すようにすることを意味します)、それに続く 0 文字以上の数字 (\d は数字を、* は 0 回以上の繰り返しを意味します) にマッチします。さらに、括弧を使うことで最初にマッチした数字を記憶させます。

このパターンは "Open Chapter 4.3, paragraph 6" という文字列で見つかり、'4' が記憶されます。このパターンは "Chapter 3 and 4" では見つかりません。この文字列は '3' の後にピリオドがないためです。

マッチした部分を記憶させることなく部分文字列にマッチさせたい場合は、その括弧においてパターンの前に ?: をつけてください。例えば (?:\d+) は 1 文字以上の数字にマッチしますが、マッチした文字列は記憶しません。

正規表現の使用

正規表現は、RegExptest および exec メソッド、Stringmatchreplacesearch、およびsplit メソッドとともに使用します。これらのメソッドの詳細は JavaScript リファレンスで説明しています。

表 4.2 : 正規表現を使用するメソッド
メソッド 説明
exec 文字列中で一致するものを検索する RegExp のメソッド。結果情報の配列を返します。
test 文字列中で一致するものがあるかをテストする RegExp のメソッド。true または false を返します。
match 文字列中で一致するものを検索する String のメソッド。結果情報の配列を返します。マッチしない場合は null を返します。
search 文字列中で一致するものがあるかをテストする String のメソッド。マッチした場所のインデックスを返します。検索に失敗した場合は -1 を返します。
replace 文字列中で一致するものを検索し、マッチした部分文字列を別の部分文字列に置換する String のオブジェクト。
split 正規表現または固定文字列を用いて文字列を分割し、部分文字列の配列に入れる String のメソッド。

あるパターンが文字列に存在するかを知りたいときは、test または search メソッドを使用してください。詳細な情報が知りたいときは (実行時間が長くなりますが) exec または match メソッドを使用してください。execmatch を使用してマッチが成功した場合、これらのメソッドは配列を返し、また結びつけられた正規表現オブジェクトと定義済みオブジェクトである RegExp オブジェクトのプロパティを更新します。マッチが失敗すると、exec メソッドは null (false に変換します) を返します。

次の例では、exec メソッドを使用して文字列を検索します。

var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");

正規表現のプロパティにアクセスする必要がない場合は、次のスクリプトが myArray を作成する別の方法になります:

var myArray = /d(b+)d/g.exec("cdbbdbsbz");

ある文字列から正規表現を組み立てたい場合は、次のスクリプトのような方法があります:

var myRe = new RegExp("d(b+)d", "g");
var myArray = myRe.exec("cdbbdbsbz");

これらのスクリプトではマッチが成功し、配列を返すとともに次の表で示すプロパティを更新します。

表 4.3 : 正規表現の実行結果
オブジェクト プロパティまたはインデックス 説明 この例の場合
myArray   マッチした文字列と、すべての記憶された部分文字列です。 ["dbbd", "bb"]
index 入力文字列でマッチした位置を示す、0 から始まるインデックスです。 1
input 元の文字列です。 "cdbbdbsbz"
[0] 最後にマッチした文字列です。 "dbbd"
myRe lastIndex

次のマッチが始まるインデックスです。(このプロパティは、 g オプションを用いる正規表現でのみセットされます。これはフラグを用いた高度な検索で説明します。)

5
source パターンのテキストです。正規表現の実行時ではなく作成時に更新されます。 "d(b+)d"

この例の 2 つ目の形式で示したようにオブジェクト初期化子を使用して、変数に代入せずに正規表現を使うことができます。しかしながら、この方法では出現する正規表現がすべて、別の正規表現として作成されます。このため、変数に代入しないこの形式を使用する場合は、その正規表現のプロパティに後からアクセスすることができません。例えば、次のようなスクリプトを使用するとします:

var myRe = /d(b+)d/g;
var myArray = myRe.exec("cdbbdbsbz");
console.log("The value of lastIndex is " + myRe.lastIndex);

このスクリプトは以下のように出力します:

The value of lastIndex is 5

しかし、このスクリプトの場合は:

var myArray = /d(b+)d/g.exec("cdbbdbsbz");
console.log("The value of lastIndex is " + /d(b+)d/g.lastIndex);

次のように出力します:

The value of lastIndex is 0

この 2 つの文にある /d(b+)d/g は別の正規表現オブジェクトであり、そのためにそれぞれの lastIndex プロパティの値も異なるのです。オブジェクト初期化子で作成する正規表現のプロパティにアクセスする必要がある場合は、まずそれを変数に代入するようにしてください。

括弧で囲まれた部分文字列のマッチの使用

正規表現パターンに括弧を含めることで、対応するサブマッチが記憶されます。例えば /a(b)c/ は 'abc' という文字列にマッチし、'b' が記憶されます。この括弧で囲まれた部分文字列のマッチは、Array の要素 [1], ..., [n] を使用して呼び戻すことができます。

括弧で囲まれた部分文字列は何個でも使用できます。返された配列には、見つかったものすべてが存在します。以下の例では、括弧で囲まれた部分文字列の使用法を説明します。

例 1

次のスクリプトは replace() メソッドを使用して文字列中の単語を入れ替えます。テキスト置き換えのために、スクリプトで $1$2 を使用して、最初とその次の括弧で囲まれた部分文字列のマッチを示しています。

var re = /(\w+)\s(\w+)/;
var str = "John Smith";
var newstr = str.replace(re, "$2, $1");
console.log(newstr);

これは "Smith, John" を出力します。

フラグを用いた高度な検索

正規表現には、グローバルな検索や大文字/小文字を区別しない検索を可能にする 4 つのオプションフラグがあります。グローバルサーチを行うには、g フラグを使用してください。大文字・小文字を区別しない検索を行うには、i フラグを使用してください。各行の行頭・行末にマッチさせるには、m フラグを使用してください。"sticky" 検索を行うには、y フラグを使用してください。この検索は、対象文字列における現在の位置から開始します。これらのフラグは、単独で使用することもまとめて使用することもできます。順番は問いません。フラグは正規表現の一部として含まれます。

Firefox 3 における注記

y フラグのサポートは Firefox 3 で追加されました。マッチが対象文字列における現在の位置で成功しない場合、y フラグは失敗します。

フラグを正規表現に含めるには、次のようにしてください:

var re = /pattern/flags;

または

var re = new RegExp("pattern", "flags");

フラグは正規表現の不可分な一部であることに注意してください。後から加えたり取り除いたりすることはできません。

例えば re = /\w+\s/g は、1 つ以上の文字とそれに続くスペースを探す正規表現を作成します。また、これは文字列全体を通してこの組み合わせを探します。

var re = /\w+\s/g;
var str = "fee fi fo fum";
var myArray = str.match(re);
console.log(myArray);

この例では ["fee ", "fi ", "fo "] が表示されます。また、この例では次の行:

var re = /\w+\s/g;

を次の行:

var re = new RegExp("\\w+\\s", "g");

に置き換えることができます。得られる結果は同じです。

m フラグは、複数行の入力文字列を複数の行として扱うときに用います。m フラグを使用した場合 ^ および $ は、文字列全体の先頭または末尾ではなく、入力文字列中のどの行の先頭または末尾にもマッチします。

以下の例では、正規表現の使用法をいくつか示します。

入力文字列の順序変更

次の例では、正規表現の構造と string.split() および string.replace() の使用法を示します。空白、タブ、1 つのセミコロンで分割された名前 (ファーストネームが先) で構成される、簡単に整形された入力文字列をきれいにします。最終的に名前の順序を逆転し (ラストネームが先)、リストをソートします。

// 名前の文字列は複数の空白やタブを含む。
// また、ファーストネームとラストネームの間に複数の空白があることもある
var names = "Harry Trump ;Fred Barney; Helen Rigby ; Bill Abel ; Chris Hand ";

var output = ["---------- Original String\n", names + "\n"];

// 2 つの正規表現パターンと、格納用の配列を用意する。
// 文字列を分割して配列の要素に収める。

// パターン: ホワイトスペースが存在しうる、セミコロンが存在する、ホワイトスペースが存在しうる
var pattern = /\s*;\s*/;

// 上記のパターンで文字列を断片に分割し、
// nameList という配列に断片を格納する。
var nameList = names.split(pattern);

// 新たなパターン: 1 つ以上の文字、1 つ以上のホワイトスペース、1 つ以上の文字
// 括弧を用いてパターンの断片を記憶する。
// 記憶した断片は後から参照される。
pattern = /(\w+)\s+(\w+)/;

// 処理された名前を格納する新しい配列。
var bySurnameList = [];

// 名前の配列を表示し、新しい配列にコンマ区切りで名前を
// ラストネーム、ファーストネームの順で格納する。
//
// replace メソッドはパターンにマッチしたものを除去し、
// 記憶しておいた文字列に置き換える。
// 「2 番目に記憶しておいた断片とその後に続くコンマとスペース、
// さらにその後に続く 1 番目に記憶しておいた断片」
//
// 変数 $1 および $2 は、パターンにマッチさせた際に
// 記憶しておいた断片を参照する

output.push("---------- After Split by Regular Expression");

var i, len;
for (i = 0, len = nameList.length; i < len; i++){
  output.push(nameList[i]);
  bySurnameList[i] = nameList[i].replace(pattern, "$2, $1");
}

// 新しい配列を表示する。
output.push("---------- Names Reversed");
for (i = 0, len = bySurnameList.length; i < len; i++){
  output.push(bySurnameList[i]);
}

// ラストネームについてソートし、ソートした配列を表示する。
bySurnameList.sort();
output.push("---------- Sorted");
for (i = 0, len = bySurnameList.length; i < len; i++){
  output.push(bySurnameList[i]);
}

output.push("---------- End");

console.log(output.join("\n"));

特殊文字を用いた入力の確認

次の例では、ユーザが電話番号を入力します。ユーザが "Check" ボタンを押すと、スクリプトがその番号の妥当性を確認します。その番号が正当である (正規表現で指定した文字の連続にマッチする) 場合、スクリプトはユーザへの感謝のメッセージを表示し、その番号を確認します。番号が正当でない場合は、その番号が妥当でないことをユーザに通知します。

正規表現は、0 または 1 つの左括弧 \(?、それに続く 3 つの数字 \d{3}、それに続く 0 または 1 つの右括弧 \)?、それに続き、見つかった場合に記憶する 1 つのダッシュ、スラッシュ、または小数点 ([-\/\.])、それに続く 3 つの数字 \d{3}、それに続く記憶された 1 つのダッシュ、スラッシュ、または小数点のマッチ \1、それに続く 4 つの数字を探します。

ユーザが Check ボタンを押した際に発動する Clink イベントは、RegExp.input の値をセットします。

<!DOCTYPE html>
<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">  
    <meta http-equiv="Content-Script-Type" content="text/javascript">  
    <script type="text/javascript">  
      var re = /\(?\d{3}\)?([-\/\.])\d{3}\1\d{4}/;  
      function testInfo(phoneInput){  
        var OK = re.exec(phoneInput.value);  
        if (!OK)  
          window.alert(RegExp.input + " isn't a phone number with area code!");  
        else
          window.alert("Thanks, your phone number is " + OK[0]);  
      }  
    </script>  
  </head>  
  <body>  
    <p>Enter your phone number (with area code) and then click "Check".
        <br>The expected format is like ###-###-####.</p>
    <form action="#">  
      <input id="phone"><button onclick="testInfo(document.getElementById('phone'));">Check</button>
    </form>  
  </body>  
</html>

ドキュメントのタグと貢献者

Contributors to this page: jislotz, ethertank, saneyuki_s, yyss, Sheppy, Mgjbot, Potappo, Electrolysis
最終更新者: jislotz,