簡體   English   中英

解析 CSV 文件 - C++

[英]Parsing a CSV file - C++

C++14

一般來說,大學的工作人員建議我們使用Boost來解析文件,但我已經安裝了它並沒有成功實現任何東西。

所以我必須逐行解析一個 CSV 文件,其中每行有 2 列,當然用逗號分隔。 這兩列中的每一列都是一個數字。 我必須取這兩個數字的整數值,並在最后使用它們來構造我的 Fractal 對象。

第一個問題是:文件看起來像這樣:

1,1
<HERE WE HAVE A NEWLINE>
<HERE WE HAVE A NEWLINE>

這種格式的文件是可以的。 但是我的解決方案為那個輸出“無效輸入”,其中正確的解決方案應該只打印一次相應的分形 - 1,1。

第二個問題是:文件看起來像:

1,1
<HERE WE HAVE A NEWLINE>
1,1

這應該是一個無效的輸入,但我的解決方案將其視為正確的輸入 - 只是跳過中間的 NEWLINE。

也許你可以指導我如何解決這些問題,這真的對我有幫助,因為我從早到晚都在努力練習這個 3 天。

這是我當前的解析器:

#include <iostream>
#include "Fractal.h"
#include <fstream>
#include <stack>
#include <sstream>
const char *usgErr = "Usage: FractalDrawer <file path>\n";
const char *invalidErr = "Invalid input\n";
const char *VALIDEXT = "csv";
const char EXTDOT = '.';
const char COMMA = ',';
const char MINTYPE = 1;
const char MAXTYPE = 3;
const int MINDIM = 1;
const int MAXDIM = 6;
const int NUBEROFARGS = 2;
int main(int argc, char *argv[])
{
    if (argc != NUBEROFARGS)
    {
        std::cerr << usgErr;
        std::exit(EXIT_FAILURE);
    }
    std::stack<Fractal *> resToPrint;
    std::string filepath = argv[1]; // Can be a relative/absolute path
    if (filepath.substr(filepath.find_last_of(EXTDOT) + 1) != VALIDEXT)
    {
        std::cerr << invalidErr;
        exit(EXIT_FAILURE);
    }
    std::stringstream ss; // Treat it as a buffer to parse each line
    std::string s; // Use it with 'ss' to convert char digit to int
    std::ifstream myFile; // Declare on a pointer to file
    myFile.open(filepath); // Open CSV file
    if (!myFile) // If failed to open the file
    {
        std::cerr << invalidErr;
        exit(EXIT_FAILURE);
    }
    int type = 0;
    int dim = 0;
    while (myFile.peek() != EOF)
    {
        getline(myFile, s, COMMA); // Read to comma - the kind of fractal, store it in s
        ss << s << WHITESPACE; // Save the number in ss delimited by ' ' to be able to perform the double assignment
        s.clear(); // We don't want to save this number in s anymore as we won't it to be assigned somewhere else
        getline(myFile, s, NEWLINE); // Read to NEWLINE - the dim of the fractal
        ss << s;
        ss >> type >> dim; // Double assignment
        s.clear(); // We don't want to save this number in s anymore as we won't it to be assigned somewhere else

        if (ss.peek() != EOF || type < MINTYPE || type > MAXTYPE || dim < MINDIM || dim > MAXDIM) 
        {
            std::cerr << invalidErr;
            std::exit(EXIT_FAILURE);
        }

        resToPrint.push(FractalFactory::factoryMethod(type, dim));
        ss.clear(); // Clear the buffer to update new values of the next line at the next iteration
    }

    while (!resToPrint.empty())
    {
        std::cout << *(resToPrint.top()) << std::endl;
        resToPrint.pop();
    }

    myFile.close();

    return 0;
}

你不需要任何特殊的東西來解析.csv文件,來自 C++11 的 STL 容器提供了解析幾乎任何.csv文件所需的所有工具。 您不需要事先知道要解析的每行值的數量,但您需要知道從.csv中讀取的值的類型,以便應用正確的值轉換。 您也不需要任何像 Boost 這樣的第三方庫。

有多種方法可以存儲從.csv文件解析的值。 基本的“處理任何類型”方法是將值存儲在std::vector<std::vector<type>> (它本質上提供了一個向量的向量,其中包含從每一行解析的值)。 您可以根據需要根據您正在閱讀的類型以及您需要如何轉換和存儲值來專門化存儲。 您的基本存儲可以是struct/classstd::pairstd::set ,或者只是像int這樣的基本類型。 任何適合您的數據。

在您的情況下,您的文件中有基本的int值。 對基本.csv解析的唯一警告是您可能在值行之間有空行。 這可以通過任意數量的測試輕松處理。 例如,您可以檢查讀取的行的.length()是否為零,或者為了獲得更大的靈活性(處理包含多個空格或其他非值字符的行),您可以使用.find_first_of()來查找行中的第一個想要的值,以確定它是否是要解析的行。

