ラベル C# の投稿を表示しています。 すべての投稿を表示
ラベル C# の投稿を表示しています。 すべての投稿を表示

2012年6月20日水曜日

Open XMLでExcelを生成する

@ryuichi111std氏からのお仕事紹介でサーバサイドでのExcel出力機能を承り、Excel周りの作業の調査をしたら時代の移り変わりを実感したので解説。

一昔前のExcelのオートメーションと言えば、ExcelのオブジェクトをCOM InteropやらCOM Callable WrapperやらでごにょごにょしてぐりぐりしてどこやらでCellオブジェクトの解放忘れやらシートとブックの解放の順番間違いやらをしてしまいメモリリークという悲劇を生む精根尽き果てる作業というのが筋だったけれど、今やそれも昔。今やOpenXMLという規格があるのでそれに準じた形のXMLを吐き出すことで、あらまOffice 2007/2010対応のファイルができちゃった、という寸法になっている。

で、OpenXMLの初期の初期のころはそんなXMLのフォーマットをごりごりと手動で実装するというキチガイ沙汰の作業が横行していたようだけれど、そんな時代が長く続くはずもなくOpenXMLのフォーマット形式で出力してくれるSDKが登場した。それが下記。

Open XML SDK 2.0 for Microsoft Office
OpenXMLSDKv2.msiの方をインストールしてね。ToolのほうはExcelファイルを渡すとそのファイルを作成するためのコードを生成してくれるというツールなので興味がある人はどうぞ。

で、OpenXMLSDK、実はこいつもかなり使いづらい。かなりどころか、ディモールト(非常に)使いづらい。使いづらいどころかまったくこなれておらず、かなり分かりづらい仕様となっているOpenXMLを単純にそのままラップしたような構成になっているので、とっても廻りくどいしバグが入り込みやすい。

一体OpenXMLSDKを使っての開発がどういうものになるのか、下記のコードを参照してもらうのが一番手っ取り早いと思う。
Using C# and Open XML SDK 2.0 for Microsoft Office to Create an Excel 2007 Document
Creating basic Excel workbook with Open XML

で、めんどくせーなー、と思いつつ実装していたら、di molto(非常に)使いやすいライブラリを発見したのでご紹介。それが下記。

ClosedXML - The easy way to OpenXML

ClosedXMLはOpenXMLSDKをラップして使いやすくしたライブラリで非常に直観的。下記のコードはワークブックを作成してシートを追加してA1セルに値を設定して名前を付けて保存している。下記と同等のコードをOpenXMLSDKだけで行おうとすると50行~100行ほどの実装が必要になる。

var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sample Sheet");
worksheet.Cell("A1").Value = "Hello World!";
workbook.SaveAs("HelloWorld.xlsx");

ほかにもほとんどのパターンに対応するサンプルコードが豊富にあるので詳細はDOCUMENTATIONを参照してもらいたい。

このプロジェクトが気に入ったらブログで紹介してね、とClosedXMLのサイトにあったのでご紹介。このエントリーをご覧になられた方もどうぞよろしく。

2011年11月18日金曜日

C#プログラムからPOSTまたはPUTしてみる

C#のプログラムからデータをPOSTなりPUTしたいときのコードスニペット。

var request = (HttpWebRequest)WebRequest.Create("http://somewhere");
var buffer = Encoding.UTF8.GetBytes("data1=hello&data2=hi&data3=234");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = buffer.Length;
request.CookieContainer = new CookieContainer();

var requestStream = request.GetRequestStream();
requestStream.Write(buffer, 0, buffer.Length);
requestStream.Flush();
requestStream.Close();

WebResponse response = null;
try
{
    response = request.GetResponse();
    var stream2 = response.GetResponseStream();
    var reader2 = new StreamReader(stream2);
    var resultText = reader2.ReadToEnd();
}
catch (Exception ex)
{
    if (response != null)
    {
        response.Close();
        response = null;
    }
}
finally
{
    request = null;
}

2011年9月18日日曜日

名前付き引数のすすめ

仕事でVB.NETとJavascriptを使い、自分のプロジェクトではC#とJavascriptにPythonと使っているのだけれど、プロジェクトをスイッチさせたときに頭の中のSyntaxがごちゃごちゃになっててスラスラと書き始めるまでしばらく時間がかかったりする。そのせいもあるのか自分のコードを忘れる時間も早くなっている気がする。

