【问题标题】:Parsing text file lines in C++Parsing text file lines in C++
【发布时间】:2022-12-28 14:56:16
【问题描述】:

我有一个 txt 文件,其中包含如下数据:

regNumber     FName         Score1   Score2   Score3
385234     John Snow         90.0     56.0     60.8
38345234   Michael Bolton    30.0     26.5     
38500234   Tim Cook          40.0     56.5     20.2
1547234    Admin__One        10.0         
                      ...

数据仅由空格分隔。

现在,我的问题是由于缺少某些数据,我不能简单地执行以下操作:

ifstream file;
file.open("file.txt")

file >> regNo >> fName >> lName >> score1 >> score2 >> score3

(我不确定上面的代码是否正确,但试图解释这个想法)

我想做的大致是这样的:

cout << "Reg Number: ";
cin >> regNo;

cout << "Name: ";
cin >> name;

if(regNo == regNumber && name == fname) {
  cout << "Access granted" << endl;
}

这是我尝试过的/我所在的位置:

ifstream file;
file.open("users.txt");
string line;

    
while(getline(file, line)) {
   stringstream ss(line);
   string word;
   while(ss >> word) {
      cout << word << "\t";
   }
   cout << " " << endl;
}

我可以完全输出文件,我的问题是在挑选零件时,例如仅获取 regNumber 或名称。

【问题讨论】:

  • 哪些字段可以缺失? regNumber 可以丢失吗?可以输入用户名吗?两者都可以吗?如果您可以保证某些特定字段始终可用,那么解决方案就会得到简化。
  • 只能缺少分数,不能缺少 regNumber 和名称

标签: c++ parsing


【解决方案1】:

