csv2 源码剖析:轻量级 CSV 处理的高效之道

深入解析 csv2 的设计与实现

纸上得来终觉浅,绝知此事要躬行。——陆游《冬夜读书示子聿》

csv2是一个轻量级 C++ 库,用于将 CSV 文件解析为 C++ 中的 STL 容器。该库的主要功能是高效地处理 CSV 数据,简化了处理 CSV 文件的代码编写过程。以下是它的主要特性:

  1. 简单易用:通过使用 STL 容器(如 std::vector 和 std::tuple),使得开发者能够轻松将 CSV 文件的内容转换为标准 C++ 数据结构。
  2. 依赖少:该库只有 C++17 标准库的依赖,因此不需要额外的第三方库。
  3. 高效解析:该库采用高效的解析机制,支持处理大型 CSV 文件。
  4. 轻量级:代码库很小,适用于嵌入式或对依赖库要求较高的项目。

准备

项目源代码地址为p-ranav/csv2 v1.0。

阅读工具为 CLion。

剖析

整个项目包含 4 个文件,分别是: reader.hppmio.hppwriter.hppparameters.hpp

.c vs .cc vs. .cpp vs .hpp vs .h vs .cxx: 由于历史渊源,造成头文件和源代码文件有些不同的命名方式,但本质而言没有什么区别。

reader.hpp

reader.hpp 文件中主要定义了一个名为Reader的类。数据部分主要有:

/images/202410/16/6.png

紧接着定义了两个方法: mmapparse,分别从文件和字符串内容解析内容。

从文件中解析内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "csv2/reader.hpp"
#include <string>

using namespace std;

int main(){
	csv2::Reader<csv2::delimiter<','>,
            csv2::quote_character<'"'>,
            csv2::first_row_is_header<true>,
            csv2::trim_policy::trim_whitespace> csv;


    std::string content = "Name, Age\nPeter, 12\nLucy, 78";
    if(csv.parse(content)){
        const auto header = csv.header();
        for (const auto row: csv) {
            for (const auto cell: row) {
                // Do something with cell value
                std::string value;
                cell.read_value(value);
                cout << value << " ";
            }
            cout << "\n";
        }
    }
}

从字符串中解析内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "csv2/reader.hpp"
#include <string>

using namespace std;

int main(){
	csv2::Reader<csv2::delimiter<','>,
            csv2::quote_character<'"'>,
            csv2::first_row_is_header<true>,
            csv2::trim_policy::trim_whitespace> csv;

    if(csv.mmap("demo.csv")){
        const auto header = csv.header();
        for (const auto row: csv) {
            for (const auto cell: row) {
                // Do something with cell value
                std::string value;
                cell.read_value(value);
                cout << value << " ";
            }
            cout << "\n";
        }
    }
}

parse方法中使用了知识点 10

reader.hpp 中还定义了CellRowRowIterator等类。

/images/202410/16/7.png

为了方便在之后的类中使用 RowIterator、Row 和 CellIterator,文件中加了 forward-declaration,如上图所示。

Cell

Cell 类的数据部分定义如下:

/images/202410/16/8.png

其中的buffer_指向 memory-mapped buffer,可参考知识点 4,我们可以简单的将其理解为指向数据内容的一个指针。

主要包括两个方法: read_raw_valueread_value,两个方法稍有区别,前者处理无转义字符,后者处理有转义字符。

Row

Row 类的数据部分定义如下:

/images/202410/16/9.png

和 Cell 类的定义大同小异。Row 类中还定义了另一个类 CellIterator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class CellIterator {
      friend class Row;
      const char *buffer_;
      size_t buffer_size_;
      size_t start_;
      size_t current_;
      size_t end_;

    public:
      CellIterator(const char *buffer, size_t buffer_size, size_t start, size_t end)
          : buffer_(buffer), buffer_size_(buffer_size), start_(start), current_(start_), end_(end) {
      }

      CellIterator &operator++() {
        current_ += 1;
        return *this;
      }

      Cell operator*() {
        bool escaped{false};
        class Cell cell;
        cell.buffer_ = buffer_;
        cell.start_ = current_;
        cell.end_ = end_;

        size_t last_quote_location = 0;
        bool quote_opened = false;
        for (auto i = current_; i < end_; i++) {
          current_ = i;
          if (buffer_[i] == delimiter::value && !quote_opened) {
            // actual delimiter
            // end of cell
            cell.end_ = current_;
            cell.escaped_ = escaped;
            return cell;
          } else {
            if (buffer_[i] == quote_character::value) {
              if (!quote_opened) {
                // first quote for this cell
                quote_opened = true;
                last_quote_location = i;
              } else {
                escaped = (last_quote_location == i - 1);
                last_quote_location += (i - last_quote_location) * size_t(!escaped);
                quote_opened = escaped || (buffer_[i + 1] != delimiter::value);
              }
            }
          }
        }
        cell.end_ = current_ + 1;
        return cell;
      }

      bool operator!=(const CellIterator &rhs) { return current_ != rhs.current_; }
    };