で、先日、自分の組んだJavascriptのコードに機能を追加しようと眺めていたら下記のようなコードがあってハタと手が止まってしまった。

$('.class').disable(true);

・・・disableにtrueってなんだろ?有効、非有効の表示を切り替えるためにjqueryのpluginを作ったのまでは覚えているけれど、その引数の意味が思い出せない。パッと見て類推できるのは、disableにする、しないのフラグかなと思ったのだが、enableというpluginも用意したはずなのでそれは当たらない。で、良く分からないのでpluginの実装を見たら下記のようだった。

disable:function(clearValue){
 if (clearValue){
  // code here
 }
}

ということであのbooleanはdisableついでに値も消去するかのフラグだった。これがC#、VB.NETだとまだ楽で優秀なVisual Studioのインテリセンスがすぐにどういう値なのか教えてくれるし、PythonにしてもPyDevなどがあればDocumentが表示されるので簡易に分かったりする。が、Visual StudioでJavascriptのインテリセンスを使用するためにはReferenceタグを各ファイルに追加しなければいけなかったりと若干面倒だったりするので、Syntaxとして名前付き引数が用意されていないJavascriptで、コードを用いて同じ機能を実現しようとするならば下記のようになる。

item.disable({clearValue:true});

disable:function(options){
 options = options || { clearValue:false};
 if ( options.clearValue){
  // code here
 }
}

これでコードの可読性はあがったけれど若干冗長なのは否めない。ただ冗長だからという理由だけで、常にBoolean、Integer、Stringなどのプリミティブな値を定数としてそのまま引数として渡すのは、コードを管理していく上で良い方法だとは思わない。なので、変数名などからその引数を類推できない場合などは極力名前付き引数を使用するほうが可読性の向上 → メンテナンス性の向上というコンボになるので、上記のような状況では名前付き引数を使用するのが良いだろう。


蛇足ながらそれぞれの言語で名前付き引数で上記のシグネチャの関数を呼び出すと以下のような感じになる。

// c#
item.Disable(clearValue:true);

' VB.NET
item.Disable(clearValue:=True);

# Python
item.disable(clearValue=True);

2011年5月20日金曜日

Google App EngineにC#プログラムからログインする方法

今回はGoogle App Engine(GAE)にC#プログラムからログインする方法を解説する。

GAEで特定のUrlに対して、特定のユーザのみにアクセスを許可する場合はapp.yamlで下記のように設定をすればよい。

app.yaml
- url: /do/something/
  script: something.py
  login: required

これでGAEアプリのユーザのみが上記Urlへのアクセスが可能となる。ユーザではない、または未ログインのものが上記UrlにアクセスするとGoogleアカウント(またはOpen Id)のログインページへとリダイレクトされる。そこでユーザ名とパスワードを入力してログインすれば上記のUrlへとリダイレクトバックされる。これがブラウザ上の動作であれば問題はないが、プログラム上から認証を必要とするUrlへとアクセスする場合は困る。

認証を必要とするGAEアプリのUrlに対してリクエストを行う場合は下記の手順で行う必要がある。

  1. ClientLoginサービスにログインしAuthトークンを取得する
  2. 取得したAuthトークンを利用しGAEアプリにログインしCookieを取得する
  3. 取得したCookieとともにHttpGETやらHttpPOSTを行う

各項目を順を追って説明する。

ClientLoginサービスにログインしAuthトークンを取得する
string GetAuth()
{
    var request = (HttpWebRequest)WebRequest.Create("http://www.google.com/accounts/ClientLogin");
    var content = "Email=test@gmail.com&Passwd=testpass&service=ah&accountType=HOSTED_OR_GOOGLE";
    var byteArray = Encoding.UTF8.GetBytes(content);
    request.ContentLength = byteArray.Length;
    request.ContentType = "application/x-www-form-urlencoded";
    request.Method = "POST";
    var dataStream = request.GetRequestStream();
    dataStream.Write(byteArray, 0, byteArray.Length);
    dataStream.Close();
    var response = (HttpWebResponse)request.GetResponse();
    var stream = response.GetResponseStream();
    var reader = new StreamReader(stream);
    var loginStuff = reader.ReadToEnd();
    reader.Close();

    var auth = loginStuff.Substring(loginStuff.IndexOf("Auth")).Replace("Auth=", "").TrimEnd('\n');
    return auth;
}
まずhttp://www.google.com/accounts/ClientLoginへと必要な情報をポストする。各項目の詳細な説明は下記を参照して欲しい。
ClientLogin for Installed Applications