I would read the whole line in at once and then just substring it (since you suggest that these are fixed width fields)

    【解决方案2】:

    Handling the spaces between the words of the names are tricky, but its apparent from your file that each column starts at a fixed offset. You can use this to extract the information you want. For example, in order to read the names, you can read the line starting at the offset that FName starts, and ending at the offset that Score1 starts. Then you can remove trailing white spaces from the string like this:

    string A = "Tim Cook       ";
    auto index = A.find_last_not_of(' ');
    A.erase(index + 1);
    
    • It isn’t apparent to me, and I wouldn’t trust a data file to maintain fixed-size fields unless that is explicitly in the file’s spec.
    • Of course the file provider should provide you with the data structure, but the question hasn't provided one. If the file doesn't have fixed-size fields then it would be hard to read it programmatically if column values would be missing from the middle.
    【解决方案3】:

    Alright, I can’t sleep and so decided to go bonkers and demonstrate just how tricky input is, especially when you have freeform data. The following code contains plenty of commentary on reading freeform data that may be missing.

    #include <ciso646>
    #include <deque>
    #include <iomanip>
    #include <iostream>
    #include <iterator>
    #include <optional>
    #include <sstream>
    #include <string>
    #include <type_traits>
    #include <vector>
    
    
    // Useful Stuff
    
    template <typename T> T& lvalue( T&& arg ) { return arg; }
    
    using strings = std::deque <std::string> ;
    auto split( const std::string& s )
    {
      return strings
      (
        std::istream_iterator <std::string> ( lvalue( std::istringstream{ s } ) ),
        std::istream_iterator <std::string> ()
      );
    }
    
    template <typename T>
    auto string_to( const std::string & s )
    {
      T value;
      std::istringstream ss( s );
      return ((ss >> value) and (ss >> std::ws).eof())
        ? value
        : std::optional<T> { };
    }
    
    std::string trim( const std::string& s )
    {
      auto R = s.find_last_not_of ( " \f\n\r\t\v" ) + 1;
      auto L = s.find_first_not_of( " \f\n\r\t\v" );
      return s.substr( L, R-L );
    }
    
    
    // Each record is stored as a “User”.
    // “Users” is a complete dataset of records.
    
    struct User
    {
      int                       regNumber;
      std::vector <std::string> names;
      std::vector <double>      scores;
    };
    using Users = std::vector <User> ;
    
    
    // This is stuff you would put in the .cpp file, not an .hpp file.
    // But since this is a single-file example, it goes here.
    namespace detail::Users
    {
      static const char * FILE_HEADER = "regNumber     FName         Score1   Score2   Score3\n";
      static const int    REGNUMBER_WIDTH   = 11;
      static const int    NAMES_TOTAL_WIDTH = 18;
      static const int    SCORE_WIDTH       =  9;
      static const int    SCORE_PRECISION   =  1;
    }
    
    
    // Input is always the hardest part, and provides a WHOLE lot of caveats to deal with.
    // Let us take them one at a time.
    //
    // Each user is a record composed of ONE OR MORE elements on a line of text.
    // The elements are:
    //   (regNumber)? (name)* (score)*
    //
    // The way we handle this is:
    //  (1) Read the entire line
    //  (2) Split the line into substrings
    //  (3) If the first element is a regNumber, grab it
    //  (4) Grab any trailing floating point values as scores
    //  (5) Anything remaining must be names
    //
    // There are comments in the code below which enable you to produce a hard failure
    // if any record is incorrect, however you define that. A “hard fail” sets the fail
    // state on the input stream, which will stop all further input on the stream until
    // the caller uses the .clear() method on the stream.
    //
    // The default action is to stop reading records if a failure occurs. This way the
    // CALLER can decide whether to clear the error and try to read more records.
    //
    // Finally, we use decltype liberally to make it easier to modify the User struct
    // without having to watch out for type problems with the stream extraction operator.
    
    // Input a single record
    
    std::istream& operator >> ( std::istream& ins, User& user )
    {
      // // Hard fail helper (named lambda)
      // auto failure = [&ins]() -> std::istream&
      // {
      //   ins.setstate( std::ios::failbit );
      //   return ins;
      // };
    
      // You should generally clear your target object when writing stream extraction operators
      user = User{};
    
      // Get a single record (line) from file
      std::string s;
      if (!getline( ins, s )) return ins;
    
      // Split the record into fields
      auto fields = split( s );
    
      // Skip (blank lines) and (file headers)
      static const strings header = split( detail::Users::FILE_HEADER );
      if (fields.empty() or fields == header) return operator >> ( ins, user );
    
      // The optional regNumber must appear first
      auto reg_number = string_to <decltype(user.regNumber)> ( fields.front() );
      if (reg_number)
      {
        user.regNumber = *reg_number;
        fields.pop_front();
      }
    
      // Optional scores must appear last
      while (!fields.empty())
      {
        auto score = string_to <std::remove_reference <decltype(user.scores.front())> ::type> ( fields.back() );
        if (!score) break;
        user.scores.insert( user.scores.begin(), *score );
        fields.pop_back();
      }
      // if (user.scores.size() > 3) return failure();  // is there a maximum number of scores?
    
      // Any remaining fields are names.
      // if (fields.empty())    return failure();  // at least one name required?
      // if (fields.size() > 2) return failure();  // maximum of two names?
      for (const auto& name : fields)
      {
        // (You could also check that each name matches a valid regex pattern, etc)
        user.names.push_back( name );
      }
    
      // If we got this far, all is good. Return the input stream.
      return ins;
    }
    
    // Input a complete User dataset
    
    std::istream& operator >> ( std::istream& ins, Users& users )
    {
      // This time, do NOT clear the target object! This permits the caller to read
      // multiple files and combine them! The caller is also now responsible to
      // provide a new/empty/clear target Users object to avoid combining datasets.
    
      // Read all records
      User user;
      while (ins >> user) users.push_back( user );
    
      // Return the input stream
      return ins;
    }
    
    
    // Output, by comparison, is fabulously easy.
    //
    // I won’t bother to explain any of this, except to recall that
    // the User is stored as a line-object record -- that is, it must
    // be terminated by a newline. Hence we output the newline in the
    // single User stream insertion operator (output operator) instead
    // of the Users output operator.
    
    // Output a single User record
    
    std::ostream& operator << ( std::ostream& outs, const User& user )
    {
      std::ostringstream userstring;
      userstring << std::setw( detail::Users::REGNUMBER_WIDTH ) << std::left << user.regNumber;
    
      std::ostringstream names;
      for (const auto& name : user.names) names << name << " ";
      userstring << std::setw( detail::Users::NAMES_TOTAL_WIDTH ) << std::left << names.str();
    
      for (auto score : user.scores)
        userstring 
          << std::left << std::setw( detail::Users::SCORE_WIDTH ) 
          << std::fixed << std::setprecision( detail::Users::SCORE_PRECISION ) 
          << score;
    
      return outs << trim( userstring.str() ) << "\n";  // <-- output of newline
    }
    
    // Output a complete User dataset
    
    std::ostream& operator << ( std::ostream& outs, const Users& users )
    {
      outs << detail::Users::FILE_HEADER;
      for (const auto& user : users) outs << user;
      return outs;
    }
    
    
    int main()
    {
      // Example input. Notice that any field may be absent.
      std::istringstream input(
        "regNumber     FName         Score1   Score2   Score3 \n"
        "385234     John Snow         90.0     56.0     60.8  \n"
        "38345234   Michael Bolton    30.0     26.5           \n"
        "38500234   Tim Cook          40.0     56.5     20.2  \n"
        "1547234    Admin__One        10.0                    \n"
        "                                                     \n" // blank line --> skipped
        "           Jon Bon Jovi                              \n"
        "11111                        22.2                    \n"
        "                             33.3                    \n"
        "4444                                                 \n"
        "55         Justin Johnson                            \n"
      );
      Users        users;
      input     >> users;
      std::cout << users;
    }
    

    To compile with MSVC:

    cl /EHsc /W4 /Ox /std:c++17 a.cpp
    

    To compile with Clang:

    clang++ -Wall -Wextra -pedantic-errors -O3 -std=c++17 a.cpp
    

    To compile with MinGW/GCC/etc use the same as Clang, substituting g++ for clang++, naturally.

    As a final note, if you can make your data file much more strict life will be significantly easier. For example, if you can say that you are always going to used fixed-width fields you can use Shahriar’s answer, for example, or pm100’s answer, which I upvoted.

      【解决方案4】:

      I would define a Person class.
      This knows how to read and write a Person on one line.

      class Person
      {
          int                 regNumber;
          std::string         FName;
          std::array<float,3> scope;
      
          friend std::ostream& operator<<(std::ostream& s, Person const& p)
          {
              return p << regNumber << " " << FName << " " << scope[0] << " " << scope[1] << " " << scope[2] << "\n";
          }
          friend std::istream& operator>>(std::istream& s, Person& p)
          {
              std::string line;
              std::getline(s, line);
      
              bool   valid = true;
              Person tmp;     // Holds value while we check
              // Handle Line.
              // Handle missing data.
              // And update tmp to the correct state.
      
              if (valid) {
                  // The conversion worked.
                  // So update the object we are reading into.
                  swap(p, tmp);
              }
              else {
                  // The conversion failed.
                  // Set the stream to bad so we stop reading.
                  s.setstate(std::ios::bad);
              }
              return s;
          }
          void swap(Person& other) noexcept
          {
              using std::swap;
              swap(regNumber, other.regNumber);
              swap(FName,     other.FName);
              swap(scope,     other.scope);
          }
      };
      

      Then your main becomes much simpler.

      int main()
      {
          std::ifstream file("Data");
          Person        person;
          while (file >> person)
          {
              std::cout << person;
          }
      }
      

      It also becomes easier to handle your second part. You load each person then ask the Person object to validate that credentials.

      class Person
      {
           // STUFF From before:
          public:
              bool validateUser(int id, std::string const& name) const
              {
                   return id == regNumber && name == FName;
              }
      };
      
      int main()
      {
      
          int reg = getUserReg();
          std::string  name = getUserName();
      
          std::ifstream file("Data");
          Person        person;
      
          while (file >> person)
          {
              if (person.validateUser(reg, name))
              {
                  std::cout << "Access Granted\n";
              }
          }
      }
      
      • None of this helps solves OP’s problem...
      猜你喜欢
      • 2012-07-16
      • 2015-06-26
      • 1970-01-01
      • 2022-11-20
      • 2020-02-03
      • 2015-05-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多