2012年2月28日火曜日

認可コードのスクレイピング(WindowsPhone)

クライアントアプリでGoogleのOAuthを行うと

という風に認可コードが取得できます。 この文字列をアプリ側に送信するのですが、 スマートフォンでこの認可コードを入力するのは至難の業です。いや苦行です。 そこでコピペとなるところですが、 Googleの認可コードのページでWindowsPhoneのコピペが何故か機能しません。 私自身開発中に頑張って入力するという苦行を行なっていました。 ※リフレッシュトークンの実装後にその苦行を忘れる エミュレータはまだキーボード入力が可能なのでいいのですが 実機になるとそれもなくなり本物の苦行を強いる事になります。 しかもコードをミスって入力した場合に開発者(私)は発狂して WindowsPhoneへの恨みが重なっていきます。 んで冷静に考えるとHTMLスクレイピングして認可コードを取得すればOKだと至りました。 まぁ他のTwitterクライアント見て実現していたので「あー可能なんだ」って思ったのが この苦行への終止符だったわけです。 ※Twitterは8桁の数値なので別に苦行レベルでもない気がしますけど。 まぁそんな苦行を強いるアプリが生まれないで済むように一応書いておきます。
        private void webBrowser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
        {
            string uri = webBrowser.Source.AbsoluteUri;

            if (uri.IndexOf("/o/oauth2/approval") != -1)
            {
                string html = webBrowser.SaveToString();

                int tagStart = html.IndexOf("<textarea");
                int tagStartEnd = html.IndexOf(">", tagStart);

                int tagEnd = html.LastIndexOf("</textarea>");

                string pin = html.Substring(tagStartEnd + 1, tagEnd - tagStartEnd-1);
                textBox.Text =pin;
            }

        }
WebBrowserに対してLoadCompletedイベントを作成します。 WebBrowser.SourceのUriからAbsoluteUri(LocalPath、AbsolutePathでも可能)から 認可した場合の画面を判定します。 WebBrowserのSaveToString()から内部のHTMLを取得して、 あとは認可コードを書いてあるtextareaタグを解析です。 まぁ試しのコードではそのままテキストボックスに設定してますけど、 そのままアクセストークンを取得するコードに流してもOKなんでしょうけど少し悩ましいです。 気持ちの問題なのですがOAuthのような認可手順に対して、 ユーザが要求したWebページを表示せず(実際は表示する寸前)に次の画面に遷移するっていうのが 果たして正解なのかどうかっていう事です。 まぁWebアプリの場合このままコールバックになるわけだからいいんでしょうね。 エンドユーザにはOAuthという認可方法はそんなに関係ないですし。

2012年2月23日木曜日

GoogleAnalytics(v3)でアクセスする

先日ブログにも書きましたがV2でのアクセスであり、ClientLoginだったので
今度はv3でのアクセスをやってみます。今回もJavaです。
Googleの日本語の資料はいつも通り古くほぼv2を使っているので
しばらくは英語の文献の方が良いと思います。


準備
まずここからクライアントライブラリをダウンロードしてきます。 で ・google-api-client-1.6.0-beta.jar ・google-http-client-1.6.0-beta.jar ・google-oauth-client-1.6.0-beta.jar ・dependencies/gson-1.7.1.jar ・dependencies/guava-r09.jar ・dependencies/jackson-core-asl-1.9.1.jar を読み込みます。 Analytics用のライブラリはここにあります。
OAuthでのアクセスする準備
Google+のアクセス時に説明しましたが、API使用したり、クライアントID取得の為に GoogleAPIConsoleでAnalyticsをONしておく必要があります。 「Services」をクリックしてAnalyticsをON
「API Access」をクリックして、クライアントIDを発行します。 私自身はWebアプリで使いたいですけど、一旦アクセスする為に「installed applications」を選びました。 違いはWebアプリケーションの場合と違い、コールバックURLが固定値になり、 手作業で認可コードを設定するっていう手順が増えるだけです。
認可を行う
さて準備ができたので、アクセスしてみます。
  String authorizationUrl = new GoogleAuthorizationRequestUrl(CLIENT_ID, REDIRECT_URL, "https://www.googleapis.com/auth/analytics.readonly").build();
  System.out.println(authorizationUrl);
  BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  String authorizationCode = null;
  try {
    authorizationCode = in.readLine();
  } catch (IOException ioe) {
   throw new RuntimeException("Error",ioe);
  }
