読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

ScalaでWebAPIをたたいてXMLを処理するための定型パターンのまとめ

scala

俺は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認証が必要な場合

TwitterAPIなどで、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を返してくれる便利な機能があるので、これでたいていのことは処理できます。


scala.xml.XML


TumblrAPIで取得できる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の集計にもよく利用されます。


Page not found | The Scala Programming Language

ページング的な

APIでは、最大取得件数が定められているものも多いので、全てのデータを取り出すには、何回かに分けてリクエストを送る必要があります。


よくやるのは、開始件数をパラメータにもらう関数を再帰で呼び出す形です。


TumblrAPIでは、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 )
  }
}