Thursday, March 20, 2014

C++11: Տեքստի տրոհումը բառերի՝ istream-ի միջոցով

Իմ բլոգի գրառումներում արդեն երկու անգամ անդրադարձել եմ տրված տեքստի բառերի հաճախության աղյուսակի կառուցման խնդրին։ Մի անգամ Tcl լեզվով և մի անգամ էլ Python լեզվով։ Այս անգամ որոշել էի նույն խնդիրը գրել C++11 լեզվով՝ այդ լեզվի հնարավորություններն ուսումնասիրելու համար։

Խնդիրը բաղկացած է երկու մասից ա) տրված տեքստը տրոհել բառերի, և բ) հաշվել տեքստում ամեն մի բառի հանդիպելու հաճախությունը։

Ֆայլից սիմվոլ առ սիմվոլ տեքստը կարդալու և բառեր կազմելու հավես ես չունեի։ C լեզվի գրադարանային strtok ֆունկցիան էլ տարբեր պատճառներով չէի ուզում օգտագործել։ Նպատակ ունեի օգտագործել C++ լեզվի istream դասը և այդ դասի համար սահմանված operator>> գործողությունը։ Ավելի պարզ ասած, ուզում էի, որ հետևյալ read_file ֆունկցիան filename ֆայլից կկարդա բառերը և կլցնի words վեկտորի մեջ.
void read_file( char* filename, std::vector<std::string>& words )
{
  std::string w{ "" };
  std::ifstream fin{ filename };
  while( !fin.eof() ) {
    fin >> w;
    words.push_back( w );
  }    
  fin.close();
}
Բայց պարզ է, որ words վեկտորը պարզապես լցվելու է տեքստի՝ բացատներով բաժանված հատվածներով, որովհետև լռելությամբ operator>> գործողությունը բաժանիչ (delimiter) է համարում միայն բացատները։
     Իմ խնդրի համար բաժանիչ պետք է համարել այբուբենի մեծատառերից ու փոքրատառերից տարբերովող բոլոր նիշերը։ Եվ istream դասի օբյեկտը պետք է մի որևէ եղանակով կարգավորել այնպես, որ բաժանիչ համարվեն և անտեսվեն բոլոր ոչ պետքական նիշերը։      Ինտերնետում քչփորելուց հետո հասկացա, որ իմ ուզած բաժանիչները սահմանելու համար պետք է ստեղծեմ նոր locale օբյեկտ։ Հետո այդ locale-ի համար էլ սահմանեմ այնպիսի ctype ֆասետ (facet - սրա անունը այդպես էլ չհասկացա), որում արդեն այբուբենի տառերից տարբերվող նիշերին տրված է space դիմակը (mask)։ Ահա այդ նոր սահմանված դասը, որ ժառանգած է std::ctype<char>-ից։
class word_ctype : public std::ctype<char> {
private:
  static const mask* custom_table()
  {
    mask* wcs = new mask[table_size];
    std::copy_n(classic_table(), table_size, wcs);
    for( int c = 32; c < table_size; ++c ) {
      if( isalpha(c) ) continue;
      wcs[c] = (mask)space;
    }
    return wcs;
  }
public:
  word_ctype( std::size_t refs = 0 )
    : ctype(custom_table(), true, refs)
  {}
};
Հետո արդեն ավելի հետաքրքիր մասն է։ Սահմանեցի create_dictionary ֆունկցիան, որի առաջին արգումենտը վերլուծվող ֆայլի անունն է, իսկ երկրորդը՝ կառուցվելիք բառարանի ֆայլի անունն է։ Ստացվեց համարյա ֆունկցիոնալ կոդ։
void create_dictionary( char* infile, char* outfile )
{
  // բառարան է, որը հաշվում է ամեն մի բառի քանակը
  std::map<std::string,int> dict;
  // ընթերցման հոսքի ստեղծում՝ տրված ֆայլի անունով
  std::ifstream fin{infile};
  // ընթերցման հոսքում ներդնել նոր locale օբյեկտ՝ վերը սահմանված facet-ով
  fin.imbue(std::locale{std::locale::classic(), new word_ctype});
  // հոսքից կարդալու երկու իտերատորներ
  std::istream_iterator sbegin(fin), send;
  // կարդալ հոսքը սկզբից մինչև վերջ և բառերն ավելացնել բառարանում
  std::for_each( sbegin, send, [&dict](std::string w){ ++dict[downcase(w)]; } );
  fin.close();
  
  // ստեղծել արտածման հոսք՝ բառարանը գրելու համար
  std::ofstream fout{outfile};
  // բառարանի ամեն մի գրառման համար ...
  for( auto w : dict )
    // ֆայլում գրել բառը և նրա քանակը
    fout << w.first << ',' << w.second << std::endl;
  fout.close();
}
Այս ֆունկցիայում հոսքից կարդացած բառի բոլոր տառերը փոքրատառ դարձնելու համար օգտագործված downcase ֆունկցիան սահմանված է հետևյալ կերպ։
std::string downcase( std::string sr )
{
  std::transform( sr.begin(), sr.end(), sr.begin(), ::tolower );
  return sr;
}

No comments:

Post a Comment