GoogleAuthorizationRequestUrlに取得したクライアントIDとリダイレクトURLを設定します。 三番目の引数はスコープ(何にアクセスできるか)です。 認証用のURLを作成してコンソールに出力しreadLine()で認可コードを待ち受けます。 ブラウザを立ち上げて、コンソールに出力されたURLにアクセスすると Googleの認証画面(マルチアカウントの場合、ユーザ選択画面等もあります)が表示され 認可画面が現れて認可を行うと、認可コードを出力してくれるのでそれをコンソールに入力します。
アクセストークンの取得
コンソールに入力された認可コードを元にアクセストークンをとりにいきます。 APIコンソールで取得したIDとSecret、それと認可コードでAccessTokenResponseを生成
  NetHttpTransport httpTransport = new NetHttpTransport();
  JacksonFactory jacksonFactory = new JacksonFactory();
  AccessTokenResponse response = null;
  try {
    response = new GoogleAccessTokenRequest.GoogleAuthorizationCodeGrant(
        httpTransport, jacksonFactory, CLIENT_ID, CLIENT_SECRET, authorizationCode,
            REDIRECT_URL).execute();
  } catch (IOException ioe) {
   throw new RuntimeException("Error",ioe);
  }

認可コードが間違ってなかったらこれでOKのはず。 responseにアクセストークン、リフレッシュトークン等が取得されます。
Analyticsのオブジェクトを生成
取得したAccessTokenResponseを元に
GoogleAccessProtectedResource googleAccessProtectedResource = 
  new GoogleAccessProtectedResource(response.accessToken, httpTransport, jacksonFactory,CLIENT_ID, CLIENT_SECRET, response.refreshToken);
Analytics analytics = Analytics.builder(httpTransport, jacksonFactory)
         .setHttpRequestInitializer(googleAccessProtectedResource)
         .setApplicationName("test")
         .build();
というような感じでAnalyticsオブジェクトを生成します。
データを取得してみる
さてやっとアクセスですね。
  Get apiQuery;
  try {
   apiQuery = analytics
     .data()
     .ga()
     .get("ga:" + "13012476", "2011-09-01", "2011-09-30",
       "ga:visits,ga:pageviews");
  } catch (IOException e) {
   throw new RuntimeException("Error", e);
  }

  apiQuery.setSort("-ga:visits");
  apiQuery.setMaxResults(50);

  try {
   GaData data = apiQuery.execute();
   Map totalsMap = data.getTotalsForAllResults();
   for (Map.Entry entry : totalsMap.entrySet()) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
   }
  } catch (IOException e) {
   throw new RuntimeException("Error", e);
  }
取得してきたanalyticsオブジェクトのdata().ga().get()をおこなってGetオブジェクトを生成します。 第1引数はProfileIdになります。v2で言っていたTableIdですね。id自体は数値などで入っている為、 「ga:」をつけてやる必要があります。 これをexecute()するとデータが取れてきます。 この取得方法だと出力は「ga:visits,ga:pageview」が取得できます。 取得できるDimensionやMetricsなどはここにあります。
ProfileのIdについて
前述した「ga:13012476」についてですが v2の時はEntries()などで比較的容易に取得できましたけど。。。
   Management management = analytics.management();

   Accounts accounts = management.accounts().list().execute();
   List<Account> accountList = accounts.getItems();
   Account account = accountList.get(0);
   String accountId = account.getId();

   Webproperties prop = management.webproperties().list(accountId)
     .execute();
   Webproperty webproperty = prop.getItems().get(0);
   String webId = webproperty.getId();

   Profiles profiles = management.profiles().list(accountId, webId)
     .execute();
   Profile profile = profiles.getItems().get(0);
   String profileId = profile.getId();

   System.out.println("accountId:" + accountId);
   System.out.println("webId:" + webId);
   System.out.println("profileId:" + profileId);
わかりやすくすべてのデータについて、itemsの1件目を取得してます。 Accountはマルチアカウント用なのかな、、、私の環境では1件でした。 WebPropertyは解析しているサイト数が出てきました。 その中からプロファイルを取得する感じですね。 少しAnalytics自体のデータ構造がわかってないのでこれで正しいのか不明です。 execute()時にAPIを発行しているはずなので、、、うーんって感じ。
感想
なんかライブラリが多すぎて少し引きました><。 実際実装する際にはライブラリではなく、RESTfulアクセスすると思います。 なぜかというと、個人(セッション)に対して何を残せば良いかがわかりにくく アクセスなどが隠蔽されているので何か使いにくいというか、、、って感じです。 ※いやこのコード位の事やるにはいいんですけどね。 まぁJSONからデータのオブジェクト作ったりするのが大変だったりもするんですけど。

GoogleAnalyticsにアクセスしてみる


そのまま勢いでv3のOAuthを作りました。

Google AnalyticsはWeb解析ソリューションです。
何も考えずにサイトにJavaSriptコードを埋め込む事でWebの訪問者などを調べる事をできます。
素晴らしいサービスであれこれ機能がありますが、
今回はAPIにJavaでアクセスしてみたいと思いました。

準備
・gdata-core-1.0.jar ・gdata-analytics-2.1.jar ・google-collections-1.0.jar を読み込みます。 見つけて行く過程で「google-api-service-analytics-v3-1.3.3-beta.jar」を見つけました。 これでアクセスするのが良かったかもなのですが、 ちょっと使い方がわからなかったので後日試します。 サンプルを元におこなってみます。
まずは認証
gdataに関してOAuthやAuthSubなどの認証方式がありますが、ClientLoginで試しました。
AnalyticsService service = new AnalyticsService("applicationName");
service.setUserCredentials(username, password);
ClientLoginはOAuthなどを触った事がある人ならわかると思いますが あまり好ましいと思える認証方式ではありません。 実際文献にも「てめーで使うようなアプリにしか使うなよ」って書いてあります。 OAuthなどを行いたかったのですが、今回はアクセスしてみる事を目標にしてますので 一旦この方式でアクセスしてみます。
サイトの一覧を取得
AnalytisServiceからアカウントの情報を取得してきます。
URL feedUrl = new URL(""https://www.google.com/analytics/feeds/accounts/default""); 
AccountFeed service.getFeed(feedUrl, AccountFeed.class);
for (AccountEntry entry : accountFeed.getEntries()) {
}
AccountFeedのgetEntries()によってそのアカウントが所有しているデータを取得する事が可能です。
サイトの訪問者を見てみる
AccountEntryには「TableId」というものがあって、それによりどのサイトかを識別できるようになってます。 そのテーブルIDを元にDataQueryを作成します。
    DataQuery query = new DataQuery(new URL(DATA_URL));
    query.setIds(tableId);
    query.setStartDate("2012-01-01");
    query.setEndDate("2012-01-31");
    query.setDimensions("ga:browser");
    query.setMetrics("ga:visits,ga:bounces");
setDimensions()は何を取ってくるか? setMetrics()何を取ってくるか?です。 まぁ日付はそのままですね。 この場合、ブラウザについての訪問者を調べています。 このデータクエリに対して
query.setSort("-ga:visits");
query.setFilters("ga:browser!@Explorer");
DataFeed dataFeed = service.getFeed(query, DataFeed.class);
というようにソートやフィルタをつけてあります。 この場合訪問者数の降順、「Explorer」とついたブラウザの排他ですね。 ・・・Googleのサンプルに悪意を見た。 これで取得したDataFeedに情報が入ってますので
    for (DataEntry entry : dataFeed.getEntries()) {
      System.out.println("\tBrowser: " + entry.stringValueOf("ga:browser"));
      System.out.println("\t\tVisits: " + entry.stringValueOf("ga:visits"));
      System.out.println("\t\tBounces: " + entry.stringValueOf("ga:bounces"));
      System.out.println("\t\tBounce rate: "
          + entry.longValueOf("ga:bounces") / (double) entry.longValueOf("ga:visits"));
    }
みたいな感じで情報にアクセスできます。
感想
まぁサービスに埋め込むにはまだ色々と考えないとなぁと思いました。 ※まぁとにかく認証ですよね。 案外調べていてPHPで文献(ブログ)を出している人が多そうでした。 まぁ現状では埋め込むサイトはPHPの方が多そうですね。 Javaでアクセス解析必要なサービスって少し前なら想像できませんが 今はGoogleAppEngineもあるので、Javaでこういったものが必要なサービスも増えてくると思います。 1.6辺りからGoogleAppEngine対応されているみたいで

2012年2月5日日曜日

ブラウザベースのEPUB読み込み

ふと思いついた
達人出版会などがEPUB形式で配布しているのもあり、 タブレットではEPUBで読む機会も増えてきました。 PDFで読むのがまだ主流と思いますが、未来に行くならEPUBだ! とまぁこう思う所存でございます。※アンマオモッテナイデス 先日EPUBの圧縮方式をJavaでやりましたが、ふと思いついたんです。 ブラウザベースでEPUBを読み込んで、そのまま読めたら最高だと。 Adobeのリーダーインストールすんのか?とかいろいろ葛藤がある中、 ChromeExtensionで読めたら最高じゃない? ※クロスブラウジングしなくていいからじゃないだからね! って事で少し作成してみる事にする。
幸運なことに
先日のブログでも言いましたが、EPUBのファイルはZIP形式です。 先人によりZIP解凍してくれるライブラリがありました。 「JavaScriptでZIP解凍できるのかよ!」と驚きながらありがたやー。 ※これがあったから始めようと思ったりしたり、、、 って事で、それを使ってEPUBを解凍する事にした。 ファイルの指定はinputのfileに指定したらクライアント上で展開っていう形。
   //ファイルを変更された場合
   var fileData = document.getElementById("epubFile").files[0]
   var reader = new FileReader(); 

   //読み込みが完了したら
   reader.onload = function(evt){
          var bytes = [];
          var byteData = evt.target.result;
          //データを変換
          for (var i=0; i<byteData.length; i++)bytes[i] = byteData.charCodeAt(i) & 0xff;
          //epubファイルを解凍
          var epub = Zip.inflate(bytes);
   };
   reader.readAsBinaryString(fileData);
って感じでchangeイベント等で行えば良い。サーバが関係しないから思ったより簡単!速い! ・・・JavaScript速くなったなぁ、、、トオイメ。 Zip.inflate()によってfilesプロパティに内部のファイルにアクセスできるようになり、 files["ファイル名"]でFileクラスがもらえるので そこに生データがあったり、inflate()で解凍してくれたりする。 なので ・mimetypeの内容確認 mimetypeは非圧縮かどうかって確認まではしませんでした。 ・META-INF/container.xmlのOPF位置の確認を行う。 container.xmlに関しては文字列からDomにして確認
function changeDom(data) {
    var xmldom = new DOMParser();
    var dom = xmldom.parseFromString( data, "application/xml" );
 return dom;
}
・目次データの作成 container.xmlのopfファイル位置を取得してそれをDOMに変換して、 manifestなどを読み込んで配列データにしてファイル名を取っておく。 ここで難しいのは 圧縮ファイル名は「OEBPS/xxxx.xhtml」となっているけど、 XHTML上の指定は「xxxx.xhtml」という感じで相対パスになっているはず。 ※OPFファイルからの相対っていう仕様なのかな? OPFファイルを抜き出した時に「OEBPS」などのディレクトリ名を残しておいて XHTML内部のファイルの解析を行うときに使用できるようにしておく。
HTMLの表示、、、と問題。
目次から表示したいファイルやら何やらは取得できるのだけど、表示する際には少し工夫が必要で、 「xxxxx.html」はJavaScript上の「xxxx/xxxx.html」という名称で取得できかつ、その場には存在しない。 要は単純にリンク先は存在しない。 ・リンクをクリックされる。 ・HTMLを取得する ・それをHTML上に展開する って寸法になる。 クリックに対しては
<a href="javascript:void(0)" onclick="open('xxxx.xhtml'); return false;"></a>
見たいにやって関数をかませてやるといい。 それをinnerHTMLやらでそのまま表示。
(∩´∀`)∩ワーイ。ママ。ヒトリデデキタヨ。 ※サンプルは白鯨です。 そしてEPUBを見ていると異変に気づく。 スタイルシートあたってないわ、画像が表示できないわ、、、である。
<img src="images/xxxx.jpg">
Σ(゚д゚lll)ガーン、、、これも相対パスやないかい! Chromeのコンソールを見ると 「GET file:///C:/xxxx/EPubReader/css/stylesheet.css 」 と嘆かわしいログが出ている、、、トリニイカナイデ!
画像の埋め込み
バイナリデータが手元にあって、画像が表示できない苦悩(´・ω・`) サーバは偉大だ、、、と今更ながらに思うこの閉塞感。。。 imgタグのアクセスを擬似的にメモリデータに持ってこようとか 頭の中ではいろいろぐるぐるしたけど、 imgタグのsrcにバイナリを埋め込む方法がある事を知る。 細かいファイルとかをこうやって埋め込んでアクセス数を減らすテクがあるのか。。。 知らなかったけどこれ使える! zipやらutf8のライブラリやらjqueryやらでワラワラjsファイルが増えていってるけど クライアントのみである事を活かす為にはアクセス数など気にしない!base64.jsを追加。
マッテタヨ(∩´∀`)∩クジラタン!
またも問題発生
200k位の画像になってくると「"Maximum call stack size exceeded"」と出るのだ。 。。。。まぁスタック足りないだろうな、、、と1日置く。 原因はzipライブラリの59行目にあった。
var blob = String.fromCharCode.apply(null, this.data);
これに大きめのデータを突っ込むとアウトらしいので
  File.prototype.getBlob = function (dataArray) {
 var blob;
    try {
        blob = String.fromCharCode.apply(null,dataArray);
    } catch (err) {
     //データを分割して変換してつなげる
     var leng = dataArray.length;
     var dataLength = 100000;
     var loopNum = Math.ceil(leng / dataLength);
     var blobData = new Array();
     blobData[loopNum] = new Array();
 
     for ( var idx = 0; idx < loopNum; ++idx ) {
      var dataIdx = idx * dataLength;
      var splitData = dataArray.slice(dataIdx,dataIdx + dataLength);
      blobData[idx] = this.getBlob(splitData);
     }
     blob = blobData.join("");
    }
    return blob;
  };
って関数を追加してやった。 もちろんこれだと100000でスタックエラー出たら永久ループじゃん!こうすれば高速だ。 ってありそうだったけど、まぁ応急処置だとご勘弁を。 一応手元にある画像ではこれでエラーはなくなったけど、 多分何らかの問題はまた出てくるんだろうな。。。
公開までの道のり
・SVGファイル指定してあるとどうもダメみたい。 ・スタイルシートを当てないとダメ ・EPUBファイルがJavaScript依存とかしているとなんかもっと大変な気がしている。 ・YouTube埋め込んで合ったら、、、、大丈夫なのかな? など。まぁ公開までいけるかは微妙だけど、これで文章読む事位はできるようになる。 JavaScriptで何やってくれんねん。って感じですが、いろいろできるもんです。 ChromeStore上にEPUB系がまだないみたいなので 少し頑張ってみようかな?とは思っています。

2012年2月2日木曜日

EPUBファイルの作り方

EPUB作りたいって人がいたので少しJavaで実装してみた。

EPUBとは
EPUBとは電子書籍の形式で最近3.0が仕様確定したのは 知ってたんだけど、内容自体はZIP形式で少し特徴があり 単純にZIPしてはいけないので注意が必要。
EPUBの構成
・mimetype (非圧縮ファイル) ・META-INF/container.xml (構成ファイルを指定) ・EPUB(任意)/****.opf (構成ファイル) という構成。 opfファイルの中身やその後の構成についてはEPUBの仕様説明になるので 一応ここから割愛。
mimetypeについて
ファイルの内容自体は固定で 「application/epub+zip」を書き込むファイルを作ってやる。 特徴なのは「非圧縮で先頭」である必要があるので ZipOutputStreamを開始して最初のエントリーを以下のように非圧縮にしてやれば良い。
        String mime = "application/epub+zip";
        ZipEntry entry = new ZipEntry("mimetype");
        byte[] mimeData;
  try {
   mimeData = mime.getBytes("UTF-8");
  } catch (UnsupportedEncodingException e1) {
   throw new RuntimeException(e1);
  }
        entry.setSize(mimeData.length);
        entry.setCompressedSize(mimeData.length);
        entry.setCrc(0x2CAB616F);
        entry.setMethod(ZipEntry.STORED);

        // 出力先 Entry を設定する。
        try {
   zos.putNextEntry(entry);
         zos.write(mimeData);
         zos.closeEntry();
  } catch (IOException e) {
   e.printStackTrace();
  }

META-INF/container.xmlについて
これは単純に構成ファイルの位置指定なので
    private String OPF_PATH = "EPUB";
    private String OPF_FILE = "book.opf";

 private void createContainerXml() {
     String xml = "<?xml version=\"1.0\"?>";
     xml += "<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">";
     xml += "<rootfiles>";

     String fileName = OPF_PATH + "/" + OPF_FILE;
     xml += "<rootfile full-path=\"" + fileName + "\" media-type=\"application/oebps-package+xml\"/>";
     xml += "</rootfiles>";
     xml += "</container>";

     try {
      ZipEntry entry = new ZipEntry("META-INF/container.xml");
   put(entry,xml.getBytes("UTF-8"));
  } catch (UnsupportedEncodingException e) {
   throw new RuntimeException(e);
  }
 }

みたいな感じで固定の値を設定してあげる。 put()関数は単純にZipOutputStreamにエントリーしている関数です。
opfファイルについて
META-INF/container.xmlファイルに指定したopfファイルに EPUBが構成するようなファイルを指定していくらしい。 EPUBのサンプルとしてGoogle Codeにおいてあったのでこちらで実験した。
ZIPする時の注意点
実際の時とは違うんだけど、Windowsのファイルシステムでやっていると 「¥」でパスを設定してしまい、AdobeのViewerはそれを認識できない(EPUBの仕様?)ので それを変換する必要がある。
     entryName = entryName.replaceAll("\\\\", "/");
おなじみのですね><v それと将来的にはメモリ上でやるのでこの失敗はないと思うんだけど、 ファイルシステムを検索してEntryを作っていると 「/META-INF/container.xml」 とEntryする事もあり、ZIP自体はこれでもOKなんだけど、 「META-INF/container.xml」 としてやらないと読み込んでくれなかった。 ※これもViewerの仕様がEPUBの仕様かは不明。 一応これでサンプルのファイルを利用してZIPで固めたらOKだった。
サービス化を考えている友人へ
固めるところは技術的に見れば簡単なんだけど、 実際は「opfファイル」をサービスとして提供しなければ行けないところで 本を作っている人がこれを書きこむってのはサービス上はありえないわけで、 まぁこんな圧縮程度で止まっている場合ではないって事です。