CellIterator 中定义了自增操作符、取值操作符和不等操作符。Iterator 必须实现这三个操作符:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <iostream>

using namespace std;

// forward-declaration to allow use in Iter
class IntVector;

class Iter
{
    public:
    Iter (const IntVector* p_vec, int pos)
        : _pos( pos )
        , _p_vec( p_vec )
    { }

    // these three methods form the basis of an iterator for use with
    // a range-based for loop
    bool
    operator!= (const Iter& other) const
    {
        return _pos != other._pos;
    }

    // this method must be defined after the definition of IntVector
    // since it needs to use it
    int operator* () const;

    const Iter& operator++ ()
    {
        ++_pos;
        // although not strictly necessary for a range-based for loop
        // following the normal convention of returning a value from
        // operator++ is a good idea.
        return *this;
    }

    private:
    int _pos;
    const IntVector *_p_vec;
};

class IntVector
{
    public:
    IntVector ()
    {
    }

    int get (int col) const
    {
        return _data[ col ];
    }
    Iter begin () const
    {
        return Iter( this, 0 );
    }

    Iter end () const
    {
        return Iter( this, 100 );
    }

    void set (int index, int val)
    {
        _data[ index ] = val;
    }

    private:
   int _data[ 100 ];
};

int
Iter::operator* () const
{
     return _p_vec->get( _pos );
}

// sample usage of the range-based for loop on IntVector
int main()
{
    IntVector v;
    for ( int i = 0; i < 100; i++ )
    {
        v.set( i , i );
    }
    for ( int i : v ) { cout << i << endl; }
}

C++ 11 range-based for loops: https://www.cprogramming.com/c++11/c++11-ranged-for-loop.html

还定义了beginend两个方法用于返回RowIterator

RowIterator

RowIterator 的定义和 CellIterator 的定义大致相同。

mio.hpp

mio.hpp 相比与其他三个文件的代码多了不少,也复杂了许多。文件一开始定义了template <access_mode AccessMode, typename ByteT> struct basic_mmap结构体,然后围绕这个结构体声明了一系列操作符:

/images/202410/16/11.png

其定义在行号 1058 处:

/images/202410/16/12.png

之后定义了 5 个工厂方法,方便构建mmapmmap_source以及mmap_sink对象:

/images/202410/16/13.png

然后在 587 处开始定义了字符串相关的工具函数:

/images/202410/16/14.png

在 684 处开始定义了与 Windows 平台相关的open_file_helper函数。

/images/202410/16/15.png

然后定义了template <typename String> file_handle_type open_fileinline size_t query_file_sizeinline mmap_context memory_map函数,以及struct mmap_context结构体。之后,实现了许多在template <access_mode AccessMode, typename ByteT> struct basic_mmap声明的方法。

最后定义了template <access_mode AccessMode, typename ByteT> class basic_shared_mmap类。

writer.hpp

writer.hpp 中包含将数据导出的功能。主要定义了两个方法:write_rowwrite_rows,代表写入一行和写入多行。

例如,将数据写入到文件中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <csv2/reader.hpp>
#include <csv2/parameters.hpp>
#include <csv2/mio.hpp>
#include <csv2/writer.hpp>
#include <iostream>
#include <ostream>
#include <vector>
#include <string>

using namespace csv2;
using namespace std;

int main() {
	std::ofstream out("info.csv");
	csv2::Writer<csv2::delimiter<','>, std::ofstream> writer(out);
	std::vector<std::string> header = { "Name", "Age" };
	std::vector<std::vector<std::string>> content{ {"Andy", "19"}, {"Peter", "21"}, {"Lucas", "20"} };
	writer.write_row(header);
	writer.write_rows(content);
}