今回は最終的にGAEアプリへとログインしたいのでserviceはahとなる。これがカレンダーであればclなどそれぞれに対応したものへと変わる。ログインに成功するとSID、LSIDとともにAuthが返却されるのでAuthのみ取得する。他の二つは不要。

取得したAuthトークンを利用しGAEアプリにログインしCookieを取得する
CookieContainer GetCookies(string auth)
{
    var cookies = new CookieContainer();
    var url = string.Format("http://test.appspot.com/_ah/login?auth={0}",
                            System.Web.HttpUtility.UrlEncode(auth));
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.AllowAutoRedirect = false;
    request.CookieContainer = cookies;
    request.ContentType = "application/x-www-form-urlencoded";
    request.Method = "GET";
    var response = (HttpWebResponse)request.GetResponse();
    var stream = response.GetResponseStream();
    var reader = new StreamReader(stream);
    var result = reader.ReadToEnd(); 
    reader.Close();
    return cookies;
}
先ほど取得したAuthトークンを使用しGAEアプリへとログインする。実際にはhttp://test.appspot.comの部分をアクセスしたいGAEアプリのUrlへと変更して欲しい。ログインに成功するとACSIDというクッキーが返却されているはずだ。余談ながらGAEアプリへのログイン後、POSTではなくGETでよい場合は、下記のようにリクエストすれば指定のUrlへとリダイレクトされる。

http://test.appspot.com/_ah/login?auth={0}&continue=http://test.appspot.com/do/something/

ただ今回はPOSTでリクエストしたいのでAllowAutoRedirect=falseで自動リダイレクトを無効化している。


取得したCookieとともにHttpGETやらHttpPOSTを行う
void PostToGAE()
{
    var auth = GetAuth();
    var cookies = GetCookies(auth);

    var url = string.Format("http://test.appspot.com/do/something/");
    var content = "testvalue=test";
    var request = (HttpWebRequest)WebRequest.Create(url);
    request.KeepAlive = false;
    request.CookieContainer = cookies;
    var byteArray = Encoding.UTF8.GetBytes(content);
    request.ContentLength = byteArray.Length;
    request.ContentType = "application/x-www-form-urlencoded";
    request.Method = "POST";
    var dataStream = request.GetRequestStream();
    dataStream.Write(byteArray, 0, byteArray.Length);
    dataStream.Close();
    HttpWebResponse response = (HttpWebResponse)request.GetResponse();
    var stream = response.GetResponseStream();
    var reader = new StreamReader(stream);
    var result = reader.ReadToEnd();
    reader.Close();
}
取得したCookieをそれ以降のリクエストと一緒に送ってやれば認証されたユーザとしてサーバサイドで認識される。

※見て分かると思うけれど、上記方法は中間でトラフィックを監視している人物がいた場合に、その人物がクッキーを使いまわすだけで簡単になりすましが可能となる。そのため繊細な情報などを扱う場合はhttpsを使用するのが望ましい。httpsを使用した場合は返却されるクッキー名がSACSIDとなる。


仕組みが分かれば至極単純なのが理解できたと思う。上記のコードは動作テスト用に組んだものなので例外処理などまったくの手付かずなので使用される場合は随時手を加えてもらいたい。


余談
私がこの動作テストを行ったときに正常に動作させるまで二日ほどかかってしまった。その要因はサーバサイドのハンドラーをDjango-nonrelで実装していたのだが、POSTリクエストがcsrf protectionに引っかかっていたせいだった。ずっとGAEの認証側の問題だと考えてそちらばかり追っかけていたので、Djangoのほうまで頭が回らなかった。気付いてしまえば単純な問題だけれど、はまるときははまる。仮にDjango-nonrelを使用する場合は忘れずにハンドラーへ@csrf_exemptを付与するようにしてほしい。

下記、はまっているときにstackoverflowへ質問した内容。
Programmatically login to google app engine c#

2011年3月8日火曜日

HttpWebRequestで画像をASP.NET MVCに送ってみる

当ブログ100回目のポストはHttpWebRequestを使って画像をASP.NET MVCで稼動しているサイトへアップロードする方法を解説する(元ネタはStackOverflow)。

