Читаем текстовый файл в программе на C++

newbie_

Опубликован:  2019-08-07T12:40:05.078353Z

Продолжаем нарабатывать моторику на решении практических задач в рамках программ на C++. В этой статье поговорим о чтении данных из текстовых файлов и рассмотрим один небольшой, но весьма поучительный пример. Я продемонстрирую один из способов чтения данных из текстового файла и их минимальную необходимую начальную обработку. Все действия будут продемонстрированы на базе операционной системы Debian buster и программ из его состава.

Однажды я уже рассказывал о чтении текстовых файлов с Python3 в одном из выпусков соседнего блога, стоит повторить данное в той статье определение текстового файла. Итак... Текстовый файл - частный случай бинарного файла, содержит набор байт поддающийся декодированию в простой текст с применением определённой кодировки.

Главная проблема при чтении данных из текстового файла - это правильное определение кодировки файла. Если кодировка файла определена верно, то в процессе обработки полученных данных не будет особых проблем, кроме одной - проблемы правильного преобразования данных из исходной кодировки в кодировку локали, на которой будет использоваться разрабатываемая программа, подробности о локалях и способах представления текстовых данных в программах на C++ были детально рассмотрены по ссылке. Две обозначенные задачи я и буду решать в рамках этой демонстрации, а именно:

  • определение кодировки исходного текста;
  • преобразование исходного текста в текст в кодировке локали;

Для решения этих задач мне потребуются соответствующие инструменты. Для определения кодировки исходного текста в репозитарии Debian имеется универсальный детектор кодировки uchardet, чтобы использовать его в рамках программ на C++, достаточно установить соответствующую библиотеку - libuchardet-dev.

sudo apt install -y libuchardet-dev

gXZ8icB5MJ.png

Для преобразования текста из одной кодировки в другую в Debian имеется специальный инструмент - iconv. В блоге Дебианыча рассмотрен способ использования обоих утилит в командной строке. Так как iconv написан на C и является системным инструментом, для его использования в программах на C++ ничего дополнительно ставить не придётся, потому что всё уже установлено в дефолте.

Чтобы продемонстрировать работу с данными, мне нужен исходный текстовый файл в кодировке, отличной от кодировки моей локали. У меня есть такой файл, просто копирую его в свою текущую директорию.

ea7jt1cD3t.png

Этот файл пригодится для ручного тестирования полученного кода. Приступим...

Создаю в текущей директории три новых файла и открываю их в текстовом редакторе Geany.

geany recode_t.cpp rcode.h rcode.cpp

Dug9Lqat1Y.png

В этих трёх файлах будет храниться исходный код задуманной программы. Программа, которую я сейчас разработаю, будет читать данные из текстового файла, определять кодировку полученного текста, преобразовывать этот текст в кодировку локали моей операционной системы и в этом виде выводить его на экран. Начнём с главного модуля программы recode_t.cpp и элементарных телодвижений. На начальном этапе мне потребуется задать имя исходного текстового файла в параметрах командной строки, не прибегая при этом к сложной обработке параметров. Пишу такой код.

#include <iostream>
#include <string>
#include <cstdlib>


int main(int argc, char ** argv)
{
  if (argc < 2 || argc > 2)
  {
    std::cout << "you have to specify a filename" << std::endl;
    std::exit(EXIT_FAILURE);
  }
  std::string fname = argv[1];
  std::cout << "Filename: " << fname << std::endl;
  return 0;
}

Здесь всё элементарно, программа обрабатывает параметры командной строки и, если параметров больше или меньше двух, прерывает исполнение с кодом ошибки и выводит соответствующее сообщение, в ином случае на экран терминала выводится переданное в параметрах имя файла. Сохраняю файл, компилирую его и пробую исполнять программу в разных вариантах, с параметрами и без параметров.

tPMRLFSlHm.png

Отлично. Далее мне потребуется инструмент, который позволит определить, существует ли файл с заданным именем. Код этой функции я вынесу в файл rcode.cpp c декларацией прототипа в файле rcode.h, начнём с прототипа, открываю файл rcode.h и пишу следующий код.

#ifndef RCODE_H_
#define RCODE_H_

