Hatena::ブログ(Diary)

cooldaemonの備忘録 RSSフィード

2010-08-11

Scala で XML を Parse する(添削希望)

Scala で XML を Parse してみたのですが、今ひとつカッコ悪いコードになってしまいました。

凄腕の Scala Hacker に添削して欲しい所です… orz

import scala.io.Source
import scala.xml.{Node, NodeSeq}
import scala.xml.parsing.XhtmlParser

object Test {
  def main(args:Array[String]) = {
    val source = Source.fromFile("/opt/local/share/scala-2.8/doc/scala-devel-docs/api/index.html")
    val xhtml = XhtmlParser(source)
    source.close

    val titles = List(
      "div" -> compareAttr("id", "tpl"),
      "ol"  -> compareAttr("class", "packages"),
      "li"  -> compareAttr("class", "pack"),
      "ol"  -> (compareAttr("class", "templates") orElse compareAttr("class", "packages"))
    ).foldLeft (
      xhtml \\ "div" filter(compareAttr("id", "browser"))
    ) {
      (nodes, selector) => (nodes \ selector._1).filter(selector._2)
    } \ "li" map(node => (node \ "@title").text)

    titles foreach {println}
  }

  class compareAttr(attrPrefix: String, val text: String) extends PartialFunction[Node, Boolean] {
    val attr = "@" + attrPrefix
    override def apply(node: Node):Boolean = if ((node \ attr).text == text) true else false
    override def isDefinedAt(node: Node):Boolean = apply(node)
  }

  object compareAttr {
    def apply(attr: String, test: String): compareAttr = new compareAttr(attr, test)
  }
}

Pair の List を foldLeft でまわす所は、素直に下記のように書いた方が良いきもするのですが、括弧の数が…。

    val titles = (((((xhtml
     \\ "div" filter(compareAttr("id", "browser"))
    ) \ "div" filter(compareAttr("id", "tpl"))
    ) \ "ol"  filter(compareAttr("class", "packages"))
    ) \ "li"  filter(compareAttr("class", "pack"))
    ) \ "ol"  filter(compareAttr("class", "templates") orElse compareAttr("class", "packages"))
    ) \ "li"  map(node => (node \ "@title").text)

うう… XPath 使いたい。

PartialFunction を case を使って定義しなおしてみた。

import scala.io.Source
import scala.xml.{Node, NodeSeq}
import scala.xml.parsing.XhtmlParser

object Test {
  def main(args:Array[String]) = {
    val source = Source.fromFile("/opt/local/share/scala-2.8/doc/scala-devel-docs/api/index.html")
    val xhtml = XhtmlParser(source)
    source.close

    val titles = List(
      "div" -> compareAttr("id", "browser"),
      "div" -> compareAttr("id", "tpl"),
      "ol"  -> compareAttr("class", "packages"),
      "li"  -> compareAttr("class", "pack"),
      "ol"  -> (compareAttr("class", "templates") orElse compareAttr("class", "packages"))
    ).foldLeft (
      xhtml \ "body"
    ) {
      (nodes, selector) => (nodes \ selector._1).filter(selector._2 orElse ignorePattern)
    } \ "li" map(node => (node \ "@title").text)

    titles foreach {println}
  }

  def compareAttr(attrPrefix: String, text: String) = {
    val attr = "@" + attrPrefix
    var pf: PartialFunction[Node, Boolean] = {
      case node if (node \ attr).text == text => true
    }
    pf
  }

  def ignorePattern: PartialFunction[Node, Boolean] = {case _ => false}
}

2009-09-12

SimpleHttpClient に HTML のフィルタを追加しました(HTML Document に対して XPath が使えます)

SimpleHttpClientKissXML+HTML を組み込んで HTML フィルタを作成しました。

下記のように、SimpleHttpClient のオブジェクトを作成し・・・

SimpleHttpClient *client = [[SimpleHttpClient alloc] initWithDelegate:self];

HTML 用のフィルタを設定し・・・

[client
    setFilter:SimpleHttpClientFilterHTML
      forHost:@"d.hatena.ne.jp"
];

リクエストを送ると・・・

[client
           get:@"http://d.hatena.ne.jp/cooldaemon/20090911/1252637257"
    parameters:nil
       context:nil
];

DDXMLDocument のオブジェクトが受け取れます。

- (void)simpleHttpClientOperationDidFinishLoading:(SimpleHttpClientOperation *)operation
                                     filteredData:(id)data
{
    [_html release];
    _html = (DDXMLDocument *)[data retain];
}

XPath を使うには、下記のようにします。

NSError *error = nil;
NSArray *body = [_html
    nodesForXPath:@"id(\"days\")//div[@class=\"body\"]//h3/following-sibling::*|id(\"days\")//div[@class=\"body\" and not(.//h3)]"
            error:&error
];

NSLog("%@", [body componentsJoinedByString:@""]);

詳しくは、test/TestHatenaDiaryHTML.m をご参照下さい。

2009-09-11

KissXML で HTML を扱えるように、KissXML+HTML を作りました

この話ですが、よくよく考えたら Objective-C にはカテゴリがあるので、Patch を作る必要はありませんでした。

Source Code と使い方は、こちら

HTML 対応とは全く無関係ですが、こっそり、xmlParseMemory を xmlReadMemory に入れ替えてます。

