2014年10月23日木曜日

LINQ to Excelを使う

今回はLINQ to Excelを使ってみたいと思います。

ExcelのファイルをC#で読み込もうとしたら、少しググればCOM相互運用を使って使う方法が出てきます。でもこの方法、Excelを実際に起動させて自分のプログラムとやり取りするので画面がとても鬱陶しいことになりますし、おそらくExcelが無いと使えません。

ですが、このLINQ to Excel、若干癖があって、というよりか、使い方が一部直観的じゃなくてリファレンス無しにいろいろ使えるわけでもないんですよね(使い方がわかればとても便利だと思います)。一方、COM相互運用側からExcelを使う方法もあるため、特に日本語ではLINQ to Excelの記事はあまり見かけません(例によって自分のググり力が足りないだけかもしれませんが)。


とりあえず、適当なデータを用意しないとサンプルを示すに示せないので、こんなエクセルシートを用意してみました。にゃんぱすーฅ(๑'Δ'๑)
ファイルはxlsでもxlsxでもcsvでも読めるっぽいですが、どうもExcel2013で作ったxlsxは読み込めないっぽいです。Excel2010なら読み込めました。また、読み込もうとしているファイルをExcelで開いていたりすると途中で例外を吐いてしまいますので注意してください。

任意のセルを読み出す

LINQ to Excelは任意のセルを読み出すのにははっきり言って全くもって適していません。でも、まあ一応読み出せるので読み出し方を紹介します。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
Console.WriteLine(excel.WorksheetRangeNoHeader("B3", "B3", "Sheet1").First().First().Value.ToString());

とりあえず呪文のように思ってください。この記事を最後まで読むと意味がわかると思います。読み出すデータはSheet1のB3です。これを実行するとしっかりと
2002/05/28 0:00:00
と表示されます。

正攻法的に読み出す

さて、このLINQ to Excelは、基本的に上のエクセルシートのように1行目がヘッダーで、2行目以降がデータになっているデータを読み出すことに特化し、様々な機能が作られているようです。そのため、WorksheetはRowクラスの列挙可能型として定義されており、RowはList<Cell>を継承したものとして定義されています。
というわけで、正攻法的に読みだしてみます。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
var worksheet = excel.Worksheet("Sheet1");

var q = from p in worksheet
        select new {
            Name = p["名前"].Cast<string>(),
            Birthday = p["誕生日"].Cast<DateTime>(),
            Height = p["身長"].Cast<int>(),
            BloodType = p["血液型"].Cast<string>(),
        };

foreach(var p in q)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.Name, p.Birthday, p.Height, p.BloodType);

Sheet1のワークシートを読み込んでからLINQのクエリ文で匿名クラスを定義し、各行をデータ化しています。ワークシートは行の列挙可能型で、行からはインデクサーのオーバーロードでヘッダー名でその列に相当するセルを読み出しています。セルはobject型のValueプロパティを持っていますのでこれを適当にキャストして読み出してもいいのですが、例えばdoube型がobjectにキャストされているときはいきなりintにキャストできませんし、例えば越谷卓は血液型が空欄ですので、そのデータ型はDBNullになっているのでstringにキャストはできません。そういう関係で、Cast<T>()型のほうが使い勝手が良いですね。

データをヘッダーと同名のプロパティを持つクラスに読み込む

さて、続いてこのようなクラスを作ってみます。

public class Person
{
    public string 名前 { get; set; }
    public DateTime 誕生日 { get; set; }
    public int 身長 { get; set; }
    public string 血液型 { get; set; }
}

ヘッダーと同じ名前のプロパティを持ったクラスですね。違和感はとてもありますが、C#はUnicodeなので日本語プロパティも作ることができます。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
var worksheet = excel.Worksheet<Person>("Sheet1");

foreach(var p in worksheet)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.名前, p.誕生日, p.身長, p.血液型);

こうすることで、直接ワークシートをPersonの列挙可能型として読み込めるようになりました。超便利です。Cast<T>()も必要ないので、非常にすっきりしました。

データをヘッダーと同名のプロパティを持たないクラスに読み込む

しかし、常にヘッダーと同名のプロパティを持つクラスを用意できるかと言ったらそうとは限りません。ヘッダー名は実行するまでわからないかもしれませんし、そもそも上記のような日本語プロパティは回避したいときなどもあるでしょう。
大丈夫です、そのようなときも、極力簡単に特定のクラスにデータを読み込むことができます。