#include <string>

bool exists_r(const std::string & fname);

#endif

Самый простой способ проверить существование файла - это функция access модуля unistd.h стандартной библиотеки C, да именно C. Открываю файл rcode.cpp и пишу реализацию функции exists_r.

#include <string>
#include <unistd.h>
#include "rcode.h"

bool exists_r(const std::string & fname)
{
  return access(fname.c_str(), F_OK|R_OK) != -1;
}

Таким образом я могу проверить не только существование файла, но ещё и доступность этого файла для чтения, и в данном случае стандартная библиотека С оказалась как никогда кстати. Вношу соответствующие изменения в функцию main.

#include <iostream>
#include <string>
#include <cstdlib>
#include "rcode.h"


int main(int argc, char ** argv)
{
  if (argc < 2 || argc > 2)
  {
    std::cout << "you have to specify a filename" << std::endl;
    std::exit(EXIT_FAILURE);
  }
  std::string fname = argv[1];
  if (exists_r(fname))
  {
    std::cout << "Filename: " << fname << std::endl;
    // в этом блоке появится дополнительный код
  }
  else
    std::cout << "file does not exist" << std::endl;
  return 0;
}

Сохраняю изменения в файлах, иду в терминал и компилирую программу, обращаю внимание, что теперь код состоит из нескольких файлов.

g++ -o exe rcode.cpp recode_t.cpp

Пробую исполнить программу передав аргументом сначала имя существующего файла, а затем имя несуществующего файла.

KkwXQBNcg9.png

Чтение данных из файла может быть сопряжено с разного рода ошибками, которые не удастся исключить, поэтому, чтобы обойти возможные ошибки мне понадобится так называемое исключение. Пишу в файле rcode.h прототип класса исключения RcodeError.

class RcodeError
{
  private:
    std::string message;
  public:
    RcodeError(const char * m) { message = m; }
    const std::string what() {return message; }
};

Теперь я могу приступить к чтению данных из текстового файла, для этого декларирую прототип ещё одной функции - read.

void read(const std::string & fname, std::string & result);

У функции два параметра: строка - имя файла и строка, в которую будут записаны прочитанные из файла данные. Реализация этой функции будет иметь следующий вид.

#include <string>
#include <fstream>
#include <unistd.h>
#include "rcode.h"

void read(const std::string & fname, std::string & result)
{
  std::ifstream fin;
  fin.open(fname);
  if (!fin.is_open())
    throw RcodeError("failed to open your file");
  char ch;
  while (fin.get(ch))
    result += ch;
  fin.close();
}

...

Для чтения файла я использовал стандартные инструменты из модуля fstream стандартной библиотеки. В очередной раз модифицирую функцию main.

  ...

  if (exists_r(fname))
  {
    std::cout << "Filename: " << fname << std::endl;
    std::string data;
    try
    {
      read(fname, data);
      std::cout << data;
    }
    catch (RcodeError & e)
    {
      std::cout << argv[0] << ":" << "error:"
                << e.what() << std::endl;
      std::exit(EXIT_FAILURE);
     }
  }

  ...

Теперь программа должна вывести имя файла и прочитанные из этого файла данные, в том виде, в котором они существовали в файле. Компилирую и пробую исполнять.

7exTZl49qE.png

Отлично. Все данные из файла сохранены в строку, но, увы, поскольку кодировка исходного текста не совпадает с кодировкой локали моей системы, вместо русских букв я вижу в консоли жуткие прямоугольники. Как с этим бороться? Ответ прост, надо перекодировать полученные данные из одной кодировки в другую... Но на этом пути возникает препятствие. Дело в том, что кодировку локали можно задать константой, а вот предугадать, в какой кодировке будет очередной текстовый файл, к сожалению невозможно, поэтому попробуем определить кодировку исходного файла программными средствами. Декларирую прототип ещё одной функции - det_enc.

void det_enc(const std::string & data, std::string & enc);

У функции два параметра:

  • data - заданная строка;
  • enc - строка, в которую будет записано имя кодировки.

Для реализации этой функции мне потребуются инструменты библиотеки libuchardet-dev, весь процесс определения кодировки текста будет выглядеть следующим образом.