ついでに、使われていなかった option を利用して htmlReadMemory と xmlReadMemory のオプションを指定できるようにしました。

KissXML がバージョンアップすると利用できなくなる可能性もありますが、きっとすぐに対応できるハズ。

KissXML の開発チームに連絡してみよっと。

2009-09-10

SimpleHttpClient に JSON と XML のフィルタを追加しました

以前作った SimpleHttpClient ですが、ダウロード後のデータを BSJSONAdditionsKissXML に引き渡し、適宜オブジェクトを作成して返すようにしました。

WSSE に対応した際と同じくドメイン毎にフィルタを設定できるので、reader.livedoor.com から取得したデータは JSON フィルタを通す、b.hatena.ne.jp から取得したデータは XML フィルタを通す・・・という使い方が可能です。

具体的には、下記のように設定を行ないます。

SimpleHttpClient *client = [[[SimpleHttpClient alloc] initWithDelegate:self] autorelease];

[client
    setFilter:SimpleHttpClientFilterJSON
      forHost:@"reader.livedoor.com"
];

[client
    setFilter:SimpleHttpClientFilterXML
      forHost:@"b.hatena.ne.jp"
];

もし、http://reader.livedoor.com/api/subs に対してリクエストを送ったのであれば、フィルタ済みのデータを受け取るには下記のようにします。

- (void)simpleHttpClientOperationDidFinishLoading:(SimpleHttpClientOperation *)operation
                                     filteredData:(id)data
{
    _subs = (NSArray *)[data retain];
}

http://reader.livedoor.com/api/subs は、Array 型の JSON を返すので NSArray * にキャストします。

data は autorelease 済みであり、simpleHttpClientOperationDidFinishLoading:filteredData: メッセージは、サブスレッドの中で呼ばれるので、retain しておかないと、サブスレッド終了時に NSAutoreleasePool に release されてしまいます。

SimpleHttpClient 残作業

  • テストコードをリファクタリングする(テストコードに重複が多数あるため)
  • NSOperationQueue を外部から受け取るようにする(SimpleHttpClient 以外でもスレッドを使うため)
  • HTML 用のフィルタを作る(HTML でも XPath 使いたい!)

KissXML で HTML を無理矢理使う方法

KissXML で HTML を扱えるように、KissXML+HTML を作りました

そんなに KissXML に思い入れがあるわけではないのですが、HTML でも XPath を使いたいのと、XPathQuery と KissXML を一つのアプリで併用するのも嫌だったので、少しだけ KissXML の Source を追って HTML を使えるよう修正してみました。

ちなみに、この修正を行なうと XML が扱えなくなります。SimpleHttpClient の HTML フィルタを作成する際には、両方扱えるような修正を入れようかと思っています。その後に patch を作成する予定です。(patch を開発元に送るか検討中)

DDXMLDocument.h の頭で HTMLparser.h を import します。

#import <libxml/HTMLparser.h>

DDXMLDocument.m

- (id)initWithData:(NSData *)data options:(unsigned int)mask error:(NSError **)error
{
    //..snip..
    xmlDocPtr doc = xmlParseMemory([data bytes], [data length]);
    //..snip..
}

xmlParseMemory ではなく、htmlReadMemory を使用するように修正します。

xmlDocPtr doc = htmlReadMemory([data bytes], [data length], "", NULL, HTML_PARSE_NOWARNING | HTML_PARSE_NOERROR);

DDXMLNode.m

+ (BOOL)isXmlDocPtr:(xmlKindPtr)kindPtr
{
    return kindPtr->type == XML_DOCUMENT_NODE;
}

type が XML_HTML_DOCUMENT_NODE の時も真を返すようにします。

    return kindPtr->type == XML_DOCUMENT_NODE
        || kindPtr->type == XML_HTML_DOCUMENT_NODE;

たったこれだけで、KissXML が HTML を扱えるようになります。

ちなみに手元では、wedata にある LDRFullFeed を使って本文を抽出する事ができました。

2009-05-05

libxml2 の SAX インターフェースを試してみた

Source はこちら

iPhone アプリの開発中に XML をパースしたくなったのだけれど、サンプルコードの XML Performance をコンパイルして実行してみると、libxml2 の SAX インターフェースを使った方が、NSXMLParser を使うより三倍以上早かったので、libxml2 を使う事にした。

NSXMLParser も内部で libxml2 を利用しているが、libxml2 を直接使う場合、xmlCreatePushParserCtxt() で作ったパーサーコンテキストに対して xmlParseChunk() を使い、ダウンロード中の XML を少しずつ追加しながらパースを行うという芸当が可能な為、速度が早い。

ただし、XML ではなく HTML をパースする場合、かつ、HTML_PARSE_RECOVER などのオプションを指定したい場合、一度 DOM ツリーを作成する必要がある為、残念ながら SAX インターフェースは使えない(本当かな?識者の突っ込み希望)ので、NSXMLParser を使った方が良いのかな?

NSXMLParser は SAX 専用である為、オプションを指定する場合は、DOM 専用の NSXMLDocument を使う必要がある。オプションの種類は この辺を参照の事。

ちなみに、SAX のコールバック関数を登録する際に下記のようにした。

SAXHander.initialized = XML_SAX2_MAGIC;

これを登録しておかないと、SAX2 用のコールバック関数を呼んでくれない。(これで少しハマった orz)