まずは同様にPersonクラスを作ってみます。

public class Person
{
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    public int Height { get; set; }
    public string BloodType { get; set; }
}

さて、当然のことながら、ヘッダーとプロパティ名が異なってしまったので、それを対応付ける作業をしなければいけません。それを「マッピング」と呼んでいるようです。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
excel.AddMapping<Person>(p => p.Name, "名前");
excel.AddMapping<Person>(p => p.Birthday, "誕生日");
excel.AddMapping<Person>(p => p.Height, "身長");
excel.AddMapping<Person>(p => p.BloodType, "血液型");
var worksheet = excel.Worksheet<Person>("Sheet1");

foreach(var p in worksheet)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.Name, p.Birthday, p.Height, p.BloodType);

このような形でマッピングを追加することでプロパティ名とヘッダー名の対応付けをすることができます。これでめでたくヘッダーと違う名前のプロパティでも適当に代入してくれるようになりました。

ちなみに、AddMapping<T>()の1つ目の引数のラムダ式が何これ?って思う人もいるかもしれませんが、これはいわゆる式木を使った技法で、こうすることでこのラムダ式を受け取った側はプロパティの名前が取得できるようになります。AddMappingのオーバーロードを見るとわかりますが、このような方法を使わずとも単にプロパティ名をstringで与えるオーバーロードもありますが、そのようなものに対して、こっちのほうがIDEのサポートを受けられる(サジェスト、リファクタリング等)ので好んで使われる技法のようです。MVVMのViewModelを作るときに通知可能プロパティで同様の技法を使ったりすることもあります。

データをヘッダーと同名のプロパティを持たないクラスに読み込む その2

上記のようにメソッドでマッピングする以外に、属性を使った方法もあります。

public class Person
{
    [ExcelColumn("名前")]
    public string Name { get; set; }

    [ExcelColumn("誕生日")]
    public DateTime Birthday { get; set; }

    [ExcelColumn("身長")]
    public int Height { get; set; }

    [ExcelColumn("血液型")]
    public string BloodType { get; set; }
}

このように、Personクラスのそれぞれのプロパティに属性をつけてやります。それだけでマッピングはしなくても同様に読み込めるようになります。簡単ですね。

ヘッダーが無いデータを読み込む

中にはヘッダーと呼べる行がなく、いきなり1行目からデータになっているエクセルシートもあるでしょう。そのようなものを読み込むこともできます。
本来は、サンプルコードとしてはWorksheetNoHeader()を呼び出すべきなんでしょうが、元のエクセルシートを書き換えるのもアレなので、下記のようにWorksheetRangeNoHeaderを呼び出しています。これは、指定した範囲内をヘッダーの無いデータとして認識するメソッドです。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
var worksheet = excel.WorksheetRangeNoHeader("A2", "D6", "Sheet1");

var q = from p in worksheet
        select new {
            Name = p[0].Cast<string>(),
            Birthday = p[1].Cast<DateTime>(),
            Height = p[2].Cast<int>(),
            BloodType = p[3].Cast<string>(),
        };

foreach(var p in q)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.Name, p.Birthday, p.Height, p.BloodType);

NoHeaderがつくメソッドを使った場合、行はRowクラスではなくRowNoHeaderクラスになります。これは、インデクサーにstringを受け取るオーバーロードがなく、セルのインデックスをint型で与えるしかありません。

LINQする

さて、ここまでデータの読み込み方が中心でした。肝心のLINQのクエリ文はあまり書いていません。まあ、どうせみんなわかってるだろうから解説しても仕方がないっていうのもあるとは思いますが。

var worksheet = excel.Worksheet<Person>("Sheet1");

var q = from p in worksheet
        where p.Birthday > new DateTime(2000, 1, 1)
        orderby p.Birthday descending
        select p;

例えばクエリ文はこんな感じで書けますね。もう至って普通です。
前述しましたが、worksheetは行の集まりなので、pは各行になります。と言っても、Worksheet<Person>で行はPersonに変換済みですから、ここから容易にPersonの条件を設定できます。こでは、2000年以降に生まれた人を若い人順に並べていますね。あまり説明はいらないと思います。
特定のクラスに変換しない場合はpがRowになりますので、where文の中では例えばp["誕生日"].Cast<DateTime>()みたいな書き方をすればいいと思います。若干煩わしいので、変換をかましたほうがきれいそうですね。



