Как построчно прочитать файл c++ с конца вверх

Есть файлик с 5+ миллионов строчек который необходимо проанализировать. Необходимо построчно прочитать этот файл с конца вверх недетерминированное кол-во строк. (Т.е. я не могу до непосредственного анализа узнать, сколько строчек будет сохранено).

Этот вопрос уже гуглил. Гугл даёт два основных ответа:

  1. Записать весь файл в вектор и развернуть. Не подходит т.к. слишком затратно читать весь файл.
  2. Переместить указатель на n строку и начать читать. Не подходит т.к. я не могу заранее определить это число n

Возможно, что я просто плохо гуглил, но, пожалуйста, подскажите куда курить.


Ответы (3 шт):

Автор решения: Harry

файлик с 5+ миллионов строчек

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

Если гарантированно нужны очень небольшие расстояния от конца файла — читаем блок, заведомо больший, чем требуется, и делаем примерно то же в нем, считая теперь номера строк от конца файла.

Поскольку так нам известны точные позиции строк, прочесть "если что" следующий блок и совместить его с предыдущим — задача не из сложных.

→ Ссылка
Автор решения: LShadow77

Добавлю свой вариант ответа с практической реализацией.

Как уже отметили выше, чтобы реверсивно читать строки из текстового файла, целесообразно использовать буфер. Читаем в буфер очередной блок с конца, ищем EOL. Если подошли к началу блока, читаем следующий (вернее, предыдущий) блок, и т.д. Когда нашли EOL, конструируем результирующую строку из символов в буфере и остатка в файле. Однако у такого подхода есть недостаток: при чтении очередного блока в буфер, данные предыдущего блока теряются, и если большая часть строки окажется в старом блоке, то её придётся прочитать из файла повторно. А операция чтения из файла медленная, эффективность такого алгоритма, стало быть, не самая лучшая.

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

  • начало строки в основном буффере;
  • остаток во вторичном буфере;
  • остаток в файле (его придётся читать из файла повторно).

Теперь по реализации. Самое простое, что приходит в голову, это представить такой реверсивно прочитываемый файл в виде класса. Назавём его CReversedFile. Определим для него основные методы, через которые будем с этим классом взаимодействовать:

    bool Open(const char* pcstrFileName);
    void Close();
    bool IsTop();  //true, если достигли начала
    char* revGets(char* pStrBuf, int bufSize);

Метод revGets() пускай будет аналогичен функции fgets(), только читать строки будет в обратном порядке. Сделаем для него три допущения: 1) метод всегда завершает строки парой символов "\n\0" (даже если прочитывается конечная строка, которая не обязательно завершается комбинацией EOL); 2) в качестве EOL считаются одиночные символы '\r', '\n', а так же пары "\r\n" либо "\n\r" (такой формат принят в Windows); 3) если длина прочитываемой строки превысит значение 'bufSize-2', то такая строка будет обрезана, однако всё равно окончена парой "\n\0".

Вот сама реализация - программа, которая читает входной текстовый файл и записывает построчно-перевёрнутый результат в выходной файл. Имена файлов задаются через командную строку. Код отладил, однако всё равно могут быть ляпы. И да, поскольку я тут использовал старые функции из библиотеки C, то корректно будет работать с файлами, размером не более 2Гб. И обработку маловероятных, но всё же возможных ошибок при позиционировании и чтении файлов я тоже опустил, чтобы не загромождать код.

#include <stdio.h>
#include <assert.h>

#define STR_BUFFER_SIZE              300

#define REVERSED_FILE_BUFFER_SIZE    65536

class CReversedFile
{
    struct _BUFFER
    {
        char*   pEnd;
        char    data[REVERSED_FILE_BUFFER_SIZE];

        void reset()            {pEnd = data;}
        void set_size(int size) {pEnd = data + size;}
        int  get_size()         {return pEnd - data;}
        bool is_empty()         {return pEnd == data;}
    };

    FILE*       m_pFile;
    long int    m_filePos;
    bool        m_bCheckEOL;
    char        m_lastEOL;
    int         m_iBuffer;
    _BUFFER     m_buffers[2];
    
    bool _fill_buffer();

public:
    CReversedFile() :m_pFile(NULL) {}
    CReversedFile(const CReversedFile&) = delete;
    operator =(const CReversedFile&) = delete;
    ~CReversedFile() {assert (m_pFile==NULL);}

    bool Open(const char* pcstrFileName);
    void Close();
    bool IsTop();
    char* revGets(char* pStrBuf, int bufSize);
};


bool CReversedFile::_fill_buffer()
{
    if (m_filePos)
    {
        m_iBuffer^= 1;  //switch buffers
        _BUFFER* pBuffer = m_buffers + m_iBuffer;

        m_filePos-= REVERSED_FILE_BUFFER_SIZE;

        if (m_filePos >= 0)
        {
            fseek(m_pFile, m_filePos, SEEK_SET);
            fread(pBuffer->data, sizeof(char), REVERSED_FILE_BUFFER_SIZE, m_pFile);
            fseek(m_pFile, m_filePos, SEEK_SET);
            pBuffer->set_size(REVERSED_FILE_BUFFER_SIZE);
            return true;
        }
        
        //m_filePos < 0        
        size_t bytes_left = m_filePos + REVERSED_FILE_BUFFER_SIZE;
        fseek(m_pFile, 0, SEEK_SET);
        fread(pBuffer->data, sizeof(char), bytes_left, m_pFile);
        fseek(m_pFile, 0, SEEK_SET);
        pBuffer->set_size(bytes_left);
        m_filePos = 0;
        return true;
    }

    return false;
}