例如,在您的情況下,您的值行的讀取循環可以簡單地讀取每一行並檢查該行是否包含digit 它可以很簡單:

    ...
    std::string line;       /* string to hold each line read from file  */
    std::vector<std::vector<int>> values {};    /* vector vector of int */
    std::ifstream f (argv[1]);                  /* file stream to read  */

    while (getline (f, line)) { /* read each line into line */
        /* if no digits in line - get next */
        if (line.find_first_of("0123456789") == std::string::npos)
            continue;
        ...
    }

上面,每一行被讀入line ,然后檢查line是否包含數字。 如果是這樣,解析它。 如果沒有,請轉到下一行並重試。

如果它是包含值的行,那么您可以從該行創建一個std::stringstream並將整數值從 stringstream 讀取到臨時int值並將該值添加到int的臨時向量中,使用getline和 the分隔符',' ,並且當您用完要從該行讀取的值時,將int的臨時向量添加到您的最終存儲中。 (重復直到讀完所有行)。

您的完整讀取循環可能是:

    while (getline (f, line)) { /* read each line into line */
        /* if no digits in line - get next */
        if (line.find_first_of("0123456789") == std::string::npos)
            continue;
        int itmp;                               /* temporary int */
        std::vector<int> tmp;                   /* temporary vector<int> */
        std::stringstream ss (line);            /* stringstream from line */
        while (ss >> itmp) {                    /* read int from stringstream */
            std::string tmpstr;                 /* temporary string to ',' */
            tmp.push_back(itmp);                /* add int to tmp */
            if (!getline (ss, tmpstr, ','))     /* read to ',' w/tmpstr */
                break;                          /* done if no more ',' */
        } 
        values.push_back (tmp);     /* add tmp vector to values */
    }

每行讀取的值數或每個文件讀取的值的行數沒有限制(最多可用於存儲的虛擬內存限制)

將上述內容放在一個簡短的示例中,您可以執行類似於以下操作的操作,該操作僅讀取您的輸入文件,然后在完成后輸出收集的整數:

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>

int main (int argc, char **argv) {

    if (argc < 2) { /* validate at least 1 argument given for filename */
        std::cerr << "error: insufficient input.\nusage: ./prog <filename>\n";
        return 1;
    }

    std::string line;       /* string to hold each line read from file  */
    std::vector<std::vector<int>> values {};    /* vector vector of int */
    std::ifstream f (argv[1]);                  /* file stream to read  */

    while (getline (f, line)) { /* read each line into line */
        /* if no digits in line - get next */
        if (line.find_first_of("0123456789") == std::string::npos)
            continue;
        int itmp;                               /* temporary int */
        std::vector<int> tmp;                   /* temporary vector<int> */
        std::stringstream ss (line);            /* stringstream from line */
        while (ss >> itmp) {                    /* read int from stringstream */
            std::string tmpstr;                 /* temporary string to ',' */
            tmp.push_back(itmp);                /* add int to tmp */
            if (!getline (ss, tmpstr, ','))     /* read to ',' w/tmpstr */
                break;                          /* done if no more ',' */
        } 
        values.push_back (tmp);     /* add tmp vector to values */
    }

    for (auto row : values) {       /* output collected values */
        for (auto col : row)
            std::cout << "  " << col;
        std::cout << '\n';
    }
}

示例輸入文件

在包含您在問題中描述的值的行上使用帶有雜項空行和每行兩個整數的輸入文件:

$ cat dat/csvspaces.csv
1,1


2,2
3,3

4,4



5,5
6,6

7,7

8,8


9,9

示例使用/輸出

結果解析:

$ ./bin/parsecsv dat/csvspaces.csv
  1  1
  2  2
  3  3
  4  4
  5  5
  6  6
  7  7
  8  8
  9  9

示例輸入未知/不均勻的列數

您不需要知道.csv每行的值數或文件中值的行數。 STL 容器自動處理內存分配需求,允許您解析任何您需要的內容。 現在,可能希望每行或每個文件的行數強制執行一些固定數量的值,但這完全取決於您向讀取/解析例程添加簡單的計數器和檢查以限制根據需要存儲的值。

不對上面的代碼做任何更改,它將每行處理任意數量的逗號分隔值。 例如,將您的數據文件更改為:

$ cat dat/csvspaces2.csv
1


2,2
3,3,3

4,4,4,4



5,5,5,5,5
6,6,6,6,6,6

7,7,7,7,7,7,7

8,8,8,8,8,8,8,8


9,9,9,9,9,9,9,9,9

示例使用/輸出

結果在每一行的每個值的預期解析中,例如:

$ ./bin/parsecsv dat/csvspaces2.csv
  1
  2  2
  3  3  3
  4  4  4  4
  5  5  5  5  5
  6  6  6  6  6  6
  7  7  7  7  7  7  7
  8  8  8  8  8  8  8  8
  9  9  9  9  9  9  9  9  9

如果您有我沒有涵蓋的問題,或者您對我所做的事情有其他問題,請告訴我,我很樂意為您提供進一步的幫助。