さて、ここまで解説すれば大体のExcelファイルについて「ヘッダー+データ」という構造ならば簡単に読み込めると思います。そういう構造じゃなければ、もしくは、ワークシートに対してセルのデータを読み出す以上の仕事を要求するのならそもそもLINQ to Excelは使えません。
そもそもExcel自体に強力なデータ整理機能があるので、あえてExcelでできるようなことをC#でやる必要はないとは思いますが、複数のExcelファイルにまたがった処理とか、毎回やるような簡単な処理をバッチ化したいとかでLINQ to Excelを活用できるととても便利かもしれません。

2014年10月15日水曜日

非同期処理とAggregateException

以前の記事で2048のGUIを作ろうとかオートソルバーを作ろうとか言っておきながら手が付いていないのですが、その件は一旦保留してまた別の記事を書いてみようと思います。

さて、以前、LINQ to Twitter v3の記事を書きましたが、ちょっとこれについてまたいじっていたら例外処理で躓きましたので、一般化して話をしていこうと思います。
例えばネットにつながっていないPCからLINQ to Twitterでタイムラインを読み込もうとしてしまうようなことがあるかもしれません。そういった場合、例外が吐かれます。 まあWebExceptionとかHttpRequestExceptionとかそのあたりが吐かれるんだろうなーと思って適当に例外処理していると痛い目に遭いました。AggregateExceptionとかいう見慣れない例外が吐かれます。

さて、AggregateExceptionが一体どのような例外なのかを調べるためにMSDNに行ってみます。

AggregateException クラス (System)

「アプリケーションの実行中に発生する1つ以上のエラーを表します。」って書いてありますがその本質はよくわかりません。どうもInnerExceptionsというコレクションがあり、複数の例外をこのクラスで保持することができるようですが、なぜそのような例外が吐かれるのかがわかりません。素直にWebExceptionとかじゃダメなの?複数の場所で例外が発生するような状況だったとしても、1つ目に遭遇した例外が吐かれればそれで問題ないよね?というか、通常の例外処理がそうでしょ?なのになぜ敢えてこのように複数の例外をまとめるような例外が吐かれるのでしょう。


その疑問に答えるべく、まずひとつこの例外を吐くメソッドを挙げてみます。
わかりやすい例としてParallel.Forが挙げられます。 指定した範囲の整数を受け取るデリゲートを同時並行で動かすメソッドです。もっと正確にいえば、同時並行に動かす可能性があるです。詳しい説明は省きますが、スレッドプールを利用して実行するため、環境や設定によっては単一スレッドでの実行になるかもしれませんし、その順序も、どの値同士が並列で実行されるかもわかりません。こんな運要素の大きなメソッドですが、この中で特定の値で例外を吐くようなプログラムを書いて実行してみます。

try {
    Parallel.For(0, 100, (i) => {
        if(i == 0)
            throw new InvalidOperationException();
        if(i == 1)
            throw new ArgumentException();
        Console.WriteLine(i);
    });
}
catch(AggregateException e) {
    ShowAggregateException(e);
}

ちなみにShowAggregateExceptionメソッドは次のように実装しています。

static void ShowAggregateException(AggregateException ae)
{
    ShowAggregateException(ae, 0);
}

static void ShowAggregateException(AggregateException ae, int depth)
{
    Console.WriteLine(new string(' ', depth) + ae.GetType().Name);
    foreach(var e in ae.InnerExceptions) {
        if(e is AggregateException)
            ShowAggregateException((AggregateException)e, depth + 1);
        else
            Console.WriteLine(new string(' ', depth + 1) + e.GetType().Name);
    }
}

いわゆる再帰ですね。AggregateExceptionに含まれるすべての内部例外を表示しますが、その時にその内部例外にAggregateExceptionが含まれていたら再帰的にこいつもこのメソッドで表示させています。そして、階層が深まるたびにインデントを追加しています。new string(' ', depth + 1)がそれにあたります。

さて、これを実行するわけですが、VisualStudioを使う場合は「デバッグなしで開始」で実行する必要があります。「デバッグ開始」でやると、下の図のようにthrow文のところで「ハンドルされていない例外としてVisualStudioにキャッチされてしまい、AggregateExceptionが活躍しているところが見られません。


同じコードでデバッグ時の挙動とデバッグしないときの挙動で違うのも困りものですが、まあ、AggregateException行きになる時点でハンドルされていない例外とも言えるのか…な?なんていうか、単にVisual Studioの対応力不足な気もしますが。
さて、話を戻します。実際にデバッグなしで実行してみると、例えば下図のような実行結果が得られます。