bool CReversedFile::Open(const char* pcstrFileName)
{
    assert (m_pFile==NULL);

    if (m_pFile = fopen(pcstrFileName, "rb"))
    {
        fseek(m_pFile,0,SEEK_END);
        m_filePos = ftell(m_pFile);
        m_bCheckEOL = false;
        //m_lastEOL = '\0';
        m_iBuffer = 0;
        m_buffers[0].reset();
        return true;
    };

    return false;
}


void CReversedFile::Close()
{
    assert (m_pFile!=NULL);
    fclose(m_pFile);
    m_pFile = NULL;
}


bool CReversedFile::IsTop()
{
    return (!m_filePos) && (m_buffers[m_iBuffer].is_empty());
}


char* CReversedFile::revGets(char* pStrBuf, int bufSize)
{
    assert (m_pFile!=NULL);
    assert (pStrBuf!=NULL);
    assert (bufSize > 2);

    if (!IsTop())
    {
        bufSize-= 2; //reserve last two bytes for trailing "\n\0"

        _BUFFER* pBuffer = m_buffers + m_iBuffer;
        size_t inBufferLeft = 0;
        size_t inFileLeft = 0;

        char* pStart = pBuffer->data;
        char* pEnd = pBuffer->pEnd;
        char* pChr = pEnd;
        char chr;

        int eolShift;
        
        while (true)
        {
            if (pChr > pStart)
            {
                chr = *(--pChr);

                if ((chr!='\n') && (chr!='\r'))
                {
                    m_bCheckEOL = false;
                    continue;
                }

                if ((m_bCheckEOL) && (chr!=m_lastEOL))
                {
                    --pEnd;
                    m_bCheckEOL = false;
                    continue;
                }
                
                pBuffer->pEnd = pChr++;
                m_bCheckEOL = true;
                m_lastEOL = chr;
                eolShift = 1;
            }
            else if (_fill_buffer())
            {
                inFileLeft+= inBufferLeft;
                inBufferLeft = pEnd - pChr;

                pBuffer = m_buffers + m_iBuffer;
                pStart = pBuffer->data;
                pEnd = pBuffer->pEnd; 
                pChr = pEnd;
                continue;
            }
            else
            {
                pBuffer->pEnd = pStart;
                eolShift = 0;
            }
        
            int cnt = pEnd - pChr;

            if (cnt > bufSize)
            {
                pEnd = pChr + bufSize;
                inBufferLeft = 0;
            }
        
            char* pDst = pStrBuf;
            //char* pStrBufEnd = pDst + bufSize;

            while(pChr < pEnd) *pDst++ = *pChr++;
        
            if (inBufferLeft)
            {
                bufSize-= cnt;

                if (inBufferLeft > bufSize)
                {
                    inBufferLeft = bufSize;
                    inFileLeft = 0;
                }

                pChr = m_buffers[m_iBuffer^1].data;
                pEnd = pChr + inBufferLeft;
                while(pChr < pEnd) *pDst++ = *pChr++;
                
                if (inFileLeft)
                {
                    bufSize-= inBufferLeft;

                    if (inFileLeft > bufSize) inFileLeft = bufSize;

                    fseek(m_pFile, m_filePos + pBuffer->get_size()+eolShift + cnt + inBufferLeft, SEEK_SET);
                    fread(pDst, sizeof(char), inFileLeft, m_pFile);
                    fseek(m_pFile, m_filePos, SEEK_SET);
                    pDst+= inFileLeft;
                }
            }

            *pDst++ = '\n';
            *pDst =   '\0';
            return pStrBuf;
        }
    }

    return NULL;
}


int main(int argc, char* argv[])
{
    if (argc > 2)
    {
        CReversedFile inRevFile;
        FILE* pOutFile;

        if (!inRevFile.Open(argv[1]))
        {
            printf("ERROR: can't open the input file \"%s\"\n", argv[1]);
            return 1;
        }

        pOutFile = fopen(argv[2], "w+");
        if (!pOutFile)
        {
            inRevFile.Close();
            printf("ERROR: can't open the output file \"%s\"\n", argv[2]);
            return 2;
        }

        char strBuf[STR_BUFFER_SIZE];
        while (inRevFile.revGets(strBuf, STR_BUFFER_SIZE)) fputs(strBuf, pOutFile);

        fclose(pOutFile);
        inRevFile.Close();
        return 0;
    }

    printf("Usage: %s <in_file_name> <out_file_name>\n", argv[0]);
    return 0;
}
→ Ссылка
Автор решения: user2240578

Все же просто: 1) тебе известен размер файла, 2) ты можешь сделать seekg конец файла/произвольная позиция, 3) читаешь по одному символу, сохраняя его в буфер, пока не встретишь "\n", 4) копируешь буфер от конца до начала - прочитанная строка

→ Ссылка