/images/202410/16/10.png

parameters.hpp

首先,为了组织代码引入了trim_policy命名空间。 包含了no_trimmingtrim_characters两个结构体,以及using trim_whitespace = trim_characters<' ', '\t'>;一句,于是给空白符' ''\t'新的使用方式——trim_whitespace。需要注意的是,该标识符在trim_policy命名空间中。

/images/202410/16/3.png

此外,还包含delimiterquote_character以及first_row_is_header三个结构体,和之前不同的是它们在csv2命令空间中。

整个文件的结构体里面的方法或数据都是static的,表示我们可以用delimiter<':'>::value的方式直接获取里面的数据,而不用实例化(实例化从逻辑上好像也有一些问题,同样是用:作为分隔符却实例化了两个不同的对象,有点奇怪)。

关于可变参数模板可看知识点 5

pair 的使用: https://cplusplus.com/reference/utility/pair/pair/

知识点

1. CMake 项目添加第三方库

在 CMakeLists.txt 中添加如下语句:

/images/202410/16/1.png

即可将三方库的头文件包含进来。

2. Static Const 使用

mio.hpp 中有如下一段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * Determines the operating system's page allocation granularity.
 *
 * On the first call to this function, it invokes the operating system specific syscall
 * to determine the page size, caches the value, and returns it. Any subsequent call to
 * this function serves the cached value, so no further syscalls are made.
 */
inline size_t page_size() {
  static const size_t page_size = [] {
#ifdef _WIN32
    SYSTEM_INFO SystemInfo;
    GetSystemInfo(&SystemInfo);
    return SystemInfo.dwAllocationGranularity;
#else
    return sysconf(_SC_PAGE_SIZE);
#endif
  }();
  return page_size;
}

page_size()内部的匿名函数只会运行一次,得益于static const声明,这样可以避免重复调用sysconf()函数(如注释所述)。

https://www.tutorialspoint.com/static-const-vs-hashdefine-vs-enum

3. static_assert

mio.hpp 中有static_assert的写法。

/images/202410/16/2.png

static_assert declaration: https://en.cppreference.com/w/cpp/language/static_assert

Understanding static_assert in C++ 11: https://www.geeksforgeeks.org/understanding-static_assert-c-11/

4. mmap

mio.hpp 中的memory_map函数使用了mmap

Use the mmap Function to Write to the Memory in C

Shared Memory: https://kuafu1994.github.io/MoreOnMemory/sharedMemory.html

mapread.c 和 mapwrite.c: https://gist.github.com/marcetcheverry/991042

Memory Mapped I/O: https://www.cs.uleth.ca/~holzmann/C/system/mmap.html

存储映射 I/O

存储映射 I/O(Memory-Mapped I/O)能将一个磁盘文件映射到存储空间的一个缓冲区上,当从缓冲区中取数据时,就相当于读文件中的相应字节。与此类似,将数据存入缓冲区时,相应字节就自动写入文件。如此一来,就可以在不调用 read 和 write 的情况下执行 I/O。——《UNIX 环境高级编程》14.8 节

探索内存原理的内存映射文件: https://zhuanlan.zhihu.com/p/429987335

File Mapping in C++ Applications: https://www.geeksforgeeks.org/file-mapping-in-cpp-applications/

File Mapping: https://learn.microsoft.com/en-us/windows/win32/memory/file-mapping

Mapping files into virtual memory in C on windows: https://stackoverflow.com/questions/68368291/mapping-files-into-virtual-memory-in-c-on-windows

