• Showing Page History #113757

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

HTMLテーブルから必要な情報を取得してCSV等に変換

概要

何らかの情報を表として出力する html から、表の情報を自動抽出するプログラムを作成したい場合、HTMLのパース→TABLE構造の解析→ジャグ配列に格納→CSV等で出力、と、結構面倒な処理手順が必要となります。 HTMLのパースは既存ライブラリを使えばできそうですが、それ以降の、「TABLE構造の解析」や、「ジャグ配列に格納」はズバリやってくれるライブラリがなかなかありませんので、サンプルとして作ってみました。 TABLEが複数ある場合は、全てのTABLEに対し、処理します。ネストしている(TDタグの中に別のTABLEが入っている)ケースでも処理できるようになっています。

ライセンス:Boostライセンス

使用しているライブラリ

HTML読み込み、整形用ライブラリです。今回はHTML読み込みに使いました。

修正履歴

  • 2015/11/26 バグ修正

ダウンロード

ソース等貼り付け

処理対象データの例(閉じタグがあまりまじめに入っていない、ちょっと処理しにくそうなデータ)

<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

ソース(C++)

// 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;
}