まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2016/09

昨年末、マンホールマップIBM Watson の画像認識機能を組み込んだ、という記事を紹介しました:
マンホールマップに画像認識機能を組み込む

この記事を作成した当時、Visual Recognition(画像認識)の API は V2 というバージョンでしたが、その後のバージョンアップで V3 になりました。ただその時点では V2 もしばらく使えるということだったのでマンホールマップ側は放っておいたのですが、いつの間にか V2 は使えなくなっており、マンホールマップの画像認識機能が動かなくなってしまっていました(要するに僕の手抜きでした、失礼しました)。

改めて V3 に対応させようとして、上記リンク先で紹介したような手順でマンホール画像の学習をさせて・・・と思っていたら信じられないことが!!!

上記リンク先ページ内でも説明しているのですが、もともと IBM Watson の画像認識サービスでは「マンホール」を識別する機能(正確には classifier)がありませんでした。そのため(自分でカスタマイズする classifier に)「マンホール画像を学習」させて、「その上で(自分でカスタマイズした classifier を使って)マンホール識別」を行う、という手順が必要になり、その内容を紹介したのが上記リンク先ページでした。

が、今回 V3 を試してみて気付いたのは「バージョン V3 では始めからマンホールが識別できるようになっている!」ということです。要するに学習などのカスタマイズを行うまでもなく、標準機能としてマンホールが識別対象として登録されていたのでした。ワトソンに何が起こったのか・・・


