QA@IT

正規表現で、突然出現する特定の文字列の存在を判定するには、パターンをどう書けば良い?

3476 PV

Visual C# 2010 を使い、Web サーバーにアクセスして得られた HTML を解析する、いわゆるスクレイピングをおこなうアプリケーションを作っています。
解析対象の文字列の中に、特定の文字列が存在するかしないかを判定したいのですが、その際に、ある目印となるような既知の文字列(後述する "<br>" のようなもの)の後に、特定の文字列が続くのか続かないのかを判定することは簡単にできます。しかし、そのような目印がまったくなく、突然、特定の文字列が出現するかどうかを判定する方法が分かりません。

つぎのサンプルコードの場合、解析対象の HTML の中に、"You have new mail" という特定の文字列があるかどうかを知りたいのですが、あってもヒットしません(match.Groups[1].Success == false になってしまう)。

using System;
using System.Windows.Forms;
using System.Text.RegularExpressions;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Regex regex = new Regex(@"(You have new mail)?.*Date: (\d\d)/(\d\d)");
            Match match = regex.Match("<html><body>Welcome to our Meeting Room.<br>You have new mail<br>All rights reserved.<br>Date: 03/05</body></html>");
            if (!match.Success)
            {
                throw new Exception();
            }

            Console.WriteLine("You have new mail = " + match.Groups[1].Success + ", Month = " + match.Groups[2].Value + ", Day = " + match.Groups[3].Value);
        }
    }
}

もしも、目印として "<br>" があり、その後に続くと分かっているならば、パターン文字列を @"<br>(You have new mail)?.*Date: (\d\d)/(\d\d)" とすれば、マッチするようにできます。しかし、もしもこのような目印の文字列がなかったとしたらマッチさせることができません。

このような用途の場合、パターン文字列はどう書けば良いのでしょうか?簡単なことだと思うのですが、正規表現はパターン文字列のキーワードが記号なので、ネットで検索しづらくてやりかたが良く分かりません。よろしくお願いします。

  • ん?「解析対象の HTML の中に"You have new mail" という特定の文字列があるかどうかを知りたい」ということなら、`@"You have new mail"`だけで済むのでは? -

回答

最初の正規表現で出来ない理由は、.NET の正規表現 量指定子は、最小一致だからです。
http://msdn.microsoft.com/ja-jp/library/3206d374.aspx

@"(You have new mail)?.*Date: (\d\d)/(\d\d)"

このパターンですと、"You have new mail" があっても直後の "." のおかげで、0回の一致として処理出来てしまいます。
変更するなら、".
" を「"You have new mail" を含まない文字列」に変更する必要があると思います。

@"(You have new mail)?((?!You have new mail).)*Date: (\d\d)/(\d\d)"
編集 履歴 (0)
  • ありがとうございます。そのパターンで判定できました。
    量指定子を試してみて、最短一致の量指定子を使って、
    @"(You have new mail)?.*?Date: (\d\d)/(\d\d)"
    のように書いてみましたが、私の理解ではこれでも判定できると思うのですが、ダメでした。@"(You have new mail)?" よりも後ろの @".*?" が優先されてしまうのかなと思います。
    -
  • 量指定子を試してみた、別のパターンです。これは判定できました。
    @"(.*?You have new mail.*?)?Date: (\d\d)/(\d\d)"
    -
  • http://msdn.microsoft.com/ja-jp/library/0yzc2yb0.aspx
    にもありますが、一致する最長の文字列を検索しようとします。なので、@"(You have new mail)?.*?Date: (\d\d)/(\d\d)" ですと、(You have new mail)? が 0回の方が最長の文字列になります。
    -
  • すいません、私ドキュメント読み違えてて回答間違ってますね。。"?" だけですと最長一致(見つかる最大の回数で一致)ですが最長文字列にするために後ろの".*"が有効になっているのでは?
    You have ~ の前に.*入れると、最長文字列でも You have new mail が引っかかりました
    @"(.*You have new mail)?.*Date: (\d\d)/(\d\d)"
    -
  • ご回答中の @"((?!You have new mail).)*" のご説明が非常に分かりやすかったです。これにより質問のプログラムで判定ができるようになったので、一旦、accept させていただきます。みなさまありがとうございました。最長一致については、あらためて別の質問として投稿させていただきます。 -