クライアント - アップロード
static void UploadFile(string url, string file, string paramName, string contentType)
{
    var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
    var boundarybytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");

    var wr = (HttpWebRequest)WebRequest.Create(url);
    wr.ContentType = "multipart/form-data; boundary=" + boundary;
    wr.Method = "POST";
    wr.KeepAlive = true;
    wr.Credentials = System.Net.CredentialCache.DefaultCredentials;

    var rs = wr.GetRequestStream();
    var headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\nContent-Type: {2}\r\n\r\n";
    var header = string.Format(headerTemplate, paramName, file, contentType);
    var headerbytes = System.Text.Encoding.UTF8.GetBytes(header);
    rs.Write(headerbytes, 0, headerbytes.Length);

    var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read);
    var buffer = new byte[4096];
    var bytesRead = 0;
    while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
    {
        rs.Write(buffer, 0, bytesRead);
    }
    fileStream.Close();

    var trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");
    rs.Write(trailer, 0, trailer.Length);
    rs.Close();

    WebResponse wresp = null;
    try
    {
        wresp = wr.GetResponse();
        var stream2 = wresp.GetResponseStream();
        var reader2 = new StreamReader(stream2);
        var result = reader2.ReadToEnd();
        Console.WriteLine(string.Format("File uploaded. Server response is: {0}", result));
    }
    catch (Exception ex)
    {
        if (wresp != null)
        {
            wresp.Close();
            wresp = null;
        }
        Console.WriteLine("Exception occurred!! " + ex.ToString());
    }
    finally
    {
        wr = null;
    }
}
データをポストするときのフォーマットを正確に作ってやって、そこに送りたいデータをはめ込むだけ。

クライアント - 使い方
UploadFile("http://localhost:55778/Image/PostImage",
          @"C:\test.JPG",
          "file", "image/jpeg");

ASP.NET MVC側 - ImageController
[HttpPost]
public int PostImage()
{
    var request = base.HttpContext.Request;
    if (request.Files.Count == 0)
        return 0;

    var httpPostedFile = request.Files[0] as HttpPostedFileBase;
    httpPostedFile.SaveAs("tekitouna_path.jpg");
    return 1;
}

上記のコードをASP.NET MVCサイトを動作させた状態でクライアントを実行すればサーバ側でファイルを受け取り保存することが可能なはずだ。

2011年2月20日日曜日

Gmailの内容をIMAPで取得する LumiSoft.Net編

前回のGmailの内容をIMAPで取得する ImapX編に引き続き、今回は下記ライブラリを使用してGmailの内容を取得する方法を解説する。

LumiSoft.Net
LumiSoft Forum
Source code
Examples

ImapXに比べてこなれていないObject Modelなのでとっつきにくいけれど、慣れればImapX以上の柔軟さがあるし、何よりOSSなので自分で問題解決できるのが良い。

コードはこんな感じ。
// すべてのメールを取得する
try
{
    using (var imap = new IMAP_Client())
    {
        imap.Logger = new Logger();
        imap.Logger.WriteLog += (s, e) => Console.WriteLine(e.LogEntry.Text);
        imap.Connect("imap.gmail.com", 993, true);
        imap.Login("username", "password");
        imap.SelectFolder("INBOX");

        var fetchHandler = new IMAP_Client_FetchHandler();
        fetchHandler.NextMessage += (s, e) =>
        {
            // 次のメッセージ
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("New message");
        };
        fetchHandler.Envelope += (s, e) =>
        {
            // To, Fromとか
            Console.WriteLine("Envelope");
            var envelope = e.Value;
            var from = "";
            if (envelope.From != null)
                from = envelope.From.ToList().Select(x => x.ToString()).Aggregate((x, y) => string.Format("{0};{1}", x, y));
            else
                from = "none";

            Console.WriteLine(from);
            var subject = envelope.Subject ?? "none";
            Console.WriteLine(subject);
        };
        fetchHandler.Flags += (s, e) =>
        {
            // フラグ(SEEN, UNSEENとか)
            e.Value.ToList().ForEach(f => Console.WriteLine(f));
        };
        fetchHandler.InternalDate += (s, e) =>
        {
            // 日付
            Console.WriteLine(e.Value.ToString());
        };
        fetchHandler.Rfc822Size += (s, e) =>
        {
            // サイズ
            Console.WriteLine(((decimal)(e.Value / (decimal)1000)).ToString("f2") + " kb");
        };
        fetchHandler.UID += (s, e) =>
        {
            // メールID
            Console.WriteLine(e.Value);
        };

        var sequence = new IMAP_SequenceSet();
        sequence.Parse("1:*");  // 取得する範囲を指定する
        // ※メールが15通あるとして
        // 2,4:7,9,12:*を指定すると
        // 2,4,5,6,7,9,12,13,14,15の順番のメールが取得できる
        // 詳細はIMAP_SequenceSetを参照のこと

        // 第一引数は指定のsequenceがUIDかどうかの判定用。UIDを指定する場合もIMAP_SequenceSetの記述は変わらない
        // 第三引数に指定したものがサーバーから取得され、値の解析後、fetchHandlerがコールバックされる
        imap.Fetch(false, sequence, new IMAP_Fetch_DataItem[]
                                                            {
                                                                new IMAP_Fetch_DataItem_Envelope(),
                                                                new IMAP_Fetch_DataItem_Flags(),
                                                                new IMAP_Fetch_DataItem_InternalDate(),
                                                                new IMAP_Fetch_DataItem_Rfc822Size(),
                                                                new IMAP_Fetch_DataItem_Uid()
                                                            }, fetchHandler);
    }
}
catch (Exception x)
{
    Console.WriteLine(string.Format("IMAP server returned : {0}", x.Message));
}