#include <string>
#include <fstream>
#include <unistd.h>
#include <uchardet/uchardet.h>
#include "rcode.h"

...

void det_enc(const std::string & data, std::string & enc)
{
  uchardet_t detector = uchardet_new();
  if (uchardet_handle_data(detector, data.c_str(), data.size()))
  {
    uchardet_delete(detector);
      throw RcodeError("encoding detection failed");
   }
  uchardet_data_end(detector);
  enc = uchardet_get_charset(detector);
  uchardet_delete(detector);
}

Дописываю код этой функции в файл rcode.cpp и в очередной раз модифицирую функцию main.

  if (exists_r(fname))
  {
    std::cout << "Filename: " << fname << std::endl;
    std::string data;
    std::string enc;
    try
    {
      read(fname, data);
      det_enc(data, enc);
      std::cout << "Encoding: " << enc << std::endl;
    }
    catch (RcodeError & e)
    {
      std::cout << argv[0] << ":" << "error:"
                << e.what() << std::endl;
      std::exit(EXIT_FAILURE);
     }
  }

Сохраняю изменения во всех файлах, перехожу в консоль и пробую скомпилировать код. Следует обратить внимание, что код теперь использует стороннюю библиотеку, поэтому необходимо в опциях компилятора задать соответствующий параметр для линковщика.

g++ -o exe -luchardet rcode.cpp recode_t.cpp

Ucqzqrqztw.png

Великолепно, часть задачи выполнена, кодировка исходного файла определена, теперь можно попытаться перекодировать строку с данными с помощью ещё одного C-ишного инструмента - iconv. Мне потребуется константа, в которой будет вписано имя текущей кодировки моей локали, и прототип ещё одной функции, дописываю их в файл rcode.h.

...

const std::string ENCODING = "UTF-8";


void recode(
  const std::string & fromenc,
  const std::string & toenc,
  std::string & data);

Функция recode имеет три параметра: имя исходной кодировки, имя новой кодировки и строка, которую необходимо перекодировать. Надо отметить, что C - язык довольно сложный, и чтобы пользоваться его инструментами, нужно достаточно скрупулёзно их изучать и понимать принципы, на которых этот диалект построен. Программисты C - без всяких сомнений мужественные и высокоинтеллектуальные люди. Мне моих текущих знаний оказалось далеко недостаточно, чтобы воспроизвести процесс перекодирования текста с помощью iconv самостоятельно, поэтому пришлось обратиться к Гуглу. И вот что у меня вышло в итоге.

void recode(
  const std::string & fromenc,
  const std::string & toenc, std::string & data)
{
  iconv_t re = iconv_open(toenc.c_str(), fromenc.c_str());
  if (re == (iconv_t) -1)
  {
    iconv_close(re);
      throw RcodeError("encoding converter failed");
  }
  char * outbuf;
  if ((outbuf = (char *) malloc(data.size() * 2 + 1)) == NULL)
  {
    iconv_close(re);
      throw RcodeError("encoding converter failed");
  }
  char * ip = (char *) data.c_str();
  char * op = outbuf;
  size_t icount = data.size();
  size_t ocount = data.size() * 2;
  if (iconv(re, &ip, &icount, &op, &ocount) != (size_t) -1)
  {
    outbuf[data.size() * 2 - ocount] = '\0';
    data = outbuf;
    free(outbuf);
    iconv_close(re);
  }
  else
  {
    free(outbuf);
    iconv_close(re);
    throw RcodeError("encoding converter failed");
  }
}

Замечание: функция recode требует соответствующей директивы препроцессора - #include <iconv.h>.

Дописываю реализацию этой функции в файл rcode.cpp, детальный анализ её содержания впереди, пока я добился только рабочей реализации, без отчётливого понимания, откуда что берется и куда потом девается, увы. Опять изменяю функцию main.

...
    try
    {
      read(fname, data);
      det_enc(data, enc);
      recode(enc, ENCODING, data);
      std::cout << "Encoding: " << enc << std::endl;
      std::cout << data;
    }
    ...

Опять компилирую код и пробую запустить полученную в результате программу.

hx0y4ATSSQ.png

