« Arduino Ethernet Shieldと他SPIデバイスの混在 | トップページ | SPIデバイスの混在とSS信号の初期状態 »

ArduinoでXMLを解析する

GLCDで漢字表示ができるようになりましたが、Arduino RSSリーダーを作るためには、RSSサイトから配信されるXMLデーターの解析を行う必要があります。この解析を行うライブラリを自作するか、フリーライブラリを移植するか迷っていたのですが、Arduinoでも動くライブラリが見つかったので試してみました。

XML構文解析を行うパーサー(Parser)の種類

標準ベースの実装としては以下の2つがあります:

1) DOM(Document Object Model)

W3Cが勧告しているXML文書を操作するためのAPI。XML文書全体をメモリーに読み込み、XML文書のツリー構造をメモリー上に展開した上で各要素へのアクセスを行います。データー全体のツリー構造を保持することからランダムに要素の取り出しを行う用途に向いていますが、反面メモリー消費が多いのが欠点です。Arduino/AVRのようにメモリーリソースが限られた組み込み系では使いづらいと思います。

2) SAX(Simple API for XML)

SAXはXML文書全体を読み込まず、データーを読み込みながら逐次処理を行います。SAXでは、開始タグ・終了タグを見つけるとイベントが発生し、call back関数が呼び出されます。このcall back関数に必要な処理を記述することで特定の要素を抽出するなのど操作が可能となります。DOMのようにランダムアクセス用途には向きませんが、メモリー消費が少ないため、組込用途には向いています。SAXの概要はここを参考にしました

当初、XMLパーサーはDOMしかない(もしくは、.Net FramewarkのXmlTextReaderのような独自の実装)と思っていたため、Arduinoで使うことをためらっていましたが、XML解析について調べていくうちにSAXを発見しました。RSSリーダーとしての用途なら、データーの先頭から逐次データーを読みながら1パスで処理することで十分なため、SAXなら使えそうです。

フリーのSAXライブラリ

例によってフリーのライブラリを物色。NunniMCAXというライブラリを見つけました。Arduinoと同じMade In Italyなのですが、読み方分かりません、、

オリジナルのライブラリは、FILE *fileを引数としてパーサーを呼び出す形式になっています。パーサー本体の中では、fgetc(file)を使ってテキストファイルから文字を読み込みながら逐次処理を行っています。この部分だけを変更して、関数ポインタを渡すようにしました。Ethernet shieldから1文字読み込む関数を作ってこの関数のポインタを渡してやることでwebサーバーから取得したXMLデーターの解析ができます。

Arduino用に変更を盛り込んだライブラリソースは以下です:
「NunniMCAX-1.4.1_arduino.zip」をダウンロード

使い方ですが、IDEのライブラリフォルダー配下に「NunniMCAX」というフォルダーを作って解凍したファイルを格納して下さい。

XML文書を解析する過程でイベントが発生すると以下の関数がcall backされますので、ユーザープログラムの中で実体を記述します。

  1. startDocument: XML文書を見つけた
  2. startElement: 要素ノードを見つけた → 開始タグを見つけた
  3. characters: タグに挟まれたテキスト情報を見つけた → 文字単位にイベントが発生するようです
  4. endElement: 要素の終了→ 終了タグを見つけた
  5. endDocument:

例えば、次のXMLデーターを入力した際の発生イベントを示します。
<?xml version="1.0" encoding="utf-8"?>
<Hello>
   <World>こんにちはみなさん!</World>
</Hello>④⑤

丸付き数字が各イベントが発生するポイントになります。③のchractorsイベントは、「こんにちはみなさん!」の各文字単位に発生します。

サンプルスケッチ1

