C++の練習を兼ねてCSVパーサーを書いてみました!
こんにちは、めのんです!
平日はZigの話題が中心で、週末は主にOpenSiv3Dの話題だったんですけど、今回はちょっと違うことに挑戦してみました。
Twitterでほえほえ@LWPさんが「(CSVを)ベタなコードで解析してほしい」とおっしゃっていましたので、さっそく私も挑戦してみました。 使ったプログラミング言語はC++です。 Zigはちょっとまだ無理です💦
CSVの話題は定期的にでてくるのだがw、実戦投入する/しないはともかく、題材としては一級品なのでぜひ自分でベタなコードで解析してほしい。その過程で多くの技法が習得できると思う。項目内改行がなければTLにあるような方式でよいけどCSVの規格よんだり実装は一度はチャレンジしてほいたほうがいい
— ほえほえ@LWP (@hoehoe1234) 2020年7月18日
まずはCSVの規格の確認からですね。 これはRFC 4180が相当すると思います。
RFC 4180そのままではASCIIしか対応できないと思いましたので、Excelで作成したCSVを読めるように拡張しました。 というか判定がザルなだけなんですけどね。
あと、Excel for Macが出力したCSVは改行文字がCRだということですので、そちらにも対応したつもりです。
では、私の書いたコードを貼ることにします。
#include "menonfled/csv.hpp" #include <utility> #include <stdexcept> namespace menonfled { namespace { /// TEXTDATAの読み込み /// @param[in] is 入力ストリーム /// @return TEXTDATAであればその文字を、それ以外であればナル文字を返す。 char get_textdata(std::istream& is) { char c; if (is.get(c)) { if (c < 0x20 || c == 0x7f || c == ',' || c == '"') is.unget(); else return c; } return '\0'; } /// non-escapedの読み込み /// @param[in] is 入力ストリーム /// @param[in] c 先頭の1文字 /// @return 読み込んだフィールドを返す。 std::string get_nonescaped(std::istream& is, char c) { std::string str; do { str.push_back(c); c = get_textdata(is); } while (c); return std::move(str); } /// escapedの読み込み /// @param[in] is 入力ストリーム /// @return 読み込んだフィールドを返す。 /// 先頭の二重引用符はスキップ済み std::string get_escaped(std::istream& is) { std::string str; char c; while (is.get(c)) { if (c == '"') { char c2; if (!is.get(c2) || c2 != '"') break; } str.push_back(c); } return std::move(str); } /// fieldの読み込み /// @param[in] is 入力ストリーム /// @param[out] field 入力したフィールドの格納先 /// @return EOFに達した場合はfalseを、それ以外はtrueを返す。 bool get_field(std::istream& is, std::string& field) { std::string str; char c; bool escaped = false; if (!is.get(c)) return false; switch (c) { case '\r': case '\n': case ',': is.unget(); field.clear(); break; case '"': field = get_escaped(is); break; default: field = get_nonescaped(is, c); } return true; } } /// CSVの1レコードを読み込む /// @param[in] is 入力ストリーム /// @return 読み込んだレコードを文字列の列として返す。 std::vector<std::string> get_csv_record(std::istream& is) { std::vector<std::string> record; while (is) { std::string field; char c; if (!get_field(is, field)) break; record.push_back(field); if (!is.get(c)) break; if (c == '\r') { if (is.get(c) && c != '\n') is.unget(); break; } if (c == '\n') break; } if (record.size() == 1 && record.front().empty()) record.clear(); return std::move(record); } }
いろいろ試行錯誤しながら書いたのでキレイなコードとはいいにくいのですが、恥を忍んで貼ってみました。 こういうのもたまにはいいですね!
それでは!!