示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <cstdio>
#include <windows.h>
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
	const TCHAR* lpFileName = TEXT("hello.txt");
	HANDLE hFile;
	HANDLE hMap;
	LPVOID lpBasePtr;
	LARGE_INTEGER liFileSize;

	hFile = CreateFile(lpFileName,
		GENERIC_READ,                          // dwDesiredAccess
		0,                                     // dwShareMode
		NULL,                                  // lpSecurityAttributes
		OPEN_EXISTING,                         // dwCreationDisposition
		FILE_ATTRIBUTE_NORMAL,                 // dwFlagsAndAttributes
		0);                                    // hTemplateFile
	if (hFile == INVALID_HANDLE_VALUE) {
		fprintf(stderr, "CreateFile failed with error %d\n", GetLastError());
		return 1;
	}

	if (!GetFileSizeEx(hFile, &liFileSize)) {
		fprintf(stderr, "GetFileSize failed with error %d\n", GetLastError());
		CloseHandle(hFile);
		return 1;
	}

	if (liFileSize.QuadPart == 0) {
		fprintf(stderr, "File is empty\n");
		CloseHandle(hFile);
		return 1;
	}

	hMap = CreateFileMapping(
		hFile,
		NULL,                          // Mapping attributes
		PAGE_READONLY,                 // Protection flags
		0,                             // MaximumSizeHigh
		0,                             // MaximumSizeLow
		NULL);                         // Name
	if (hMap == 0) {
		fprintf(stderr, "CreateFileMapping failed with error %d\n", GetLastError());
		CloseHandle(hFile);
		return 1;
	}

	lpBasePtr = MapViewOfFile(
		hMap,
		FILE_MAP_READ,         // dwDesiredAccess
		0,                     // dwFileOffsetHigh
		0,                     // dwFileOffsetLow
		0);                    // dwNumberOfBytesToMap
	if (lpBasePtr == NULL) {
		fprintf(stderr, "MapViewOfFile failed with error %d\n", GetLastError());
		CloseHandle(hMap);
		CloseHandle(hFile);
		return 1;
	}

	// Display file content as ASCII charaters
	char* ptr = (char*)lpBasePtr;
	LONGLONG i = liFileSize.QuadPart;
	while (i-- > 0) {
		fputc(*ptr++, stdout);
	}

	UnmapViewOfFile(lpBasePtr);
	CloseHandle(hMap);
	CloseHandle(hFile);

	printf("\nDone\n");
}

5. 可变参数模板

在 parameters.hpp 中使用了可变参数模板(Variadic Template Function)。

C++11 – Variadic Template Function | Tutorial & Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

template<typename T>
void logging(T t){
    cout << t;
    cout << "\nLast Call\n";
}

template<typename T, typename ... Args>
void logging(T first, Args... args){
    cout << first << ", ";
    logging(args...);
}

6. #pragma once

What does #pragma once mean in C?

截至到 2023 年为止,主流的编译器都支持#pragma once

7. __has_include()

根据Source file inclusion的描述,__has_include()可以用来检测某个头文件是否存在,但此时并没有将其引入。

8. defined(identifier)

reader.hpp 中有#if defined(identifier)一句。

/images/202410/16/4.png

#if, #elif, #else, and #endif directives

9. 模板默认参数

reader.hpp 有默认模板参数的写法:

/images/202410/16/5.png

在 C++ 17 之前,如果不用任何模板参数且正常使用 Reader 类的话,需要使用如下语法:

1
Reader<> reader;

将 CMakeLists.txt 中的 C++版本由 14

1
set(CMAKE_CXX_STANDARD 14)

改为 17

1
set(CMAKE_CXX_STANDARD 17)

即可用如下轻便的语法使用 Reader。

1
Reader reader;

10. std::forward

https://cplusplus.com/reference/utility/forward/

通过使用std::forward函数可以根据实参调用不同的函数,如下面例子所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <utility>      // std::forward
#include <iostream>     // std::cout

// function with lvalue and rvalue reference overloads:
void overloaded (const int& x) {std::cout << "[lvalue]";}
void overloaded (int&& x) {std::cout << "[rvalue]";}

// function template taking rvalue reference to deduced type:
template <class T> void fn (T&& x) {
  overloaded (x);                   // always an lvalue
  overloaded (std::forward<T>(x));  // rvalue if argument is rvalue
}

int main () {
  int a;

  std::cout << "calling fn with lvalue: ";
  fn (a);
  std::cout << '\n';

  std::cout << "calling fn with rvalue: ";
  fn (0);
  std::cout << '\n';

  return 0;
}

11. std::string

https://cplusplus.com/reference/string/string/

string::erase 可用于清除指定位置的字符。

string::reserve 可用于指定 string 存储空间的大小。

string::push_back 可将字符存入 string 中。

最后

项目中涉及到的存储映射 I/O ,若要想彻底弄清楚机制,可能需要补充一些操作系统方面的知识。

请作者喝杯咖啡!
AndyFree96 支付宝支付宝
AndyFree96 微信微信
0%