表示されている数字が昇順や降順でないあたり、並列処理が実行されていそうな雰囲気ですね。
そして、InvalidOperationExceptionとArgumentExceptionが同時に吐かれていることがわかると思います。なるほど、確かにこういう場面を想定するとAggregateExceptionも役に立ちそうですね。
ちなみにですが、先ほども言いましたがこのParallel.Forはかなり気まぐれなメソッドです。何回か実行してみると、表示される数字の順番は毎回異なりますし、InvalidOperationExceptionしか例外が吐かれないこともあります。おそらくどこか1つでも例外を吐いたら直ちに新しい処理を開始するのをやめてメインルーチンに処理を返す仕様になっているんでしょうね。たまたま0と1が別スレッドで同時に実行された時のみ、上記のコードではこのような実行結果を吐くでしょう。なので、もしかしたらシングルコアのCPUのマシンとかだったらこれは一生再現できないかもしれませんね。


さてさて、ここではParallel.Forを使ってみましたが、もともとはこれは非同期処理全般に使われている例外のようです。非同期処理の基礎で出てくるこのあたりのメソッドやプロパティでもAggregateExceptionが吐かれるようです。

Task.Wait メソッド  (System.Threading.Tasks)
Task(TResult).Result プロパティ  (System.Threading.Tasks)

Task.Resultのほうには明言されていませんが、これはタスクの終了を待ってから制御を返すプロパティなので、内部的にWaitを呼んでいるのか知りませんが、これもAggregateExceptionを吐くことがあるようです。

Task t = Task.Factory.StartNew(() => {
    throw new InvalidOperationException();
});
Thread.Sleep(1000);
try {
    t.Wait();
}
catch(AggregateException e) {
    ShowAggregateException(e);
}

さて、このように非同期処理中で例外を吐くプログラムを書いてみましょう。
タスクを開始してからメインスレッドでは怪しげな1秒のスリープを入れてみました。こうすることで、t.Wait()が呼ばれる前に例外を吐くスレッドのほうではいかにも例外を吐いたであろう状態にあることは明らかです(もちろん、スレッドプールが2スレッド以上あることが前提ですが)。ですが、その例外は即座にプログラムの実行に影響を及ぼしません。t.Wait()が呼び出されたときに例外が吐かれます。予想通り、AggregateExceptionの中にはInvalidOperationExceptionが1つ入っています。

では、この別スレッドの中でさらに別スレッドにを実行し、そこで例外を吐かせてみたらどうでしょう。

Task t = Task.Factory.StartNew(() => {
    Task.Factory.StartNew(() => {
        throw new ArgumentException();
    }, TaskCreationOptions.AttachedToParent);
    throw new InvalidOperationException();
});
try {
    t.Wait();
}
catch(AggregateException e) {
    ShowAggregateException(e);
}

ここで、子スレッドにはTaskCreationOptions.AttachedToParentオプションを付けていることに気を付けてください。これを付けないと、ArgumentExceptionを吐くスレッドが終わる前にInvalidOperationExceptionを吐くスレッドが終わってしまって、ArgumentExceptionのほうがAggregateExceptionに含まれなくなってしまいます。


この通り、見事にInvalidOperationExceptionとAggregateExceptionを含むAggregateExceptionが吐かれました。


さて、ここまで見てきたら、何となくなぜ非同期処理や並列処理でAggregateExceptionが必要なのかがわかってきたかと思います。非同期処理や並列処理は根本的に複数の例外を同時に吐く可能性をはらんでいる点があるからAggregateExceptionが必要というわけです。
しかし、よくよく考えれば、例えば別スレッド内での例外をそのままWait()呼び出し時に再スローするような仕組みをつくった場合、Waitメソッドが吐く例外の種類をメソッドのドキュメンテーションに定義できなくなりますし、Waitメソッドそのものが吐いた例外なのか、それとも別スレッド内のユーザーが書いた処理に起因する例外なのかを判別することもできません。そういう意味からも、例外をカプセル化してしまったほうが良い、といった判断なのかもしれません。

最後に、少し実用的なコードを書いてみようと思います。

using(WebClient wc = new WebClient()) {
    var t = wc.DownloadStringTaskAsync(@"http://www.google.co.jp");
    try {
        Console.WriteLine(t.Result);
    }
    catch(AggregateException ae) {
        ae.Handle((e)=>{
            if(e is WebException) {
                Console.WriteLine(e.GetType().Name);
                Console.WriteLine(" " + e.Message);
                return true;
            } else
                return false;
        });
    }
}