(You have new mail)? の "?" が不要です。

ここに挙げられたサンプルコードでは問題ないですが、
実際に解析しようとしているhtmlテキストの中に、
You have new mail よりも前に、
「Date: \d\d/\d\d」という文字列が出現していませんか?

"?"は、
「直前の文字(この場合は()で囲ったYou have new mail)の0回もしくは1回の出現にマッチする」
ので、たとえば、
<br><div>Hello, world. Date: 03/05</div><br>
のような文字列でもマッチするはずです。
(手元に、C#の環境がないので、試してなくて申し訳ないですが...)

この場合、後方参照の1番目に相当する文字列は存在していないので、
Groups[1].Successはfalseになります。

それと、正規表現では通常、
直前に目印となる文字列があるかないかは、関係ありません。

たとえば「 <br>の直後に出現するYou have new mail 」だけをマッチさせたい場合のみ、
質問の文中にあるように、<br>をパターンに含めることになります。

編集 履歴 (1)
  • みなさまご回答ありがとうございます。ここにまとめて質問を補足させていただきます。Web スクレイピングで、1画面の中の複数の要素を、一度に解析したいと思っています。"Date: 03/05" のような文字列は常に存在しているので、これは月と日を Groups[2] と Groups[3] で取得したいです。それと同時に Groups[1] を取得する方法が分かりません。 -
  • 別々に(正規表現を2回使う)ならば取得することはできます。1回で済まそうとするとできなくて悩んでいます。パターン文字列から "?" を取り除いてしまうと、"You have new mail" が存在しない場合、パターン全体でマッチしなくなるため、月日も取得できなくなってしまいます。 -
  • なぜ1回で済ませる必要があるのでしょう?。また、そもそも正規表現を使わなければならない理由があるのでしょうか?Webスクレイピングをしたいと言うのなら、他にもいろいろ手段があるのでは?あと、質問の捕捉は質問に追記するのがよいかと。 -
  • ご質問を寄せられたので回答させていただきます。1回で済ませるほうが解析が楽だと思ったからです。2回に分けると文字列が登場する順序(今回の場合、"You have new mail" と Date: 03/05" の前後関係)も正規表現の外で管理する必要が出てきますが、1つの正規表現を使えば正規表現の中のパターン文字列で管理できます。 -
  • 理由は正規表現を使ったほうが便利だと思ったからです。他の手段も検討した結果、正規表現を使うことを選択しました。Web スクレイピングは質問に至った背景を簡単に説明するために書かせていただきました。 -
  • 前回の私のコメントは、自分の質問に対する補足であると同時に、みなさまのご回答に対する返信でもあり、後者の比率が低くなかったので、質問への追記ではなくコメントという形式で投稿することを選択しました。 -

質問の補足というよりも回答に近いので、回答として投稿します。

いろいろ試してみたところ、パターン文字列をつぎのようにすると、期待したように判定できるような気がします。(少なくとも match.Groups[1].Success == true にはなる。)
@"(?:.*(You have new mail).*|.*)Date: (\d\d)/(\d\d)"
ただし、なぜこれで判定できて、最初の質問のパターン文字列では判定できないのか理由が分かりません。

ちなみにつぎのようにしてもいずれも判定できませんでした。
@"(?:(You have new mail).*|.*)Date: (\d\d)/(\d\d)"
@".*(You have new mail)?.*Date: (\d\d)/(\d\d)"

編集 履歴 (0)
  • 本題とそれますが、提示された成功している正規表現ではグループの数/Indexの位置も変わっているので、グループ名指定に変えたほうが良いですよ。 -
  • こちらの勘違いでした、失礼しました。 -
regex.IsMatch("<html><body>Welcome to our Meeting Room.<br>You have new mail<br>All rights reserved.<br>Date: 03/05</body></html>")

の結果、trueで返ってくるのでマッチしているのでは?

match.Groups[0]

に結果が入っているような気がします。

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。