8

我正在制作一个处理 txt 文件数据的应用程序。

这个想法是txt文件可能有不同的格式,它应该被读入C++。

一个例子可能是3I2, 3X, I3,应该这样做:“首先我们有 3 个长度为 2 的整数,然后我们有 3 个空点,然后我们有 1 个长度为 3 的整数。

最好迭代文件,产生行,然后将行作为字符串迭代?什么是巧妙地迭代忽略三个被忽略的点的有效方法?

例如

101112---100
102113---101
103114---102

至:

10, 11, 12, 100
10, 21, 13, 101
10, 31, 14, 102
4

6 回答 6

8

Kyle Kanos 提供的链接很好;*scanf/*printf 格式字符串很好地映射到 fortran 格式字符串。使用 C 风格的 IO 实际上更容易做到这一点,但使用 C++ 风格的流也是可行的:

#include <cstdio>
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream fortranfile;
    fortranfile.open("input.txt");

    if (fortranfile.is_open()) {

        std::string line;
        getline(fortranfile, line);

        while (fortranfile.good()) {
            char dummy[4];
            int i1, i2, i3, i4;

            sscanf(line.c_str(), "%2d%2d%2d%3s%3d", &i1, &i2, &i3, dummy, &i4);

            std::cout << "Line: '" << line << "' -> " << i1 << " " << i2 << " "
                      << i3 << " " << i4 << std::endl;

            getline(fortranfile, line);
        }
    }

    fortranfile.close();

    return 0;
}

运行给

$ g++ -o readinput readinput.cc
$ ./readinput
Line: '101112---100' -> 10 11 12 100
Line: '102113---101' -> 10 21 13 101
Line: '103114---102' -> 10 31 14 102

这里我们使用的格式字符串是- (宽度为 2 的十进制整数)的%2d%2d%2d%3s%3d3 个副本,然后是(宽度为 3 的字符串,我们将其读入一个我们从未使用过的变量),然后是(宽度为 3 的十进制整数)。 %2d%3s%3d

于 2013-08-15T18:58:59.310 回答
6

鉴于您希望动态解析 Fortran Format specifier flags,您应该注意:您已经立即进入了解析器领域。

除了其他人在此处提到的解析此类输入的其他方法:

  • 通过使用 Fortran 和 CC/++ 绑定为您进行解析。
  • 通过使用以下组合编写解析器,使用纯 C++ 为您解析它:
    • sscanf
    • streams

我的建议是,如果你可以使用boost,你可以使用它来实现一个简单的解析器,用于运行中的操作,使用正则表达式和 STL 容器的组合。

根据您所描述的内容以及在不同位置显示的内容,您可以使用正则表达式捕获构建您希望支持的语法的简单实现:

(\\d{0,8})([[:alpha:]])(\\d{0,8})
  1. 其中第一组是该变量类型的编号
  2. 第二个是变量的类型
  3. 第三是变量类型的长度

使用Fortran Format Specifier Flags 这个参考,您可以实现一个简单的解决方案,如下所示:

#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/regex.hpp>
#include <boost/tokenizer.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>

//A POD Data Structure used for storing Fortran Format Tokens into their relative forms
typedef struct FortranFormatSpecifier {
    char type;//the type of the variable
    size_t number;//the number of times the variable is repeated
    size_t length;//the length of the variable type
} FFlag;

//This class implements a rudimentary parser to parse Fortran Format
//Specifier Flags using Boost regexes.
class FormatParser {
public:
    //typedefs for further use with the class and class methods
    typedef boost::tokenizer<boost::char_separator<char> > bst_tokenizer;
    typedef std::vector<std::vector<std::string> > vvstr;
    typedef std::vector<std::string> vstr;
    typedef std::vector<std::vector<int> > vvint;
    typedef std::vector<int> vint;

    FormatParser();
    FormatParser(const std::string& fmt, const std::string& fname);

    void parse();
    void printIntData();
    void printCharData();

private:
    bool validateFmtString();
    size_t determineOccurence(const std::string& numStr);
    FFlag setFortranFmtArgs(const boost::smatch& matches);
    void parseAndStore(const std::string& line);
    void storeData();

    std::string mFmtStr;                //this holds the format string
    std::string mFilename;              //the name of the file

    FFlag mFmt;                         //a temporary FFlag variable
    std::vector<FFlag> mFortranVars;    //this holds all the flags and details of them
    std::vector<std::string> mRawData;  //this holds the raw tokens

    //this is where you will hold all the types of data you wish to support
    vvint mIntData;                     //this holds all the int data
    vvstr mCharData;                    //this holds all the character data (stored as strings for convenience)
};

FormatParser::FormatParser() : mFmtStr(), mFilename(), mFmt(), mFortranVars(), mRawData(), mIntData(), mCharData() {}
FormatParser::FormatParser(const std::string& fmt, const std::string& fname) : mFmtStr(fmt), mFilename(fname), mFmt(), mFortranVars(), mRawData(), mIntData(), mCharData() {}

//this function determines the number of times that a variable occurs
//by parsing a numeric string and returning the associated output
//based on the grammar
size_t FormatParser::determineOccurence(const std::string& numStr) {
    size_t num = 0;
    //this case means that no number was supplied in front of the type
    if (numStr.empty()) {
        num = 1;//hence, the default is 1
    }
    else {
        //attempt to parse the numeric string and find it's equivalent
        //integer value (since all occurences are whole numbers)
        size_t n = atoi(numStr.c_str());

        //this case covers if the numeric string is expicitly 0
        //hence, logically, it doesn't occur, set the value accordingly
        if (n == 0) {
            num = 0;
        }
        else {
            //set the value to its converted representation
            num = n;
        }
    }
    return num;
}

//from the boost::smatches, determine the set flags, store them
//and return it
FFlag FormatParser::setFortranFmtArgs(const boost::smatch& matches) {
    FFlag ffs = {0};

    std::string fmt_number, fmt_type, fmt_length;

    fmt_number = matches[1];
    fmt_type = matches[2];
    fmt_length = matches[3];

    ffs.type = fmt_type.c_str()[0];

    ffs.number = determineOccurence(fmt_number);
    ffs.length = determineOccurence(fmt_length);

    return ffs;
}

//since the format string is CSV, split the string into tokens
//and then, validate the tokens by attempting to match them
//to the grammar (implemented as a simple regex). If the number of
//validations match, everything went well: return true. Otherwise:
//return false.
bool FormatParser::validateFmtString() {    
    boost::char_separator<char> sep(",");
    bst_tokenizer tokens(mFmtStr, sep);
    mFmt = FFlag();

    size_t n_tokens = 0;
    std::string token;

    for(bst_tokenizer::const_iterator it = tokens.begin(); it != tokens.end(); ++it) {
        token = *it;
        boost::trim(token);

        //this "grammar" is based on the Fortran Format Flag Specification
        std::string rgx = "(\\d{0,8})([[:alpha:]])(\\d{0,8})";
        boost::regex re(rgx);
        boost::smatch matches;

        if (boost::regex_match(token, matches, re, boost::match_extra)) {
            mFmt = setFortranFmtArgs(matches);
            mFortranVars.push_back(mFmt);
        }
        ++n_tokens;
    }

    return mFortranVars.size() != n_tokens ? false : true;
}

//Now, parse each input line from a file and try to parse and store
//those variables into their associated containers.
void FormatParser::parseAndStore(const std::string& line) {
    int offset = 0;
    int integer = 0;
    std::string varData;
    std::vector<int> intData;
    std::vector<std::string> charData;

    offset = 0;

    for (std::vector<FFlag>::const_iterator begin = mFortranVars.begin(); begin != mFortranVars.end(); ++begin) {
        mFmt = *begin;

        for (size_t i = 0; i < mFmt.number; offset += mFmt.length, ++i) {
            varData = line.substr(offset, mFmt.length);

            //now store the data, based on type:
            switch(mFmt.type) {
                case 'X':
                  break;

                case 'A':
                  charData.push_back(varData);
                  break;

                case 'I':
                  integer = atoi(varData.c_str());
                  intData.push_back(integer);
                  break;

                default:
                  std::cerr << "Invalid type!\n";
            }
        }
    }
    mIntData.push_back(intData);
    mCharData.push_back(charData);
}

//Open the input file, and attempt to parse the input file line-by-line.
void FormatParser::storeData() {
    mFmt = FFlag();
    std::ifstream ifile(mFilename.c_str(), std::ios::in);
    std::string line;

    if (ifile.is_open()) {
        while(std::getline(ifile, line)) {
            parseAndStore(line);
        }
    }
    else {
        std::cerr << "Error opening input file!\n";
        exit(3);
    }
}

//If character flags are set, this function will print the character data
//found, line-by-line
void FormatParser::printCharData() {    
    vvstr::const_iterator it = mCharData.begin();
    vstr::const_iterator jt;
    size_t linenum = 1;

    std::cout << "\nCHARACTER DATA:\n";

    for (; it != mCharData.end(); ++it) {
        std::cout << "LINE " << linenum << " : ";
        for (jt = it->begin(); jt != it->end(); ++jt) {
            std::cout << *jt << " ";
        }
        ++linenum;
        std::cout << "\n";
    }
}

//If integer flags are set, this function will print all the integer data
//found, line-by-line
void FormatParser::printIntData() {
    vvint::const_iterator it = mIntData.begin();
    vint::const_iterator jt;
    size_t linenum = 1;

    std::cout << "\nINT DATA:\n";

    for (; it != mIntData.end(); ++it) {
        std::cout << "LINE " << linenum << " : ";
        for (jt = it->begin(); jt != it->end(); ++jt) {
            std::cout << *jt << " ";
        }
        ++linenum;
        std::cout << "\n";
    }
}

//Attempt to parse the input file, by first validating the format string
//and then, storing the data accordingly
void FormatParser::parse() {
    if (!validateFmtString()) {
        std::cerr << "Error parsing the input format string!\n";
        exit(2);
    }
    else {
        storeData();
    }
}

int main(int argc, char **argv) {
    if (argc < 3 || argc > 3) {
        std::cerr << "Usage: " << argv[0] << "\t<Fortran Format Specifier(s)>\t<Filename>\n";
        exit(1);
    }
    else {
        //parse and print stuff here
        FormatParser parser(argv[1], argv[2]);
        parser.parse();

        //print the data parsed (if any)
        parser.printIntData();
        parser.printCharData();
    }
    return 0;
}

这是标准的 c++98 代码,可以编译如下:

g++ -Wall -std=c++98 -pedantic fortran_format_parser.cpp -lboost_regex

奖金

这个基本的解析器也可以工作Characters(Fortran 格式标志“A”,最多 8 个字符)。您可以通过编辑正则表达式并对捕获的字符串的长度与类型进行检查来扩展它以支持您可能喜欢的任何标志。

可能的改进

如果您可以使用 C++11,您可以lambdas在某些地方使用并替换auto迭代器。

vectors如果这是在有限的内存空间中运行,并且您必须解析一个大文件,那么由于内部管理内存的方式,vector 将不可避免地崩溃。改为使用会更好deques。有关更多信息,请参见此处讨论的内容:

http://www.gotw.ca/gotw/054.htm

而且,如果输入文件很大,并且文件 I/O 是瓶颈,您可以通过修改ifstream缓冲区的大小来提高性能:

如何让 IOStream 表现更好?

讨论

您会注意到:您正在解析的类型必须在运行时已知,并且类声明和定义中必须支持任何关联的存储容器。

正如您想象的那样,在一个主类中支持所有类型并不高效。但是,由于这是一个幼稚的解决方案,因此可以专门改进一个完整的解决方案来支持这些情况。

另一个建议是使用Boost::Spirit。但是,由于 Spirit 使用了很多模板,当错误可能而且确实发生时,调试这样的应用程序不适合胆小的人。

表现

@Jonathan Dursi 的解决方案相比,这个解决方案很慢

对于使用相同行格式(“3I2,3X,I3”)的 10,000,000 行随机生成的输出(124MiB 文件):

#include <fstream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main(int argc, char **argv) {
    srand(time(NULL));
    if (argc < 2 || argc > 2) {
        printf("Invalid usage! Use as follows:\t<Program>\t<Output Filename>\n");
        exit(1);
    }

    ofstream ofile(argv[1], ios::out);
    if (ofile.is_open()) {
        for (int i = 0; i < 10000000; ++i) {
             ofile << (rand() % (99-10+1) + 10) << (rand() % (99-10+1) + 10) << (rand() % (99-10+1)+10) << "---" << (rand() % (999-100+1) + 100) << endl;
        }
    }

    ofile.close();
    return 0;
}

我的解决方案:

0m13.082s
0m13.107s
0m12.793s
0m12.851s
0m12.801s
0m12.968s
0m12.952s
0m12.886s
0m13.138s
0m12.882s

时钟的平均挂钟时间12.946s

乔纳森·杜尔西的解决方案:

0m4.698s
0m4.650s
0m4.690s
0m4.675s
0m4.682s
0m4.681s
0m4.698s
0m4.675s
0m4.695s
0m4.696s

Blazes 的平均墙上时间为4.684s

在 O2 上,他的速度至少比我的快 270% 。

但是,由于您不必每次要解析附加格式标志时都实际修改源代码,因此该解决方案更为优化。

注意: 您可以实现一个涉及sscanf/的解决方案,streams它只需要您知道您希望读取的变量类型(很像我的),但是额外的检查(例如验证类型)会增加开发时间。(这就是为什么我在 Boost 中提供我的解决方案,因为标记器和正则表达式的便利性——这使得开发过程更容易)。

参考

http://www.boost.org/doc/libs/1_34_1/libs/regex/doc/character_class_names.html

于 2013-08-19T03:25:56.373 回答
5

鉴于 Fortran 很容易从 C 调用,您可以编写一个小的 Fortran 函数来“本机”执行此操作。毕竟,Fortran READ 函数采用您所描述的格式字符串。

如果你想让它工作,你需要稍微复习一下 Fortran ( http://docs.oracle.com/cd/E19957-01/806-3593/2_io.html ),并学习如何使用您的编译器链接 Fortran 和 C++。这里有一些提示:

  • Fortran 符号可能带有下划线后缀,因此 MYFUNC 可以从 C 中调用为 myfunc_()。
  • 多维数组具有相反的维度顺序。
  • 在 C++ 标头中声明 Fortran(或 C)函数需要将其放置在extern "C" {}作用域中。
于 2013-08-17T05:12:21.197 回答
5

你可以翻译3I2, 3X, I3成 scanf 格式。

于 2013-07-23T08:57:34.683 回答
3

如果您的用户实际上应该以 Fortran 格式输入它,或者如果您很快适应或编写 Fortran 代码来执行此操作,我会按照 John Zwinck 和 MSB 的建议进行操作。只需编写一个简短的 Fortran 例程将数据读入数组,然后使用“bind(c)”和 ISO_C_BINDING 类型来设置接口。请记住,数组索引将在 Fortran 和 C++ 之间发生变化。

否则,我建议使用 scanf,如上所述:

http://en.cppreference.com/w/cpp/io/c/fscanf

如果您不知道每行需要阅读的项目数,则可以使用 vscanf 代替:

http://en.cppreference.com/w/cpp/io/c/vfscanf

不过,虽然看起来很方便,但是我从来没有用过这个,所以YMMV。

于 2013-08-18T08:50:52.850 回答
2

今天想了一些,但没有时间写一个例子。@jrd1 的示例和分析正在进行中,但我会尝试使解析更加模块化和面向对象。格式字符串解析器可以构建一个项目解析器列表,这些解析器或多或少独立工作,允许添加新的解析器,如浮点数,而无需更改旧代码。我认为一个特别好的界面将是一个用格式字符串初始化的 iomanip,这样 ui 就像

cin >> f77format("3I2, 3X, I3") >> a >> b >> c >> d;

在实现时,我将让 f77format 解析位并按组件构建解析器,因此它将创建 3 个固定宽度的 int 解析器、一个 devNull 解析器和另一个将使用输入的固定宽度解析器。

当然,如果你想支持所有的编辑描述符,那将是一项艰巨的工作!一般来说,它不会只是将字符串的其余部分传递给下一个解析器,因为有些编辑描述符需要重新读取该行。

于 2013-08-22T05:24:00.470 回答