こんな感じでどうでしょうか。
非同期にGoogleのトップページのソースをダウンロードしています。正常にダウンロードできれば画面にそのテキストが表示されます。一方、正常にダウンロードできなかったらAggregateExceptionが発生します。このHandleメソッドでは、InnerExceptionsに含まれてるすべての例外についての例外処理のデリゲートを 書いています。処理した例外はtrueを返し、処理しなかったらelseを返します。
例えばデスクトップPCならLANケーブルを引っこ抜いて、ノートPCなら機内モードにするなどで正常にネットにつながらない状態にしてみましょう。そうすると、見事にAggregateExceptionが吐かれ、そのWebExceptionについての例外情報が画面に表示されます。

LINQ to Twitter v3でもクエリが並列非同期処理となっているためか例外はAggregateExceptionに内包される形で吐かれるようです。なので、上記のようにAggregateExceptionを処理する形に書き直しておけば、各々の例外に対応することができるようになります。


今回は非同期処理の例外処理について紹介しました。
非同期処理や並列処理の簡単さはC#5.0/.NET4.5あたりの目玉機能ですが、この辺りを使うときはぜひとも気を使ってみたいところですね。

2014年10月9日木曜日

Html Agility PackとXPath

今回はC#でHTMLパースをしてみようと思います。

C#でHTMLをパースする方法をググると真っ先に出てくるのがHtml Agility Packというライブラリです。しかし、Documentationは何も書いておらず、ググって出てくる断片的な記事しか無いので、私も1つの断片的な記事を書いてみようと思います(

さて、このライブラリ、どうもXPathというものと切っても切れない関係があるようです。
XPathというものを知らなかった私は、ググりながら、同時にHtml Agility Packを使っていかなければなりませんでした。
XPathというのは、XMLにおいて特定の場所を指し示すための言語みたいですね。
すなわち、このライブラリでは、HTMLの中の特定のタグ等をXPathを使って検索し、そこから目的の属性とか内容を取得するといった流れになっているようです。

まずは、HtmlDocumentのインスタンスの生成です。このインスタンスに対していろいろいじくり回すので、まずはこれを作らなければなりません。

HtmlDocument html = new HtmlDocument();

using(WebClient wc = new WebClient())
using(Stream s = wc.OpenRead(URL)) {
    html.Load(s);
}

このライブラリは直接WebからHTMLを取得するということはしないようなので、このように適当にWebClientを使って読みだしてWebからHTMLを取得しています。ローカルの場合は、パスを指定するだけで読み出せるLoadメソッドのオーバーロードがあるのでそれを使えば良いでしょう。

では、実際にXPathで読みだしてみます。

HtmlNode head = html.DocumentNode.SelectSingleNode(@"//head//title");
Console.WriteLine(head.InnerText);

例えば上記のHtmlDocumentクラスの生成のときのURLをhttp://www.google.co.jpにしてこのプログラムを実行すると「Google」という文字列が表示されます。DocumentNodeプロパティはHTMLの中身全体を含むHtmlNodeクラスのインスタンスです。この子ノードを見てみると例えば<html>などを含んでいるのでそのことからもわかると思います。

HtmlNodeクラスは、一つのタグで表されるノードの内容を保持するクラスです。いろいろなプロパティがありますが、主に使うのは
  • Name(このノードの名前)
  • Attributes(このノードの属性のコレクション)
  • ParentNode(親ノード)
  • ChildNodes(子ノードのコレクション)
  • InnerText(そのノードに含まれるテキスト。子ノードを含む場合はタグなどを差し引いたテキスト部分全体)
  • InnerHtml(そのノードに含まれるHTML。子ノードを含む場合はタグ等もすべて含まれたHTMLがstringで格納されている)
あたりですかね。今回のテーマにXPathというのがありますが、それを完全に無視して、ルートノードからこれらのプロパティをLINQで探索して条件に合うノードのコレクションを抽出することはできなくはないですが、LINQのメソッドチェーンがとても長くなったり、ラムダ式が大量に入ったりして可読性が著しく悪くなります。多分。