一見して分かる通りかなり癖があるのでとっつきにくい部分もあるが、慣れてしまえば細かいところまで制御できるので非常に有用だ。

もうひとつサンプルとして未読メッセージのみ取得する方法を紹介しておく。
try
{
    using (var imap = new IMAP_Client())
    {
        imap.Logger = new Logger();
        imap.Logger.WriteLog += (s, e) => Console.WriteLine(e.LogEntry.Text);
        imap.Connect("imap.gmail.com", 993, true);
        imap.Login("username", "password");
        imap.SelectFolder("INBOX");
        
        // 未読のものだけ取得
        var unseenMessages = imap.Search(false, "", "unseen");
        unseenMessages.ToList().ForEach(uid =>
        {
            var seqSet = new IMAP_SequenceSet();
            seqSet.Parse(uid.ToString());

            // Bodyのみハンドル
            var fetchHandler = new IMAP_Client_FetchHandler();
            fetchHandler.Rfc822 += (s, e) =>
            {
                var storeStream = new MemoryStream();
                e.Stream = storeStream;
                e.StoringCompleted += (s2, e2) =>
                {
                    storeStream.Position = 0;
                    var mime = Mail_Message.ParseFromStream(storeStream);

                    Console.WriteLine("Attachment");
                    foreach (var entity in mime.Attachments)
                    {
                        if (entity.ContentDisposition != null && entity.ContentDisposition.Param_FileName != null)
                        {
                            // 添付ファイルの保存
                            var path = Path.Combine(@"C:\", entity.ContentDisposition.Param_FileName);
                            File.WriteAllBytes(path,((MIME_b_SinglepartBase)entity.Body).Data);
                            Console.WriteLine(string.Format("{0} saved", entity.ContentDisposition.Param_FileName));
                        }
                        else
                        {
                            Console.WriteLine("untitled");
                        }
                    }

                    if (mime.BodyText != null)
                        Console.WriteLine(mime.BodyText);

                     // おまけ
                     // テスト用に今回の処理で既読メールになったのを未読メールに戻す
                     imap.StoreMessageFlags(true, seqSet, IMAP_Flags_SetType.Replace, IMAP_MessageFlags.Recent);
                };                
            };

            imap.Fetch(true, seqSet,
                new IMAP_Fetch_DataItem[]
                    {
                        new IMAP_Fetch_DataItem_Rfc822()
                    },fetchHandler);
        });
    }
}
catch (Exception x)
{
    Console.WriteLine(string.Format("IMAP server returned : {0}", x.Message));
}

Gmailの内容をIMAPで取得する ImapX編

送信されてきたメールをIMAPで取得、解析して内容をDBに突っ込もう、というツールが必要だったので良いライブラリが無いかと探して見つけたのが下記。

ImapX – free for use .NET library
数あるライブラリの中でもObject Modelがよく設計されているので至極簡単に使えて便利。ただOpen Sourceではないので何か問題があったときに何も対処できないし、作者のレスポンスもかなり遅いので対応は期待できないので注意が必要だ。

今回の要件は下記。
  • Gmailを使う
  • 画像の添付がある
  • IMAPを使う(未読メールのみ取得したいため)

GmailのアカウントでIMAPを使えるようにする方法は下記を参照のこと。
Enabling IMAP

で、実際のソースコードはこんな感じ。
var client = new ImapX.ImapClient("imap.gmail.com", 993, true);
var result = false;

result = client.Connection();
if (result)
    Console.WriteLine("@Connected");

result = client.LogIn("username", "password");
if (result)
    Console.WriteLine("@Logged in");

// 未読のものだけ取得
var messages = client.Folders["INBOX"].Search("UNSEEN", true);  // Searchの第二引数をfalseにする場合は、後でMessage.Processを呼び出す必要がある
foreach (var m in messages)
{
    //m.Process();  // Searchの第二引数がfalseの場合はこの処理が必要
    m.SetFlag(ImapX.ImapFlags.SEEN);    // 既読フラグを設定
    foreach (var attachment in m.Attachments)
    {
        var location = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
        attachment.SaveFile(Path.Combine(location, m.From[0].Address));     // SaveFileを呼び出すと添付ファイルのファイル名で指定の場所に保存してくれる
    }

    // テキストをデコードする
    var encoding = Encoding.GetEncoding("iso-2022-jp");  // 実際にはContentTypeのCharsetからエンコード名を取得しよう
    var text = encoding.GetString(encoding.GetBytes(m.TextBody.TextData));

    // Do someting...
}

Searchメソッドに使えるコマンドは、ALL、ANSWERE、BCC、BEFORE、BODY、CC、DELETED、DRAFT、FLAGGED、SEEN、UNSEENなどのほかにもかなりあるのでImapX.dllと一緒に配布されるimap search commands.txtを参照して欲しい。

POP3で良いのならOpenPop.NETもかなり使いやすい。サンプルコード集も分かりやすい。

2010年8月21日土曜日

ExcelのAutomationのTipsあれこれ

業務でExcelで来たデータをSQL Serevrに流し込む必要があり、ExcelのAutomation用のツールを作ったのでそのときのTipsをいくつか紹介。

Excel Automationの概要自体は下記のリンクを参考にして欲しい。
How to automate Microsoft Excel from Microsoft Visual C#.NET


必ずCOMインスタンスを開放する
Excel Automationでもっともつまづきやすいのが、アプリが終了してもいつまでもExcelのプロセスが居座ることだ。これは色々と理由があるが下記のコードのような状況が最も陥りやすい。

var application = new Excel.Application();
var workBook = application.Workbooks.Open("path", Type.Missing.... );
var sheet = workBook.Sheets[2];

Marshal.FinalReleaseComObject(sheet);
workBook.Close();
Marshal.FinalReleaseComObject(workBook);
application.Quit();
Marshal.FinalReleaseComObject(application);

問題はOpenを呼んでいるところだ。実はapplication.Workbooks.Openを呼び出した際に、内部的にapplication.WorkbooksのCOMインスタンスが生成されている。そのためCOMインスタンスを開放するためにWorkBooksもMarshal.FinalReleaseComObjectしなければならないのだが、ここではWokrBooksをハンドルしていないためにそれができない。正しくは下記のようになる。

var application = new Excel.Application();
var workBooks = application.Workbooks;
var workBook = workBooks.Open("path", Type.Missing.... );
var sheet = workBook.Sheets[2];

Marshal.FinalReleaseComObject(sheet);
workBook.Close();
Marshal.FinalReleaseComObject(workBook);
Marshal.FinalReleaseComObject(workBooks);
application.Quit();
Marshal.FinalReleaseComObject(application);


これでExcelのプロセスが居座ることは解決できたが、例外処理が起きたときや、Cellから値を取得するためにCOMインスタンスを生成したときなど、開放処理を常に行うように気を使うのはなかなか骨が折れる。そのため、開放処理を簡便に行えるようなWrapperを作ることにした。

ちなみにこのアイディアは下記のStackoverflowのGary McGillから拝借している。
c# and excel automation - ending the running instance


Wrapperクラス
class ComWrapper<T> : IDisposable
{
    public ComWrapper(T t)
    {
        ComObject = t;
    }
    public void Dispose()
    {
        Marshal.FinalReleaseComObject(ComObject);
    }
    public T ComObject { get; set; }
}

使っているところ
using (var application = new ComWrapper(new Application()))
{
 try
 {
    using (var workbooks = new ComWrapper(application.ComObject.Workbooks))
    {
        using (var workbook = new ComWrapper(workbooks.ComObject.Open(filePath, ...)))
        {
            try
            {
                using (var worksheet = new ComWrapper(workbook.ComObject.Sheets[1]))
                {
                    // なにか処理
                }
            }
            finally
            {
                workbook.ComObject.Close();
            }
        }
    }
 }
 finally
 {
    application.ComObject.Quit();
 }
}

おまけ - Cellの値を取得
for( var rowIndex = 1; rowIndex < 10; rowIndex ++)
{
 var colIndex = 0;
 using (var cell1 = new ComWrapper(worksheet.ComObject.Cells[rowIndex, ++colIndex]))
 using (var cell2 = new ComWrapper(worksheet.ComObject.Cells[rowIndex, ++colIndex]))
 {
 // 処理
 var value = cell1.ComObject.Value
 }
}


usingを使うことによりスコープ範囲外到達時に常にDisposeが呼ばれ、そのDispose内部でCOMインスタンスの開放を行っているため、COMの開放というわずらわしい処理を気にする必要がまったくなくなった。これでロジック部分に集中できるだろう。

2010年5月19日水曜日

Bing API 2.0で独自のサーチアプリを作る

複数のキーワードを色々と組み合わせた検索結果が欲しい場合にいちいちその組み合わせごとに入力するのが面倒なのでBing API 2.0を使うアプリを作った。


初めはGoogle Search APIを使おうと思ったのだが、いつの間にだか仕様変更がされていて、Googleから提供されるライブラリを使いクライアントスクリプト上からのみしかアクセスできないようになっていた。それなので、クエリに対してほぼ制限のないBing APIを使うことにした。

Bing APIはかなり使いやすくBing Developer Centerで下図のフォームに必要事項を入力しAppIDを取得しさえすれば、後はBing APIの形式にのっとってHttp GETメソッドで取得すればよい。ちなみにAppIDの取得にはWindwows Live IDが必要なので、ない場合はWindows Live IDを取得してから作業をすすめよう。

(Company Nameなどは適当に入力したが特に問題はなかった)

Bing APIの実際を知るにはSDKをダウンロードしてコードを見るのが一番手っ取り早いが、いくつか解説しておこうと思う。
(Developer CenterでAPI Documentationなどが参照できるので詳細はそちらへ)

基本のクエリは下記のようになる(Bing APIではXMLかJsonで結果を受け取れる。Jsonで取得したい場合はxml.aspxではなくjson.aspxに対してリクエストすることになる)。

http://api.search.live.net/xml.aspx?Appid=&query=漫画&sources=web

一番後ろのパラメータにsourcesを指定しているがBing APIではSourceTypeを指定する必要がある。SourceTypeにはWeb, Image, News, InstantAnswer, Spell, Phonebook, RelatedSearch, Adとあるが下記のように複数組み合わせることも可能だ。

http://api.search.live.net/xml.aspx?Appid=&query=漫画&sources=web+image

いくつかの例を下記に示す。

複数のキーワードを組み合わせる場合
http://api.search.live.net/xml.aspx?Appid=&query=漫画 おすすめ&sources=web
http://api.search.live.net/xml.aspx?Appid=&query=漫画 おすすめ -Amazon&sources=web

検索結果を30件取得する場合
http://api.search.live.net/xml.aspx?Appid=&query=漫画&sources=web&web.count=30

30件の検索結果を、51件目から取得する場合
http://api.search.live.net/xml.aspx?Appid=&query=漫画&sources=web&web.count=30&web.offset=51

ちなみに、web.countは最大で50になる。そして検索結果は最初の1000件まで取得可能だが、web.offsetはweb.count + web.offset <= 1,000となる必要がある。 WebソースタイプのXMLフォーマットのレスポンスサンプルはここで参照できる。レスポンスとして取得したXMLやJsonの解析方法などはSDKで詳細にコーディングされているのでそちらを参照して欲しい。

最後に、Bing APIを使う上で次のようにいくつかの制約がある:検索結果をすべて表示すること、IPアドレスごとに秒間7つまでのリクエストしかできない、SEOのために使ってはならない、Bingを使っていると表示すること、などなど他にもいくつかあるのでAPI Basicsの一番最後の部分をしっかりと読んでから使って欲しい。