・・・唯一思い当たる節があるとすれば、私ですw σ(^^; 先程から紹介しているように、V2 の頃にマンホールを学習させ、識別機能をマンホールマップに組み込みました。マンホールマップはそこそこ(苦笑)のアクセス数を誇る位置情報付きマンホール情報サイトであり、マンホール情報ページへのアクセスがある度に画像認識の識別 API が実行され、そしてその多くは「マンホール」として認識されていたはずでした。要するに「マンホール」と認識されるアクセスがワトソン側にもそこそこあったはずなのです。元々の機能ではマンホールを対象としていなかったはずなのですが、カスタマイズされた結果のマンホールがそれだけの人気があったとすると・・・ と都合よく妄想しただけですが、可能性がないとは言い切れないではないですかっ!


というわけで、現在のマンホールマップでは V3 に対応した識別機能が復活しています。そしてこのマンホール識別はワトソンの標準識別対象になっているものをそのまま使っている、ということを付け加えておきます。
2016090601


マンホールマップ(PC版)
マンホールマップ(スマホ版)


メール送信(送信だけじゃないけど)サービスの最大手と思われる SendGrid を使う機会がありました。SDK や API を使って、メールを送信できるサービスです。
2016090601


メール送信そのものは(sendmail などを使えば)単純に実現できますが、一度に大量のメールを送る場合や迷惑メール対策など、本格的に使おうとすると色々な面倒が待っています。そういう面倒な所を一手に引き受けて、心配なくメール送信が実現できる、というサービスです。

この SendGrid によるメール送信ですが、ダッシュボード画面から目的の相手にインタラクティブに送信するだけではなく、プログラミングのためのインターフェースが公開されているので、自分の作るアプリケーションから利用することもできます。ただ実際に送信するサンプルを Java でググると、多くの場合 SDK を使ったものが見つかります。自分は SDK ではなく Web API(REST API) での実現を考えていたので挑戦してみました。ウェブ上にあまり資料がなかったこともあって、その手順を以下に紹介します。


まず最初に大事なこと。SendGrid Web API の最新バージョンは V3 ですが、V2 を使います。SendGrid の推奨でもあります:
https://support.sendgrid.kke.co.jp/hc/ja/articles/206231901-Web API v2とv3どちらを利用すべきでしょうか?-


というわけで、Web API V2 の Mail API を使って実装することにします。リファレンスはこちらです:
https://sendgrid.kke.co.jp/docs/API_Reference/Web_API/mail.html


これを読むと、「メールを送信して結果を JSON で受け取る」場合は以下の様な REST API を実行することになります(これは curl コマンドで実行する場合の例):
$ curl -X POST https://api.sendgrid.com/api/mail.send.json -H 'Authorization:Bearer SG.*****' -d 'to=user1@recipient.com&subject=abcd&from=info@from.com&html=<b>ハロー</b>、ワールド'

上記コマンドは info@from.com ユーザーから user1@recipient.com ユーザーへ、メールサブジェクトは "abcd" 、本文は HTML で "<b>ハロー</b>、ワールド"(HTML として送信しているので、ハローだけが太字になります)を送信する場合のコマンドになります。 user1@recipient.com は送信先なので実在している必要がありますが、送信アドレスである info@from.com は実在している必要はありません。またヘッダの認証情報として使っている SG. で始まる文字列は SendGrid から取得した api key です。SendGrid のアカウントをお持ちで、まだ api key を取得していない場合、取得方法についてはこちらを参照してください。

上記 curl コマンドは(入力ミスなどがなければ)正しく実行されて、メールも送信されるはずです。つまり正しいコマンドです。これを REST API と見なして、同じ処理を Java で実装しなおせばいいわけですが、これが意外と手間取りました。

まず最初に、普段使っている Jakarta Commons HTTP Client 3.1 (メンテナンスモード)を使って、こんなコードを書いてみました(あらかじめ書いておきますが、以下のコードでは期待通りに動きません):
  :
public int sendMail( String to, String subject, String html ){
  int r = 0;

  try{
    String data = "to=" + to + "&from=info@from.com&subject=" + subject + "&html=" + html;
    PostMethod method = new PostMethod( "https://api.sendgrid.com/api/mail.send.json" );
    method.setRequestHeader( "Authorization", "Bearer SG.*****" );
    method.setRequestBody( data );
    HttpClient client = new HttpClient();
    int sc = client.executeMethod( method );
    String json = method.getResponseBodyAsString();
      :
r = 1; }catch( Exception e ){ mothod.setRequest e.printStackTrace();
r = -1; } return r; } :

オプションで指定している内容は curl コマンドのものと同じです(例えば Content-Type ヘッダを指定していませんが、curl でも指定せずに動いていたので)。ただこの sendMail 関数を to = user1@recipient.com, subject =  abcd, html = <b>ハロー</b>、ワールド というパラメータで実行した場合、ステータスコード(上記コード内の sc 変数)の値は 400 となり、また実行結果である json 変数の値は以下のようなものになりました:
{"errors":["Empty from email address (required)"],"message":"error"}

「必須項目である from 値が空である」というエラーメッセージのように見えます。しかしその値は上記のようにハードコーディングで入力しているつもりでした。

これまでも HTTP Client を Java で実装する場合はこの HTTPClient 3.x を使うことが多かったし、今回のようなポスト時のエラーに遭遇したことはなかったのですが・・・ まずここで躓きました。


次に試したのはポスト方法の変更です。sendMail 関数を以下の様な形に変え、ポストデータをプレーンテキストで送信するのではなく NameValuePair 配列で送信するように変更してみました(これもまだ期待通りには動きません):
  :
public int sendMail( String to, String subject, String html ){
  int r = 0;

  try{
    PostMethod method = new PostMethod( "https://api.sendgrid.com/api/mail.send.json" );
    method.setRequestHeader( "Authorization", "Bearer SG.*****" );
    List<namevaluepair> params = new ArrayList<namevaluepair>();
    params.add( new NameValuePair( "to", to ) );
    params.add( new NameValuePair( "from", "info@from.com" ) );
    params.add( new NameValuePair( "subject", subject ) );
    params.add( new NameValuePair( "html", html ) );
    method.setRequestBody( ( NameValuePair[] )params.toArray( new NameValuePair[0] ) );
    HttpClient client = new HttpClient();
    int sc = client.executeMethod( method );
    String json = method.getResponseBodyAsString();
      :
    r = 1;
  }catch( Exception e ){
    mothod.setRequest
    e.printStackTrace();
    r = -1;
  }

  return r;
}
  :

自分としては先程のコードと同じことを記載しているつもりでした。が、このコードは一応動いて、ステータスコードは 200 (成功)を返してくれます。

しかし、実際に送られてくるメールを受け取ると、残念ながら日本語部分が全て ? という文字に化けてしまっていました。。
2016090601
 (↑ ハロー、ワールド という結果を期待していたが文字化け)


文字化けということは文字コードの指定と実際の文字コードが違っていると読み、であれば強制的に UTF-8 で指定すれば・・・ と考えたのですが、この HTTPClient 3.1 には Content-Type で指定する以外の文字コード指定方法はありません( NameValuePair 配列で指定した本文がどのような内部処理をされているのかはわかりません。。) というわけで、この方法も詰み・・・

結論として、HTTPClient のバージョンをより新しいものに上げて対処しました。現在の最新バージョンは 4.5.2 のようです(僕は 4.5.1 を使いました):
https://hc.apache.org/httpcomponents-client-ga/

このライブラリに置き換えた上で、コードも以下のように 4.5.x 仕様に書き換えました(完成版です):
  :
public int sendMail( String to, String subject, String html ){
  int r = 0;

  try{
    PostMethod method = new PostMethod( "https://api.sendgrid.com/api/mail.send.json" );
    method.setRequestHeader( "Authorization", "Bearer SG.*****" );
    List<namevaluepair> params = new ArrayList<namevaluepair>();
    params.add( new BasicNameValuePair( "to", to ) );
    params.add( new BasicNameValuePair( "from", "info@from.com" ) );
    params.add( new BasicNameValuePair( "subject", subject ) );
    params.add( new BasicNameValuePair( "html", html ) );
    method.setEntity( new UrlEncodedFormEntity( params, "UTF-8" ) );
    CloseableHttpClient client = HttpClients.createDefault();
    CloseableHttpResponse response = client.execute( method );
    int sc = response.getStatusLine().getStatusCode();
    HttpEntity entity = response.getEntity();
    String json = EntityUtils.toString( entity, StandardCharsets.UTF_8 );
      :
    r = 1;
  }catch( Exception e ){
    mothod.setRequest
    e.printStackTrace();
    r = -1;
  }

  return r;
}
  :

この関数は期待通りに動き、実行後にメールを受け取ると、期待通りの HTML コンテンツが文字化けなしに表示されました:
2016090602


と、偉そうに書きましたが、実の所、なぜ前の2つのコードで動かないのかわかりません。最新コードでは NameValuePair 配列を UTF-8 指定で追加していて、これは旧バージョンにはない関数なので、その差で文字化けの有無になっているのかもしれません。ただ繰り返しますが、curl では最小限の指定だけで動いていたことと同じ指定をしているのに、Java でうまく動かない理由が説明できないのでした(少なくともエラーメッセージの内容をそのまま信用して対処しようとするとハマりそうな予感・・・)。

まあとりあえずは結果オーライ、ということで。あと SendGrid のメール送信を Java から、それも SDK ではなく Web API 経由で送る例はあまり見かけることがなかったので、後からやる人の助けになれば嬉しいです。


このページのトップヘ