RSS リーダー 〜 HTTP と XML の BREW C++ プログラミング 〜
RSS データの解析
RssReader のデータ構造
// RSSItem 構造体 ( アイテム 1 件 ) SFMTYPEDEFSTRUCT(RSSItem) struct RSSItem { SFXAnsiString title; SFXAnsiString description; SFXAnsiString topic; SFXDate date; // 日付をSFXDate形式で格納 Bool alreadyRead; // 既読かどうかを表すフラグ RSSItem(SFXAnsiStringConstRef rsstitle, SFXAnsiStringConstRef rssdescription, SFXAnsiStringConstRef rsstopic, SFXDateConstRef rssdate, Bool rssalreadyRead = false); }; // RSSFeed クラス ( フィード 1 件 ) SFMTYPEDEFCLASS(RSSFeed) class RSSFeed { public: SFXAnsiString _title; // サイトのタイトル SFXAnsiString _url; // サイトの URL SFXDate _date; // 最終更新日時 private: SFXAnsiString _filename; // RSS を保存するファイル名 SFXList<RSSItemPtr> _itemList; // 複数の RSS アイテムを保存 public: RSSFeed(SFXAnsiStringConstRef title, SFXAnsiStringConstRef url, SFXDateConstRef date = SFXDate()); ~RSSFeed(Void); SFCError Parse(SFXAnsiStringConstRef xml); SFCError SaveToFile(Void); Void LoadFromFile(Void); Bool Find(RSSItemConstRef item); RSSItemPtr GetItem(SInt32 index) { return _itemList.Get(index); } SFXAnsiStringConstRef GetFilename(Void) { return _filename; } Void SetFilename(SFXAnsiStringConstRef filename) { _filename = filename; } SInt32 GetSize(Void) { return _itemList.GetSize(); } friend class RSSFeedList; }; // RSSFeedList クラス ( フィードのリスト ) SFMTYPEDEFCLASS(RSSFeedList) class RSSFeedList { private: SFXArray<RSSFeedPtr> _feedArray; // 複数の RSS フィードを保存 public: ~RSSFeedList(Void); Bool Add(RSSFeedPtr feed); SFCError SaveToFile(Void); Void LoadFromFile(Void); SInt32 GetSize(Void) { return _feedArray.GetSize(); } SInt32 IndexOf(RSSFeedPtr item) { return _feedArray.IndexOf(item); } RSSFeedPtr operator[](SInt32 index) { return _feedArray[index]; } };
※ "SFXList<RSSItemPtr> _itemList;" について
SophiaFramework UNIVERSE の コレクションクラスには 格納できるオブジェクトのサイズが 4 バイトまでなので、 SFXList は、RSSItem ではなく、 RSSItemPtr ( ポインタ ) を格納するリストにしています。
RSSItemPtr への要素の追加
RSSItemPtr item; // 要素を new で生成 item = ::new RSSItem("title", "desc", "topic", SFXDate::CurrentDate()); // 要素の格納 _itemList.Append(item);
RSSItemPtr への要素の参照
// 最初の要素を参照 _itemList.Get(0);
RSSFeed のメンバ変数のファイルへの書き込み
// RSSFeed クラスをファイル保存するときのデータフォーマット title<>description<>topic<>date<>alreadyRead\r\n title<>description<>topic<>date<>alreadyRead\r\n ...
date は UInt32 型の整数、alreadyRead は true なら 1、false なら 0 とします。
#define DIRECTORY_NAME "/data" #define DELIMITER_STR "<>" #define DELIMITER_LEN 2 SFCError RSSFeed::SaveToFile(Void) { SFCError error; SFXPath temp; SFXFile file; SFXAnsiStringStreamWriter writer; RSSItemPtr item; SFXPath filename(DIRECTORY_NAME + _filename); SFXPath dir(DIRECTORY_NAME); // 一時保存のためのパスを取得 error = SFXFile::GetTemporaryPath(dir, "temp", "", &temp); if (error == SFERR_NO_ERROR) { error = file.OpenReadWrite(temp); // ファイルを開く if (error == SFERR_NO_ERROR) { // 書き込みストリームを取得 error = file.GetStreamWriter(4096, &writer); if (error == SFERR_NO_ERROR) { // イテレータを取得 SFXList<RSSItemPtr>::Iterator itor(_itemList.GetFirstIterator()); while (itor.HasNext()) { // RSSItemをファイルに書き出す item = itor.GetNext(); writer << item->title << DELIMITER_STR << item->description << DELIMITER_STR << item->topic << DELIMITER_STR << SFXAnsiString::Format("%u", item->date.AsUInt32()) << DELIMITER_STR << ((item->alreadyRead) ? "1" : "0") << "\r\n"; // Flush 関数を呼ぶとファイルに書き込まれる writer.Flush(); } } file.Close(); } } if (error == SFERR_NO_ERROR) { // ファイル消去 SFXFile::Remove(filename); // ファイル名変更 error = SFXFile::Rename(temp, filename); } return error; }
SFXFile::GetTemporaryPath 関数
SFXFile::GetTemporaryPath 関数で一時ファイル名を取得しています。
一時的なファイルにデータを書き込んでから本来のファイル名にしています。 このようにすれば、ファイルへの書き込みが途中で失敗しても安全です。
ファイルからの読み込み
Void RSSFeed::LoadFromFile(Void) { SFCError error; SFXFile file; SFXAnsiStringStreamReader reader; SFXAnsiString string; SFXPath path(DIRECTORY_NAME + _filename); UInt32 size; SFXFile::GetSize(path, &size); // ファイルサイズの取得 error = file.OpenReadOnly(path); // ファイルオープン if (error == SFERR_NO_ERROR) { // ファイルオープンに成功したなら // 読み取りストリームを取得 error = file.GetStreamReader(size + 1, &reader); if (error == SFERR_NO_ERROR) { // ファイルから読み取り reader.Fetch(); // string に格納 reader.ReadSFXAnsiString(&string); SInt32 c1 = 0; SInt32 c2, c3, c4, c5, c6; while (true) { // 行の最初の区切りを探す c2 = string.IndexOf(DELIMITER_STR, c1); if (c2 < 0) { // 見つからなかったら行がないので終了 break; } c3 = string.IndexOf(DELIMITER_STR, c2 + 1); c4 = string.IndexOf(DELIMITER_STR, c3 + 1); c5 = string.IndexOf(DELIMITER_STR, c4 + 1); c6 = string.IndexOf('\n', c5) + 1; SFXAnsiString dateNumber = string.Substring(c4 + DELIMITER_LEN, c5); SFXDate date; // 日時は数字で格納されているので、それを取り込む date.Set(dateNumber.ToUInt32()); RSSItemPtr item = new RSSItem(string.Substring(c1, c2), string.Substring(c2 + DELIMITER_LEN, c3), string.Substring(c3 + DELIMITER_LEN, c4), date, string.Substring(c5 + DELIMITER_LEN, c6).Trim() == "1"); // リストに加える if (_itemList.Append(item) != SFERR_NO_ERROR) { ::delete item; break; } c1 = c6; } } file.Close(); } return; }
※ ファイルの場合、Fetch 関数の処理はすぐ完了するので、コールバック関数の登録は不要です。
RSS データの解析
DOM 方式の XML パーサー SFXXMLDOMParser を使います。
SFXXMLDOMParser の Parse 関数で XML 文書 を解析した結果を GetDocument 関数で取得します。
ノードの処理では、最初の子ノードを取得する GetFirstChild 関数、兄弟ノードを取得する GetNextSibling 関数、ノード名を取得する GetNodeName 関数 などを使います。
SFCError RSSFeed::Parse(SFXAnsiStringConstRef xml) { SFCError error; SFXXMLDOMParser parser; SFXXMLDocumentPtr root; SFXXMLNodePtr child; SFXAnsiString title; SFXAnsiString description; SFXDate date; RSSItemPtr item; SInt32 listcount; SInt32 count = 0; SFXAnsiString temp; // XML を解析する error = parser.Parse(xml); // 解析が失敗した場合 if (error != SFERR_NO_ERROR) return SFERR_FAILED; root = parser.GetDocument(); // ルートを取得する if (root == null) return SFERR_FAILED; // Item タグを探す SFXList<SFXXMLNodePtr>* list = root->GetElementsByTagName("channel"); // Item タグがあった場合 if (list != null && list->GetSize() > 0) { // 要素を取得する child = list->Get(0); for (child = child->GetFirstChild() ; child != null && error == SFERR_NO_ERROR ; child = child->GetNextSibling()) { if (SFXAnsiString(child->GetNodeName()) == "title") { temp = child->GetText(); if (_title.IsEmptyCString()) { error = SFXTextEncoding::UTF8ToShiftJIS(temp, &_title); } } else if (SFXAnsiString(child->GetNodeName()) == "dc:date") { // 文字列を解析して日付に変換 date.Parse("YYYY-MM-DD%Thh:mm:ss+", child->GetText()); } } SFXList<SFXXMLNodePtr>* list = root->GetElementsByTagName("item"); if (list != null && list->GetSize() > 0) { for (listcount=0; listcount < (list->GetSize()); listcount++) { child = list->Get(listcount); for (child = child->GetFirstChild() ; child != null && error == SFERR_NO_ERROR ; child = child->GetNextSibling()) { // title タグなら if (SFXAnsiString(child->GetNodeName()) == "title") { // ノードのテキストを取得する temp = child->GetText(); // UTF-8 を ShiftJIS に変換する error = SFXTextEncoding::UTF8ToShiftJIS(temp, &title); } // description タグなら else if (SFXAnsiString(child->GetNodeName()) == "description") { // ノードのテキストを取得する temp = child->GetText(); // UTF-8 を ShiftJIS に変換する error = SFXTextEncoding::UTF8ToShiftJIS(temp, &description); } // dc: date タグなら else if (SFXAnsiString(child->GetNodeName()) == "dc: date") { // 文字列を解析して日付に変換する date.Parse("YYYY-MM-DD%Thh:mm:ss+", child->GetText()); } } item = ::new RSSItem(title, description, date); if (item != null) { // 重複がないなら if (!Find(*item)) { // 要素を挿入 error = _itemList.Insert(count, item); if (error == SFERR_NO_ERROR) { ++count; } else { ::delete item; } } else { ::delete item; } } else { error = SFERR_NO_MEMORY; } } } } else { TRACE ("list empty"); } // エラー処理 if (error != SFERR_NO_ERROR) { _title = "解析失敗"; } SFXHelper::dbgprintf("[SGXAWSParser] ParseXML END %d", error); if (error == SFERR_NO_ERROR) { // ファイルに保存 error = RSSFeed::SaveToFile(); } return error; }