何らかの情報を表として出力する html から、表の情報を自動抽出するプログラムを作成したい場合、HTMLのパース→TABLE構造の解析→ジャグ配列に格納→CSV等で出力、と、結構面倒な処理手順が必要となります。 HTMLのパースは既存ライブラリを使えばできそうですが、それ以降の、「TABLE構造の解析」や、「ジャグ配列に格納」はズバリやってくれるライブラリがなかなかありませんので、サンプルとして作ってみました。 TABLEが複数ある場合は、全てのTABLEに対し、処理します。ネストしている(TDタグの中に別のTABLEが入っている)ケースでも処理できるようになっています。
ライセンス:Boostライセンス
<div> <table border="1"> <tr> <td>1-1 <td><input type="button" onclick='func("destination.php", "type1")' value="open"></input> <td><img src="./1-3.png"> <tr> <td>2-1 <td><a href="./hogehoge.html"></a> <td><img src="./2-3.png"></td> <tr> <td>3-1 <td> <table> <tr> <td>1-1 <td>1-2 <tr> <td>2-1 </table> <table> <tr> <td>1-1 <td>1-2 <tr> <td>2-1 </table> <table> <tr> <td>2-1 </table> <td><img src="./3-3.png"> </table> </div> <p>END</p>
[table:0] 1-1,destination.php?type=type1,./1-3.png 2-1,./hogehoge.html,./2-3.png 3-1,(null),./3-3.png [table:1] 1-1,1-2 2-1 [table:2] 1-1,1-2 2-1 [table:3] 2-1
// Copyright Mocchi 2019 // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) #include "tidy.h" #include "tidybuffio.h" #include <cstdio> #include <cstdlib> #include <cstring> #include <vector> #include <map> // 処理内容 // [処理(1)] 1つめのコマンドライン引数で示したhtmlファイルを読み込む。 // [処理(2)] ADD_HTMLBODYTAG が定義されているときは、 読み込んだファイル内容を <html>タグ、<body>タグで括る。 // [処理(3)] html tidy でパースする。 // [処理(4)] パースしたノードを深さ優先探索し、 tableタグを見つけたときに table情報格納配列(各tdノード、およびそこから抽出した情報を格納するための2次元配列)を生成する。 // [処理(5)] 取得した各 td ノードから、ParseTD 関数で情報を抽出する。 /// ====================== /// ===== 文字列処理 ===== /// ====================== // バッファに入っているヌル終端文字列から、改行文字を除去する。 void remove_cr(TidyBuffer &buf){ byte *str_t = buf.bp, *str_s = buf.bp; while(*str_t){ while (*str_s && (*str_s == '\n' || *str_s == '\r')){ ++str_s; } if (str_t < str_s){ *str_t = *str_s; } ++str_s; ++str_t; } *str_t = '\0'; } // 指定された文字列長分、バッファに追記し、ヌル終端を追加する。 void append_nstring(TidyBuffer &dest, const char *str, size_t len){ if (dest.size > 0 && dest.bp[dest.size-1] == '\0') dest.size--; // destにあるヌル終端の位置を自動的に付け替える処理を持っているappend関数を使用する場合は、上記処理は不要 tidyBufAppend(&dest, const_cast<char *>(str), len); tidyBufPutByte(&dest, '\0'); } // ヌル終端文字列 str の文字列長を調べ、そのヌル終端までをバッファに追記する。 void append_string(TidyBuffer &dest, const char *str){ append_nstring(dest, str, std::strlen(str)); } // ダブルクォートで括られた範囲を抽出し、終端ヌル文字を足した形でバッファに格納する。 // 戻り値として閉じクォートの次の文字のポインタを返す。 // 括られた範囲が見つからなかった場合はバッファには何もせずに NULL を返す。 const char *extract_quoted_string(const char *src, TidyBuffer &dest){ const char *p1 = std::strchr(src, '"'); if (!p1) return 0; const char *p2 = std::strchr(p1+1, '"'); if (!p2) return 0; size_t sz = p2 - p1; append_nstring(dest, const_cast<char *>(p1 + 1), sz - 1); return p2+1; } /// ============================== /// ===== ツリー構造解析処理 ===== /// ============================== typedef std::pair<TidyNode, TidyBuffer> td_type; typedef std::vector<td_type> tr_type; typedef std::vector<tr_type> table_type; // 深さ優先探索 TidyNode DepthFirstNext(TidyNode cur, bool skip_child, int &depth){ if (!skip_child){ // まず、子ノードを探索 TidyNode child = tidyGetChild(cur); if (child){ depth++; return child; } } // ない場合は次の兄弟ノードを探索 TidyNode next = tidyGetNext(cur); if (next) return next; // ない場合は親の次の兄弟ノードがある場合はそれを、ない場合はさらに親を探索 for(;;){ TidyNode parent = tidyGetParent(cur); depth--; if (!parent){ cur = 0; break; } TidyNode parent_next = tidyGetNext(parent); if (parent_next){ cur = parent_next; break; } else cur = parent; } return cur; } // TDタグから必要な情報を抽出する処理 // 下記のような形で情報を抽出したい場合の例 // <td><input type="button" onclick="open_form("HogeHogeForm.php", "type1") value="Open"></input></td> // => HogeHogeForm.php?type=type1 // <td><img src="./TestImage.png"></td> // => ./TestImage.png // <td><a href="./HogeHoge.html">Link</a></td> // => ./HogeHoge.html // <td>hello</td> // => hello void ParseTD(TidyDoc doc, td_type &td){ TidyNode td_node = td.first; int depth = 1; bool skip_child = false; for(TidyNode cur = tidyGetChild(td_node); depth > 0 && cur; cur = DepthFirstNext(cur, skip_child, depth)){ skip_child = false; ctmbstr name = tidyNodeGetName(cur); if (name){ // テーブルが入れ子になっている場合にもう一度ツリー構造を解析してしまわないよう、スキップする。 if (std::strcmp(name, "table") == 0){ skip_child = true; continue; } for (TidyAttr attr = tidyAttrFirst(cur); attr; attr = tidyAttrNext(attr)){ const char *attrname = tidyAttrName(attr); // **************************** // *** 状況に応じて書き換え *** if (std::strcmp(attrname, "onclick") == 0){ ctmbstr value = tidyAttrValue(attr); if (value){ TidyBuffer &tb = td.second; const char *p = value; p = extract_quoted_string(p, tb); append_string(td.second, "?type="); p = extract_quoted_string(p, tb); } }else if (std::strcmp(attrname, "href") == 0 || std::strcmp(attrname, "src") == 0){ ctmbstr value = tidyAttrValue(attr); if (value) append_string(td.second, value); }else continue; // **************************** } }else{ // **************************** // *** 状況に応じて書き換え *** // href で情報を抽出済の場合は子のtextノードの情報は不要なため、それらの情報がないときのみテキストノードの内容を書き出す if (td.second.size == 0){ tidyNodeGetText(doc, cur, &td.second); remove_cr(td.second); } // **************************** } } } // bodyタグの内部だけが与えられるケースでも // tidyParse でツリー構造を取得できるようにするためのオプション #define ADD_HTMLBODYTAG int main(int argc, char *argv[]){ // ************** // [処理(1)] 1つめのコマンドライン引数で示したhtmlファイルを読み込む。 ***** // ************** // htmlファイルオープン if (argc < 2) return 0; FILE *fp = std::fopen(argv[1], "r"); std::fseek(fp, 0, SEEK_END); size_t sz = std::ftell(fp); std::rewind(fp); // ************** // [処理(2)] ADD_HTMLBODYTAG が定義されているときは、 読み込んだファイル内容を <html>タグ、<body>タグで括る。 // ************** size_t header = 0; TidyBuffer html; tidyBufInit(&html); #ifdef ADD_HTMLBODYTAG append_string(html, "<html><body>"); header = html.size-1; #endif tidyBufCheckAlloc(&html, header + sz, 0); size_t sz_r = std::fread(html.bp + header, 1, sz, fp); html.size += sz_r; std::fclose(fp); #ifdef ADD_HTMLBODYTAG append_string(html, "</body></html>"); #else tidyBufPutByte(&html, '\0'); #endif // ************** // [処理(3)] html tidy でパースする。 // ************** TidyDoc doc = tidyCreate(); tidySetCharEncoding(doc, "shiftjis"); tidyOptSetBool(doc, TidyShowInfo, no); tidyOptSetBool(doc, TidyShowWarnings, no); #ifdef NDEBUG tidyOptSetBool(doc, TidyShowErrors, no); #endif tidyParseString(doc, reinterpret_cast<ctmbstr>(html.bp)); // tidyRunDiagnostics(doc); std::vector<table_type> tables; std::vector<std::pair<size_t, int> > table_stack; // first: table系ノード、 second: ノードスタックの深さ // ************** // [処理(4)] パースしたノードを深さ優先探索し、 tableタグを見つけたときに table情報格納配列(各tdノード、およびそこから抽出した情報を格納するための2次元配列)を生成する。 // ************** int depth = 0; for(TidyNode cur = tidyGetRoot(doc); cur; cur = DepthFirstNext(cur, false, depth)){ while(table_stack.size() && table_stack.back().second >= depth){ table_stack.resize(table_stack.size()-1); } ctmbstr name = tidyNodeGetName(cur); if (name){ if (std::strcmp(name, "table") == 0){ table_stack.push_back(std::make_pair(tables.size(), depth)); tables.push_back(table_type()); }else if (table_stack.size()){ table_type &cur_table = tables[table_stack.back().first]; if (std::strcmp(name, "tr") == 0 || std::strcmp(name, "th") == 0){ cur_table.push_back(tr_type()); }else if(std::strcmp(name, "td") == 0){ cur_table.back().push_back(std::make_pair(cur, TidyBuffer())); tidyBufInit(&cur_table.back().back().second); } } } } // ************** // [処理(5)] 取得した各 td ノードから、ParseTD 関数で情報を抽出する。 // ************** for (size_t k = 0; k < tables.size(); ++k){ table_type &table = tables[k]; std::printf("[table:%u]\n", k); for (size_t j = 0; j < table.size(); ++j){ tr_type &tr = table[j]; for (size_t i = 0; i < tr.size(); ++i){ td_type &td = tr[i]; ParseTD(doc, td); std::printf("%s", td.second.bp); if (i < tr.size() -1) std::printf(","); } std::printf("\n"); } } tidyRelease(doc); return 0; }