Как видно на снимке экрана, теперь на терминал выводятся данные в кодировке локали, и все символы исходного файла теперь имеют своё отображение. Задача решена.

Подведём итоги... Главный мотивирующий вывод: разработка программ на C++ очень часто будет требовать использования C-ишных библиотек и инструментов, и, чтобы не испытывать сложностей и неловких моментов в работе, хороший программист просто обязан знать С, его базовые принципы работы с данными и функции стандартной библиотеки, эти знания могут существенно облегчить жизнь программиста и дать ключи к решению многих очень часто встречающихся типовых задач.

Код всех файлов программы будет прикреплён в комментариях. И да, комментарии, в том числе критические, приветствуются.

Комментарии:

newbie_

2019-08-07T12:46:03.566294Z

recode_t.cpp

#include <iostream>
#include <string>
#include <cstdlib>
#include "rcode.h"


int main(int argc, char ** argv)
{
  if (argc < 2 || argc > 2)
  {
    std::cout << "you have to specify a filename" << std::endl;
    std::exit(EXIT_FAILURE);
  }
  std::string fname = argv[1];
  if (exists_r(fname))
  {
    std::cout << "Filename: " << fname << std::endl;
    std::string data;
    std::string enc;
    try
    {
      read(fname, data);
      det_enc(data, enc);
      recode(enc, ENCODING, data);
      std::cout << "Encoding: " << enc << std::endl;
      std::cout << data;
    }
    catch (RcodeError & e)
    {
      std::cout << argv[0] << ":" << "error:"
                << e.what() << std::endl;
      std::exit(EXIT_FAILURE);
    }
  }
  else
    std::cout << "file does not exist" << std::endl;
  return 0;
}

rcode.h

#ifndef RCODE_H_
#define RCODE_H_

#include <string>

const std::string ENCODING = "UTF-8";

void recode(
  const std::string & fromenc,
  const std::string & toenc,
  std::string & data);
bool exists_r(const std::string & fname);
void read(const std::string & fname, std::string & result);
void det_enc(const std::string & data, std::string & enc);

class RcodeError
{
  private:
    std::string message;
  public:
    RcodeError(const char * m) { message = m; }
    const std::string what() {return message; }
};


#endif

rcode.cpp

#include <string>
#include <fstream>
#include <unistd.h>
#include <iconv.h>
#include <uchardet/uchardet.h>
#include "rcode.h"

bool exists_r(const std::string & fname)
{
  return access(fname.c_str(), F_OK|R_OK) != -1;
}

void read(const std::string & fname, std::string & result)
{
  std::ifstream fin;
  fin.open(fname);
  if (!fin.is_open())
    throw RcodeError("failed to open your file");
  char ch;
  while (fin.get(ch))
    result += ch;
  fin.close();
}

void det_enc(const std::string & data, std::string & enc)
{
  uchardet_t detector = uchardet_new();
  if (uchardet_handle_data(detector, data.c_str(), data.size()))
  {
    uchardet_delete(detector);
    throw RcodeError("encoding detection failed");
  }
  uchardet_data_end(detector);
  enc = uchardet_get_charset(detector);
  uchardet_delete(detector);
}

void recode(
  const std::string & fromenc,
  const std::string & toenc, std::string & data)
{
  iconv_t re = iconv_open(toenc.c_str(), fromenc.c_str());
  if (re == (iconv_t) -1)
  {
    iconv_close(re);
      throw RcodeError("encoding converter failed");
  }
  char * outbuf;
  if ((outbuf = (char *) malloc(data.size() * 2 + 1)) == NULL)
  {
    iconv_close(re);
      throw RcodeError("encoding converter failed");
  }
  char * ip = (char *) data.c_str();
  char * op = outbuf;
  size_t icount = data.size();
  size_t ocount = data.size() * 2;
  if (iconv(re, &ip, &icount, &op, &ocount) != (size_t) -1)
  {
    outbuf[data.size() * 2 - ocount] = '\0';
    data = outbuf;
    free(outbuf);
    iconv_close(re);
  }
  else
  {
    free(outbuf);
    iconv_close(re);
    throw RcodeError("encoding converter failed");
  }
}