Yahoo JAPANのお天気情報RSS(http://rss.weather.yahoo.co.jp/rss/days/4610.xml)を読み込んで解析を行うサンプルスケッチを以下に示します。

このスケッチでは、①~⑤のイベント毎にシリアルポートに結果の出力を行います。③はcharactorsイベント発生毎に検出した文字をバッファに蓄積し、④のイベント発生時に文字列として出力しています。Attribute(タグの中に書き込まれた属性)情報を取得するAPIもあるのですが、ここでは割愛します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include <NunniMCAX.h>
#include <Ethernet.h>

#define MAXLEN 256
static char m_characters[MAXLEN];
struct NunniMCAXContentHandler handler;
bool RootDetected = false;

byte mac[] = { xx, xx, xx, xx, xx, xx };
byte ip[] = { 192, 168, 0, 111 };
byte server[] = { 192, 168, 0, 10 };
Client client(server, 80);

void setup()
{
  delay(1000);
  Ethernet.begin(mac, ip);
  Serial.begin(115200);

  /* set up the paser handler functions */
  handler.startDocument = startDocument;
  handler.startElement = startElement;
  handler.characters = characters;
  handler.endElement = endElement;
  handler.endDocument = endDocument;
}

void loop()
{
  Serial.println("##connecting...");
  if (client.connect()) {
    Serial.println("##connected");
    client.write("GET /ansi.xml HTTP/1.0\r\n\r\n");
  } else {
    Serial.println("##connection failed");
    delay(5000);
    return;
  }

  // パーサーの呼び出し 
  NunniMCAXparse( &getcFunc, &handler );

  Serial.println("##disconnecting.");
  Serial.println("");
  client.stop();
  RootDetected = false;
  delay(5000);
  return;
}

// Ethernetからの1文字読み取るコールバック関数
// 最初の'<'(タグの開始)まで読み飛ばし
//  → HTTPサーバーレスポンス文字を読み飛ばす
int getcFunc()
{
  while(!RootDetected)
  {
    if (getChar() == '<')
    {
      RootDetected = true;
      return '<';
    }
  }

  return getChar();
}

int getChar()
{
  if (client.available()) 
    return client.read();

  if (!client.connected()) 
    return EOF;
}

/* SAXパーサーのイベントハンドラー */
int startDocument(void) 
{
  Serial.println( "startDocument" );
  return 0;
}

int startElement( const char *tagname, struct NunniHashtable *args ) 
{
  const int size = NunniHashtableSize( args );
  char ** keys;
  int i, ret;
  const char *name, *value;
  keys = (char**)calloc( size, sizeof( char * ) );
  Serial.print("start element: ");
  Serial.println(tagname);
  ret = NunniHashtableKeys( args, keys );
  for ( i = 0; i < size; ++i ) {
    name = keys[i];
    value = NunniHashtableGet( args, name );
    Serial.print("  attrName: ");
    Serial.print(name);
    Serial.print("   attrValue: ");
    Serial.println(value);
  }
  memset( m_characters, 0, MAXLEN );
  return 0;
}

int characters( char ch[], int start, int length ) 
{
  int i = strlen( m_characters );
  if ( i == MAXLEN )
    return -1;
  strncat( m_characters, &(ch[start]), length );
  return 0;
}

int endElement( const char *tagname ) 
{
  int len;
  char *data = m_characters;
  while( isspace( *data ) ) {
    ++data;
  }
  len = strlen( data );
  while( isspace( data[--len] ) ) {
    data[len] = 0;
  }
  if ( data != NULL && strncmp( data, "", 1 ) )
  {
    Serial.print("text: ");
    Serial.println(data);
  }
  memset( m_characters, 0, MAXLEN );
  Serial.print("end element: ");
  Serial.println(tagname);

  return 0;
}

int endDocument(void) 
{
  Serial.println( "endDocument" );
  return 0;
}

実行結果は以下の通りです。IDEのSerial Monitorは漢字表示に対応していないため、UTF-8に対応したターミナルソフト(UTF-8 TeraTerm Proなど)に表示させます。

##connecting...
##connected
startDocument
start element: rss
start element: channel
start element: title
text: Yahoo!天気情報 - 東部(横浜)の天気
end element: title
start element: link
text: http://rd.yahoo.co.jp/rss/l/weather/days/*http://weather.yahoo.co.jp/weather/jp/14/4610.html
end element: link
start element: description
text: Yahoo! JAPANの天気情報に掲載されている最新の情報を提供しています。
end element: description
start element: language
text: ja
end element: language
start element: copyright
text: Copyright (C) 2009 Yahoo Japan Corporation. All Rights Reserved.
end element: copyright
start element: lastBuildDate
text: Mon, 24 Aug 2009 23:55:16 +0900
end element: lastBuildDate
start element: item
start element: title
text: 【 24日(月) 東部(横浜) 】 雨後曇 - 29℃/23℃ - Yahoo!天気情報
end element: title
start element: link
text: http://rd.yahoo.co.jp/rss/l/weather/days/*http://weather.yahoo.co.jp/weather/jp/14/4610.html?d=20090824
end element: link
start element: description
text: 雨後曇 - 29℃/23℃
end element: description
start element: pubDate
text: Mon, 24 Aug 2009 17:00:00 +0900
end element: pubDate
end element: item
start element: item
start element: title
text: 【 25日(火) 東部(横浜) 】 曇時々晴 - 28℃/22℃ - Yahoo!天気情報

end element: title
start element: link
text: http://rd.yahoo.co.jp/rss/l/weather/days/*http://weather.yahoo.co.jp/weather/jp/14/4610.html?d=20090825
end element: link
start element: description
text: 曇時々晴 - 28℃/22℃
end element: description
start element: pubDate
text: Mon, 24 Aug 2009 17:00:00 +0900
end element: pubDate
end element: item
~中略~
end element: channel
end element: rss
endDocument
##disconnecting.

上記のように、RSS情報を、要素名(タグ名)やTextに分解して取得できます。

サンプルスケッチ2

サンプルスケッチ1では、サイト情報全体を表示していますが、例えば上記の例で、1番目の"item"要素→"title"要素→text情報を抜き出すと、今日の天気を取得できます。このスケッチを以下に示します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include <NunniMCAX.h>
#include <Ethernet.h>

#define MAXLEN 256
static char m_characters[MAXLEN];
struct NunniMCAXContentHandler handler;
bool RootDetected = false;
int ItemCount = 0;
int ItemToGet = 1;

byte mac[] = { XX, XX, XX, XX, XX, XX };
byte ip[] = { 192, 168, 0, 111 };
byte server[] = { 124, 83, 139, 175 };    // rss.weather.yahoo.co.jp
Client client(server, 80);

void setup()
{
  delay(1000);
  Ethernet.begin(mac, ip);
  Serial.begin(115200);

  /* set up the paser handler functions */
  handler.startDocument = startDocument;
  handler.startElement = startElement;
  handler.characters = characters;
  handler.endElement = endElement;
  handler.endDocument = endDocument;
}

void loop()
{
  Serial.println("##connecting...");
  if (client.connect()) {
    Serial.println("##connected");
    client.write("GET /rss/days/4610.xml HTTP/1.0\r\n\r\n");
  } else {
    Serial.println("##connection failed");
    delay(5000);
    return;
  }

  // パーサーの呼び出し
  NunniMCAXparse( &getcFunc, &handler );

  Serial.println("##disconnecting.");
  Serial.println("");
  client.stop();
  for (;;);    // stop
}

// Ethernetからの1文字読み取るコールバック関数
// 最初の'<'(タグの開始)まで読み飛ばす
//  → HTTPサーバーレスポンス文字を読み飛ばす
int getcFunc()
{
  while(!RootDetected)
  {
    if (getChar() == '<')
    {
       RootDetected = true;
       return '<';
    }
  }

  return getChar();
}

int getChar()
{   
  if (client.available())
    return client.read();

  if (!client.connected())
    return EOF;   
}

/* SAXパーサーのイベントハンドラー */
int startDocument(void)
{   
  return 0;
}

int startElement( const char *tagname, struct NunniHashtable *args )
{   
  if (strcmp(tagname, "item") == 0)
    ItemCount++;
  memset( m_characters, 0, MAXLEN );
  return 0;
}

int characters( char ch[], int start, int length )
{
  int i = strlen( m_characters );
  if ( i == MAXLEN )
    return -1;
  strncat( m_characters, &(ch[start]), length );
  return 0;
}

int endElement( const char *tagname )
{
  int len;
  char *data = m_characters;
  while( isspace( *data ) ) {
    ++data;
  }
  len = strlen( data );
  while( isspace( data[--len] ) ) {
    data[len] = 0;
  }

  if ( data != NULL && strncmp( data, "", 1 ) )
    if (ItemCount == ItemToGet && strcmp(tagname, "title")  == 0)
    {
      Serial.println(data);
    }
  memset( m_characters, 0, MAXLEN );

  return 0;
}

int endDocument(void)
{
  return 0;
}

実行結果を以下に示します。

##connecting...
##connected
【 24日(月) 東部(横浜) 】 雨後曇 - 29℃/23℃ - Yahoo!天気情報
##disconnecting.

イベントハンドラー部分を書き替えることで抜き出す情報を制御できることが分かります。

メモリー使用量

気になるサンプルスケッチ2のメモリー使用量は以下です:

  • プログラムサイズ(ROM占有量):20944bytes
  • データーサイズ(SRAM占有量):3021bytes

データーサイズは、「Arduinoメールチェッカー(その3)」に記載した、avr-objdump -hコマンドで調べました。SRAMを3KB程度占有している点が大きく、Arduino Megaでないと実行できないサイズになっています。ATmega 328でも動かせる程度のメモリー量に収めたいところですが、NunniMCAXライブラリーはなかなかよく出来ており捨てがたいです。

« Arduino Ethernet Shieldと他SPIデバイスの混在 | トップページ | SPIデバイスの混在とSS信号の初期状態 »

Arduino」カテゴリの記事

コメント

この記事へのコメントは終了しました。

2018年10月
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
無料ブログはココログ