csv 파일
csv(comma-separated values) 파일은 쉼표로 구분되는 문자열을 나열한 파일 형식이다.
엑셀과 메모장에서 csv파일을 쉽게 보고 편집할 수 있다.
데이터베이스를 csv 파일로 구축하면 각 필드를 쉼표로 구분하여 튜플들을 저장할 수 있다.
작성 규칙
- 각 필드는 띄어쓰기 없이 , 로 구분한다.
- 각 튜플은 개행문자 \n, \r 로 구분한다.
- 문자열 필드일 경우 " " 로 감싼다.
- 문자열 내에서 " 를 사용할 때에는 "" 와 같이 두 번씩 쓴다.
- 문자열 내에서의 ,(쉼표), \n, \r 는 구분자가 아닌 문자열 내의 문자로 취급한다.
- 집합 필드일 경우 { } 로 감싸고, , 대신 공백(띄어쓰기, 탭, 개행문자)으로 구분한다.
예시
- "월요일","화요일","수요일","목요일"
- 200,{300 400},{2.3 4.5 6.7}
- 100,"플레이어","플레이어의 ""공격"" ",{20 40 60}
- {10 2.4 "오늘은 ""월요일"" 입니다.
월요일 좋아~"},"스폰지밥",50
csv 문서 파싱
파싱(parsing)이란 데이터를 특정 패턴이나 순서로 추출해 가공하는 것을 말한다.
csv 파일로 데이터베이스를 구축할 경우, csv 파일을 읽어와 데이터베이스의 자료구조에 저장할 필요가 있다.
csv 파일은 하나의 거대한 문자열인데, 실제로 자료구조에 필요한 자료형은 int, double, string 등등이다.
일단, csv 문서를 파싱하여 필드 단위의 조각 문자열들을 추출해 행렬구조에 담는다.
튜플의 갯수가 그대로 행의 갯수가 되고 튜플 당 필드의 갯수가 열의 갯수가 된다.
작성 규칙이 위에 정해져 있으므로, 그 규칙에 따라 문자열들을 분리할 수 있다.
분리된 문자열을 std::stoi, std::stod 나 std::stringstream 을 이용하여 실제 필요한 타입에 맞는 데이터로 변환한 뒤 데이터베이스에 저장한다. 이 글에서는 데이터베이스 구축 이전, csv 문서를 파싱하는 것까지만 다룬다.
아이디어
- csv 파일의 모든 행을 읽어 행렬로 반환한다.
- 파일에서 문자 하나를 읽는다.
- " " { } , \r \n 을 제외한 문자는 문자열 스트림에 저장된다.
- " " 와 { } 에 대해 플래그 변수를 두어, 열고 닫을 때 각각 활성화, 비활성화한다.
- " " 안에 "" 가 등장하는 경우 문자열 내 하나의 문자로 취급한다.
- ,는 " " 플래그가 비활성화되어 있을 때 필드의 구분을 의미, 문자열 스트림을 현재 읽고 있는 행에 문자열 필드로 추가하고 문자열 스트림을 초기화한다.
- \r와 \n은 플래그가 비활성화되어 있을 때 한 행의 종료를 의미, 문자열 스트림을 현재 읽고 있는 행에 문자열 필드로 추가하고 문자열 스트림을 초기화한 뒤 읽어들인 행을 반환할 행렬에 추가한다.
모든 행 읽기
#ifndef _database
#define _database
#include <fstream>
#include <string>
#include <vector>
#include <sstream>
namespace _csv
{
// ...
template <typename Str_t>
auto csv_read(const Str_t file_path)
{
std::ifstream in(file_path);
std::vector<std::vector<std::string>> parsed;
parsed.reserve(0x20);
if (in.fail())
throw std::ios_base::failure{ "csv_read : cannot open file" };
while (in.good())
parsed.push_back(_csv::csv_read_row(in, ','));
parsed.shrink_to_fit();
return parsed;
}
// ...
}
// ...
#endif
|
cs |
const char*, const wchar*, const string, const wstring 을 전부 받을 수 있도록 템플릿 함수로 만들었다.
auto csv_read(const Str_t file_path) 대신 auto csv_read(Str_t&& file_path) 와 같이 선언하면 보편 참조로 Str_t를 받을 수 있는데, 이 경우 왼값 오른값, 자료형 그 무엇으로부터도 자유롭지만 Str_t가 const char[13], const char[14] 등의 배열 타입으로 연역되는 경우
크기가 다른 배열은 서로 다른 타입이기 때문에 너무 많은 템플릿 인스턴스가 만들어진다.
크기가 다른 배열들을 동일하게 포인터 타입으로 연역할 수 있고 string이나 wstring이 전달되어 오는 경우에도
파일 경로에 해당하는 문자열은 그리 길지 않아 복사로 인한 성능 손실이 작기 때문에 보편 참조 대신 값에 의한 전달(pass-by-value)을 선택했다.
만일 string, wstring을 이 함수에 인자로 주는 경우 std::move를 활용하면 이러한 성능 손실도 메꿀 수 있다.
한 행 읽기
#ifndef _database
#define _database
#include <fstream>
#include <string>
#include <vector>
#include <sstream>
namespace _csv
{
auto csv_read_row(std::istream& in, char delimiter)
{
std::stringstream ss;
bool inquotes = false;
bool packs = false;
std::vector<std::string> row;
while (in.good())
{
char c = in.get();
switch (c)
{
case '"':
if (!inquotes) // begin quote char
inquotes = true;
else
{
if (in.peek() == '"') // 2 consecutive quotes resolve to 1
ss << static_cast<char>(in.get());
else // end quote char
inquotes = false;
}
break;
case '{':
if (!packs)
packs = true;
else
throw std::ios_base::failure{ "csv_read_row : '{' duplicated" };
break;
case '}':
if (!packs)
throw std::ios_base::failure{ "csv_read_row : '{' must be followed by '}'" };
else
packs = false;
break;
case '\r': case '\n':
if (!inquotes && !packs)
{
if (in.peek() == '\n')
in.get();
row.push_back(ss.str());
row.shrink_to_fit();
return row;
}
break;
default:
if (c == delimiter && !inquotes)
{
row.push_back(ss.str());
ss.str("");
}
else
ss << c;
break;
}
}
}
// ...
}
// ...
#endif
|
cs |
위의 아이디어 대로,
- 파일에서 문자 하나를 읽는다.
- " " { } , \r \n 을 제외한 문자는 문자열 스트림에 저장된다.
- " " 와 { } 에 대해 플래그 변수를 두어, 열고 닫을 때 각각 활성화, 비활성화한다.
- " " 안에 "" 가 등장하는 경우 문자열 내 하나의 문자로 취급한다.
- ,는 " " 플래그가 비활성화되어 있을 때 필드의 구분을 의미, 문자열 스트림을 현재 읽고 있는 행에 문자열 필드로 추가하고 문자열 스트림을 초기화한다.
- \r와 \n은 플래그가 비활성화되어 있을 때 한 행의 종료를 의미, 문자열 스트림을 현재 읽고 있는 행에 문자열 필드로 추가하고 문자열 스트림을 초기화한 뒤 읽어들인 행을 반환할 행렬에 추가한다.
전체 코드
#ifndef _database
#define _database
#include <fstream>
#include <string>
#include <vector>
#include <sstream>
#include <tuple>
namespace _csv
{
auto csv_read_row(std::istream& in, char delimiter)
{
std::stringstream ss;
bool inquotes = false;
bool packs = false;
std::vector<std::string> row;
while (in.good())
{
char c = in.get();
switch (c)
{
case '"':
if (!inquotes) // begin quote char
inquotes = true;
else
{
if (in.peek() == '"') // 2 consecutive quotes resolve to 1
ss << static_cast<char>(in.get());
else // end quote char
inquotes = false;
}
break;
case '{':
if (!packs)
packs = true;
else
throw std::ios_base::failure{ "csv_read_row : '{' duplicated" };
break;
case '}':
if (!packs)
throw std::ios_base::failure{ "csv_read_row : '{' must be followed by '}'" };
else
packs = false;
break;
case '\r': case '\n':
if (!inquotes && !packs)
{
if (in.peek() == '\n')
in.get();
row.push_back(ss.str());
row.shrink_to_fit();
return row;
}
break;
default:
if (c == delimiter && !inquotes)
{
row.push_back(ss.str());
ss.str("");
}
else
ss << c;
break;
}
}
}
template <typename Str_t>
auto csv_read(const Str_t file_path)
{
std::ifstream in(file_path);
std::vector<std::vector<std::string>> parsed;
parsed.reserve(0x20);
if (in.fail())
throw std::ios_base::failure{ "csv_read : cannot open file" };
while (in.good())
parsed.push_back(_csv::csv_read_row(in, ','));
parsed.shrink_to_fit();
return parsed;
}
// ...
}
template <typename... TupleElems>
class database
{
public:
template <typename Str_t>
database(const Str_t file_path)
{
auto parsed_csv = _csv::csv_read(file_path);
// ...
}
private:
std::vector<std::tuple<TupleElems...>> impl;
};
#endif
|
cs |
테스트
#ifndef _database
#define _database
#include <fstream>
#include <string>
#include <vector>
#include <sstream>
#include <tuple>
namespace _csv
{
// ...
#include <iostream>
void test_csv_read()
{
auto parsed = csv_read("testcsv.csv");
for (const auto& row : parsed)
{
for (const auto& key : row)
{
std::cout << "[ " << key << " ] ";
}
std::cout << std::endl;
}
}
}
// ...
#endif
|
cs |
" ", { } 가 활용된 부분에서도 문제없이 파싱되었다.
'Project > Win32API로 던그리드 모작' 카테고리의 다른 글
std::stringstream 을 통해 문자열을 값으로 (0) | 2021.08.23 |
---|---|
코드 리팩토링 (0) | 2021.08.22 |
게임 프레임 관리 (0) | 2021.08.19 |
빌더 패턴 - 직사각형 만들기 (0) | 2021.08.18 |
* passkey 패턴 : public 멤버 함수의 접근 권한 관리로 public 함수를 non-public 하게 (0) | 2021.08.18 |