2011年7月28日木曜日

NSXMLDocument と文字コード指定の謎

 NSXMLDocument の謎な挙動でハマったのでメモ。

 HTML ファイルの内容を NSXMLDocument でパースして情報を取得するという場合を考える。読み込むのは以下のような HTML ファイル。
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>あいうえお</title>
  </head>
  <body>
    <p>あめんぼあかいなあいうえお</p>
  </body>
</html>
 ポイントは HTML5 的な <meta charset="utf-8" /> という文字コード指定。従来の <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> というような指定方法では問題ないのに、上記 HTML5 的な指定をすると NSXMLDocument が文字コードを正しく解釈してくれず、場合によっては取得する文字列値が化ける。

 NSXMLDocument オブジェクトを初期化する方法として三つの異なる初期化メソッドを用いて実験してみる。
#import <Foundation/Foundation.h>

static void output(NSXMLDocument *xmlDoc)
{
  NSLog(@"encoding: %@", [xmlDoc characterEncoding]);
  NSArray *nodes = [xmlDoc nodesForXPath:@"html/body/p" error:NULL];
  NSXMLNode *node = [nodes objectAtIndex:0];
  NSLog(@"result  : %@", [node stringValue]);
}

int main (int argc, const char *argv[])
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSUInteger options = NSXMLDocumentTidyHTML;
  NSURL *url = [NSURL fileURLWithPath:@"/Users/genji/Desktop/test.html"];

  NSXMLDocument *xmlDoc;
  xmlDoc = [[NSXMLDocument alloc] initWithContentsOfURL:url
                                                options:options
                                                  error:NULL];
  output(xmlDoc);
  [xmlDoc release];

  NSData *htmlData = [NSData dataWithContentsOfURL:url];
  xmlDoc = [[NSXMLDocument alloc] initWithData:htmlData
                                       options:options
                                         error:NULL];
  output(xmlDoc);
  [xmlDoc release];

  NSString *htmlString;
  htmlString = [NSString stringWithContentsOfURL:url
                                        encoding:NSUTF8StringEncoding
                                           error:NULL];
  xmlDoc = [[NSXMLDocument alloc] initWithXMLString:htmlString
                                            options:options
                                              error:NULL];
  output(xmlDoc);
  [xmlDoc release];

  [pool drain];
  return 0;
}
 処理内容は NSXMLDocument を用いて HTML ファイル中の p 要素の文字列値を出力するという単純なもの。こいつの実行結果が以下のようになる。
encoding: (null)
result  : ã‚ã‚ã‚“ã¼ã‚ã‹ã„ãªã‚ã„ã†ãˆãŠ
encoding: (null)
result  : ã‚ã‚ã‚“ã¼ã‚ã‹ã„ãªã‚ã„ã†ãˆãŠ
encoding: UTF-8
result  : あめんぼあかいなあいうえお
 はじめのふたつ、初期化メソッドに initWithContentsOfURL:options:error: を用いた場合と、一度 NSData にファイル内容を読み込んでから initWithData:options:error: を用いた場合は characterEncoding に何もセットされておらず、文字列値を出力しても化けてしまっている。

 翻ってファイル内容を NSString に読み込んでから initWithXMLString:options:error: を用いた場合は characterEncoding が正しくセットされており出力も正常に出る。

 NSString でファイル内容を読んだあとに dataUsingEncoding:NSData に変換して初期化した場合は characterEncoding はセットされず文字化ける。

 さらに、上記のコード例では NSXMLDocument の初期化メソッドに渡すオプションは NSXMLDocumentTidyHTML にしているが、これを NSXMLDocumentTidyXML にすると、characterEncoding がセットされないのは変わらないが、出力文字列値が化けないのである。

0 comments:

コメントを投稿