我不會更新你的代碼。 我看了你的標題Parsing a CSV file - C++ ,想向你展示如何以更現代的方式讀取 csv 文件。 不幸的是,您仍在使用 C++14。 使用 C++20 或getlines庫,使用getlinessplit會非常簡單。

在 C++17 中,我們可以使用 CTAD 和if與初始化器等。

但是我們不需要的是boost。 C++ 的標准庫就足夠了。 我們從不使用scanf和類似的舊東西。

我的拙見,指向 10 歲問題的鏈接如何在 C++ 中讀取和解析 CSV 文件? 不應再給予。 現在是 2020 年。 應該使用更現代和現在可用的語言元素。 但正如所說。 每個人都可以自由地做他想做的事。

在 C++ 中,我們可以使用std::sregex_token_iterator 它的使用非常簡單。 它也不會顯着減慢您的程序。 std::getline也可以。 雖然它不是那么靈活。 為此必須知道列數。 std::sregex_token_iterator不關心列數。

請參閱以下示例代碼。 在那,我們創建了一個 tine 代理類並覆蓋它的提取器操作符。 然后我們使用std::istream_iterator並在一個小的單行中讀取和解析整個 csv 文件。

#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <regex>
#include <string>
#include <vector>

// Define Alias for easier Reading
// using Columns = std::vector<std::string>;
using Columns = std::vector<int>;

// The delimiter
const std::regex re(",");

// Proxy for the input Iterator
struct ColumnProxy {
    // Overload extractor. Read a complete line
    friend std::istream& operator>>(std::istream& is, ColumnProxy& cp) {
        // Read a line
        std::string line;
        cp.columns.clear();
        if(std::getline(is, line) && !line.empty()) {
            // Split values and copy into resulting vector
            std::transform(
                std::sregex_token_iterator(line.begin(), line.end(), re, -1), {},
                std::back_inserter(cp.columns),
                [](const std::string& s) { return std::stoi(s); });
        }
        return is;
    }
    // Type cast operator overload.  Cast the type 'Columns' to
    // std::vector<std::string>
    operator Columns() const { return columns; }

protected:
    // Temporary to hold the read vector
    Columns columns{};
};

int main() {
    std::ifstream myFile("r:\\log.txt");
    if(myFile) {
        // Read the complete file and parse verything and store result into vector
        std::vector<Columns> values(std::istream_iterator<ColumnProxy>(myFile), {});

        // Show complete csv data
        std::for_each(values.begin(), values.end(), [](const Columns& c) {
            std::copy(c.begin(), c.end(),
                      std::ostream_iterator<int>(std::cout, " "));
            std::cout << "\n";
        });
    }
    return 0;
}

請注意:還有很多其他可能的解決方案。 請隨意使用任何你想要的。


編輯

因為我在這里看到了很多復雜的代碼,所以我想展示第二個示例來說明如何

解析 CSV 文件 - C++

基本上,代碼中不需要超過 2 個語句。 您首先為數字定義一個正則表達式。 然后您使用 C++ 語言元素,該元素專為將字符串標記為子字符串而設計。 std::sregex_token_iterator 而且由於這種最合適的語言元素多年來在 C++ 中可用,因此可能值得考慮使用它。 也許您基本上可以在 2 行中完成任務,而不是 10 行或更多行。 而且很容易理解。

但當然,有成千上萬種可能的解決方案,有些喜歡繼續使用 C 風格,有些喜歡更現代的 C++ 特性。 這取決於每個人的個人決定。

下面的代碼按指定讀取 csv 文件,無論它包含多少行(行)以及每行有多少列。 甚至 foreing 字符也可以在其中。 空行將是 csv 向量中的空條目。 這也可以很容易地防止,在 emplace 返回之前使用“if !empty”。

但有些人喜歡,有些人喜歡。 人們想要什么。

請參閱一般示例:

#include <algorithm>
#include <iterator>
#include <iostream>
#include <regex>
#include <sstream>
#include <string>
#include <vector>

// Test data. Can of course also be taken from a file stream.
std::stringstream testFile{ R"(1,2
3, a, 4 
5 , 6  b ,  7

abc def
8 , 9
11 12 13 14 15 16 17)" };

std::regex digits{R"((\d+))"};

using Row = std::vector<std::string>;

int main() {
    // Here we will store all the data from the CSV as std::vector<std::vector<std::string>>
    std::vector<Row> csv{};


    // This extremely simple 2 lines will read the complete CSV and parse the data
    for (std::string line{}; std::getline(testFile, line);  ) 
        csv.emplace_back(Row(std::sregex_token_iterator(line.begin(), line.end(), digits, 1), {}));


    // Now, you can do with the data, whatever you want. For example: Print double the value
    std::for_each(csv.begin(), csv.end(), [](const Row& r) { 
        if (!r.empty()) {
            std::transform(r.begin(), r.end(), std::ostream_iterator<int>(std::cout, " "), [](const std::string& s) {
            return std::stoi(s) * 2; }
        ); std::cout << "\n";}});

    return 0;
}

所以,現在,你可能會明白,你可能喜歡它,或者你不喜歡它。 任何。 隨意做任何你想做的事。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM