ScalaでWebAPIをたたいてXMLを処理するための定型パターンのまとめ
俺はLTなどの資料に使うためのネタ画像をTumblrで収集してるのですが、いざスライドに画像を貼ろうと思ったらTumblrから検索して落としてこなきゃいけなくてめんどくさいです。
ってことで、Tumblrから画像を落とすプログラムをscalaで書いてみました。
せっかくなので、この手のフィードのようなXML的なものをげとしてほげるための定型パターンをまとめたいと思います。
まぁ処理にもよりますが、だいたい50〜60行くらいでかけるので、scalaの練習にもちょうどいいと思います。
今まで書いたのは、Gistにおいてあります。
ゆろよろのGist
もっといい書き方があるよ!って場合は、おしえてくだしあ><
URLとか引数とか
引数にIDをもらって、String#formatを使ってURLを生成します。
"http://hogehoge/%s".format( args.first )
XMLの取得
scala.io.Sourceオブジェクトを使います。scala2.7系のSourceには、内部で保持してるInputStreamをcloseできないバグがありますがちょっとした処理なら気にせず使ってしまえ!
Source#fromFile( filename )でファイルからも読み込めますよ!
scala.io.Sourceオブジェクトとscala.io.Sourceクラス
もし接続できなかったら例外が飛ぶんで好きに処理して。簡単なプログラムだったら面倒なのではしょります(←ホントはいくない)
SourceからXMLを文字列で取ってきて、XMLオブジェクトを作ります。
XMLオブジェクトを作っておくと、XPath的な検索ができたりして便利です。
// URLから文字列で読み込む val source = Source.fromURL( url ) // 文字列からXMLオブジェクトを作る val xml = XML.loadString( source.getLines.mkString )
前に書いた記事も参考にしてください。
scala.io.Sourceとscala.xml.parsing.XhtmlParser - ゆろよろ日記
Basic認証が必要な場合
TwitterのAPIなどで、Basic認証が必要な場合は、import java.net.Authenticator#setDefaultでBasic認証のid,passwordを設定したjava.net. PasswordAuthenticationのサブクラスを登録しておくと、Source#fromURLでリクエストするときにBasic認証が有効になります。
こんなコードです。
import java.net.{Authenticator, PasswordAuthentication} Authenticator.setDefault( new Authenticator { override def getPasswordAuthentication = new PasswordAuthentication( userid, passwd.toCharArray) } )
XMLを処理する
XMLオブジェクトを作ったら、処理したいタグを検索するなどして目的のデータをげとします。
XMLオブジェクトでは、 xml \\ "foo"のように、xpath的にタグを検索して、NodeSeqを返してくれる便利な機能があるので、これでたいていのことは処理できます。
TumblrのAPIで取得できるXMLは、以下のようのな構造になっています。
<tumblr version="1.0"> <tumblelog> <posts> <post> <photo-caption/> <photo-url max-width="1280">画像のURL</photo-url> <photo-url max-width="500">画像のURL</photo-url> ・・・ </post> <post> <photo-caption/> <photo-url max-width="1280">画像のURL</photo-url> <photo-url max-width="500">画像のURL</photo-url> ・・・ </post> </posts> </tumblelog> </tumblr>
今回の例では、"post"タグの中の"photo-url"タグの中で、もっとも解像度の高い(max-width属性がもっと大きい)タグの画像URLを取り出したいので、以下のような形で検索しました。
// photosは"post"タグのリスト。件数でパターンマッチ photos size match { // 取れなくなったら終了 case 0 => None // 画像をファイルに書き出して再帰 case _ => // "post"タグのリストをfor式で繰り返し処理する for( photo <- photos ) { // "post"タグの中の"photo-url"のリストを取り出す val ps = photo \ "photo-url" // もっとも解像度の高いURLを取り出す処理 // "photo-url"タグの中で、"max-width"属性がもっとも大きいタグを // 検索して、urlを取得する val imageUrl = ( ps.first /: ps ){ (p1:Node, p2:Node) => { def getSize( node:Node ) = ( node \ "@max-width" text ).toInt if( getSize( p1 ) > getSize( p2 ) ) p1 else p2 }}.text // URLの画像を保存 saveImage( imageUrl ) } // 件数を増加して再帰 crawlingTumblrImages( cnt + 50 ) }
このような、Listの中から、他の要素と比較してもっとも大きい/小さいなどの条件で要素を選択したい場合は、List#foldLeft[B](z : B)(f : (B, A) => B) : Bを使うのが便利です。
foldLeftは、Listの集計にもよく利用されます。
ページング的な
APIでは、最大取得件数が定められているものも多いので、全てのデータを取り出すには、何回かに分けてリクエストを送る必要があります。
よくやるのは、開始件数をパラメータにもらう関数を再帰で呼び出す形です。
TumblrのAPIでは、URLの中のstart=開始件数で開始件数を指定できるので、1件のXMLを取得するdef crawlingTumblrImages( cnt:Int ):Unitを再帰で呼び出して処理してます。
crawlingTumblrImages関数は、引数に開始件数をもらってTumblrAPIのURLを生成し、SourceオブジェクトとXMLオブジェクトでXMLを取得して、"post"タグおよび"photo-url"タグを抽出して画像をローカルファイルに書き出してます。
"post"タグを検索した結果が0件ならば、それ以上のデータが無いので、再帰を終了しています。この判定は、"post"タグのリスト件数に対してパターンマッチを行っています。
def crawlingTumblrImages( cnt:Int ):Unit = { // 引数の件数からURLを作る val url = tumblrUrl + "?type=photo&start=%d&num=50".format( cnt ) // APIをたたいてXMLを取得する val source = Source.fromURL( url ) val xml = XML.loadString( source.getLines.mkString ) // xmlから"post"タグを抽出する val photos = xml \\ "post" // "post"タグの件数でパターンマッチ photos size match { // 件数0件なら、取れなくなったので終了 case 0 => None // 1件以上ある場合は、XMLの中身を処理する case _ => // 何らか処理する // 件数を増加させて、自分自身を再帰で呼び出す crawlingTumblrImages( cnt + 50 ) } }
Tumblrから画像をダウンロー丼するプログラム
これが完成型です。
import java.net.URL import java.awt.image.BufferedImage import javax.imageio.ImageIO import java.io._ import scala.xml._ import scala.io.Source object TumblrCrawler { def main( args:Array[String] ):Unit= { val tumblrUrl = "http://%s.tumblr.com/api/read".format( args.first ) val r = """http\:\/\/.+/([^\/]+)*$""".r val extR = """.+\.(.+)$""".r def crawlingTumblrImages( cnt:Int ):Unit = { val url = tumblrUrl + "?type=photo&start=%d&num=50".format( cnt ) println( url ) val source = Source.fromURL( url ) val xml = XML.loadString( source.getLines.mkString ) val photos = xml \\ "post" photos size match { // 取れなくなったら終了 case 0 => None // 画像をファイルに書き出して再帰 case _ => // もっとも解像度の高いURLを取り出す for( photo <- photos ) { val ps = photo \ "photo-url" val imageUrl = ( ps.first /: ps ){ (p1:Node, p2:Node) => { def getSize( node:Node ) = ( node \ "@max-width" text ).toInt if( getSize( p1 ) > getSize( p2 ) ) p1 else p2 }}.text saveImage( imageUrl ) } crawlingTumblrImages( cnt + 50 ) } } def saveImage( url:String ) = { val r(fname) = url val (ext,file) = fname match { case extR(e) => (e, new File( fname ) ) case _ => ( "png", new File( fname + ".png" ) ) } val img = ImageIO.read( new URL( url ) ) ImageIO.write( img , ext , file ) Thread.sleep( 1000 ) println( "Download:" + fname ) } crawlingTumblrImages( 0 ) } }