次は、XPathでの検索のしかたです。XPathで検索するのによく使うメソッドを紹介します。
HtmlNodeクラスのメソッドでSelectNodes()メソッドとSelectSingleNode()メソッドです。これの唯一の引数に、XPathの構文のテキストを与えればおkです。この2つのメソッドの使い分けは簡単で、前者は条件に合うノードのコレクションを返し、後者は条件に合うコレクションの最初のノードを返します。すなわち、イメージとしてはSelectNodes().FirstOrDefault()って感じですね。はい、ここからもわかるように、SelectSingleNodeメソッドは該当するノードが無い場合はnullを返します。では、該当するノードが無いときにSelectNodes()はどうなるのかと言うと、空のコレクションではなくnullを返します。これは個人的にクソ仕様だと思っています。すなわち、SelectNodes().FirstOrDefault()という書き方をして該当するノードが無かった場合はFirstOrDefault()がnullを返すのではなく、null.FirstOrDefault()になるためにNullReferenceExceptionが発生します。これはぜひとも改善してほしいところですね。いや、こういう仕様にした拡張メソッドでも実装して自分でそれを使ってしまうのが一番なのかもしれませんが。


さて、それでは実際にXPathの中身を見てみましょう。上記のXPathは
//head//title
となっています。 文頭の/はルートノードを表します。文中の/は、その子ノードを表します。WindowsやUNIX系とかのディレクトリ階層の表し方そっくりなので、ここは感覚的にも理解しやすいかと思います。そして、//になると、子だけではなく子孫ノード全体を表します。すなわち、子、孫、ひ孫など、いくら階層が下のノードでも条件に合致するようになるわけですね。

Googleのトップページのソースを見てみるとわかると思いますが、htmlノードの子のheadノードの下にmetaノードが何層かあって、そのあとにtitleノードがあります。丁寧にmetaノードを書くのが面倒なので、「htmlノードの子孫ノードのtitleノード」という意味で、このような構文にしてみました。


さて、今回は簡単にtitleノードを抽出するXPathでしたが、ほかにもいろいろとやりたいことは増えてくると思います。例えば、特定の子ノードを持つノードを抽出したいとか、特定の属性を持つノードを抽出したいとか、そういった話があるかと思います。
大丈夫です。そういった構文もXPathにはあります。

HtmlNode div = html.DocumentNode.SelectSingleNode(@"//div[@id=""hplogo""]");
Console.WriteLine(div.Attributes["style"].Value);

これも同じくGoogleのトップページをパースするプログラムです。これを実行すると
height:110px;width:276px;background:url(/images/srpr/logo9w.png) no-repeat
という出力が得られます。
//余談ですが、エスケープを無視するC#の文字列の@""っていうやつ、ダブルクオーテーション2個「""」で「"」を表すんですね。知りませんでした。
こちらのXPathでは鍵かっこ[]が出てきています。これが条件式です。鍵かっこ外に合致するノードのうち、鍵かっこ内の条件に合致するものが返されます。
ちなみに、鍵カッコ内の@ですが、これは属性を示す記号です。すなわち、@idでidという属性という意味ですね。[@id]でidという属性を持つノードという意味になりますが、="hplogo"でさらにその属性の値がhplogoに一致するという意味になります。何も難しくありません。
ちなみに、属性を示す@ですが、正式にはattribute::と書くことになっているようですが、この省略記法として@が使えるという立ち位置のようです。attribute::とかいちいち書いていると冗長なので、このような省略記法は積極的に使っていきたいですね。
というわけで、このXPathは、ルートノード以下のすべてのdivノードのうちidという属性を持ち、その値がhplogoのノードを検索するといった意味になります。次の行では、divの属性のうち、style属性の値を画面に表示しています。特に説明は無くても、C#erなら普通にわかる表現かと思います。

また、検索条件として、「特定の属性を持つ」だけでなく、「特定の子ノードを持つ」という条件を課したくなることも結構あると思います。それは、[child::hoge]と書くのがいわゆる省略しない記法です。省略記法は「何も書かない」なので[hoge]になります。

ちなみに、この条件式の鍵カッコは入れ子にもできますし([hoge[@id="foo"]]など)、複数並べるとandの意味になります([@id="hoge"][@style="foo"]など)。//node1[@id="hoge"]/node2みたいな書き方をして、特定の条件を満たすノードの子ノードなどを示すこともできます。

ここまでくればだいたいの条件式は書けるようになると思います。まだいろいろな条件の書き方はあるようですが、この辺の基本がわかっていればあとはググればなんとかなるでしょう。はい。

というわけで、XPathの導入をやってみました。
ちょっとしたウェブページの情報を読み出すソフトとかを作るときに使えそうな技ですね。