用C/C++从文件中读取最后n行

12

我看了很多帖子,但没有找到我想要的东西。
我得到了错误的输出:

ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ......  // may be this is EOF character

进入无限循环。

我的算法:

  1. 跳转到文件结尾。
  2. 将指针位置减1并逐个读取字符。
  3. 如果我们找到了10行或到达文件开头,则退出。
  4. 现在我将扫描整个文件直到EOF并打印它们 //代码未实现。

代码:

#include<iostream>
#include<stdio.h>
#include<conio.h>
#include<stdlib.h>
#include<string.h>

using namespace std;
int main()
{
    FILE *f1=fopen("input.txt","r");
    FILE *f2=fopen("output.txt","w");
    int i,j,pos;
        int count=0;
        char ch;
        int begin=ftell(f1);
        // GO TO END OF FILE
        fseek(f1,0,SEEK_END);
        int end = ftell(f1);
        pos=ftell(f1);

        while(count<10)
        {
            pos=ftell(f1);
            // FILE IS LESS THAN 10 LINES
            if(pos<begin)
                break;
            ch=fgetc(f1);
            if(ch=='\n')
                count++;
            fputc(ch,f2);
            fseek(f1,pos-1,end);
        }
    return 0;
}

更新 1:

修改后的代码:现在只有一个错误 - 如果输入包含以下行

3enil
2enil
1enil

it prints 10 lines only

line1
line2
line3ÿine1
line2
line3ÿine1
line2
line3ÿine1
line2
line3ÿine1
line2

PS:
1. 在notepad++上在Windows上工作

  1. 这不是作业

  2. 我也想不使用更多的内存或STL来完成它。

  3. 我正在练习提高我的基础知识,所以请不要发布任何函数(如tail -5 tc.)

请帮助改进我的代码。


8
提示:fgetc 函数会将文件位置指示器向前移动一个字符位置。 - jsalonen
1
尝试使用 fseek(f1, pos-1, SEEK_SET); 和文件模式 bin。 - BLUEPIXY
4
C还是C++?选一个。(提示:大部分情况下是C。) - Lightness Races in Orbit
是的,为什么不使用 fstream - nkint
@JamesKanze 您能详细说明您想说的话吗? - Aseem Goyal
显示剩余2条评论
8个回答

9

代码中的注释

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *in, *out;
    int count = 0;
    long int pos;
    char s[100];

    in = fopen("input.txt", "r");
    /* always check return of fopen */
    if (in == NULL) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }
    out = fopen("output.txt", "w");
    if (out == NULL) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }
    fseek(in, 0, SEEK_END);
    pos = ftell(in);
    /* Don't write each char on output.txt, just search for '\n' */
    while (pos) {
        fseek(in, --pos, SEEK_SET); /* seek from begin */
        if (fgetc(in) == '\n') {
            if (count++ == 10) break;
        }
    }
    /* Write line by line, is faster than fputc for each char */
    while (fgets(s, sizeof(s), in) != NULL) {
        fprintf(out, "%s", s);
    }
    fclose(in);
    fclose(out);
    return 0;
}

你的实现很好,但我想知道如果我有非常非常多的行和字符,比如约20-30 GB的文件,它是否能正常工作。 - Aseem Goyal
看起来您正在使用Windows,而20-30GB的文件可能会有问题。请使用_fseeki64_ftelli64,这两个函数即使在32位的Windows系统下也支持更长的文件偏移量。 - David Ranieri

8

您的代码存在若干问题。最重要的一个问题是您从未检查任何函数是否成功执行。将 ftell 的结果保存在一个 int 中也不是一个很好的想法。然后是测试 pos < begin;只有出现错误时才会发生这种情况。您将 fgetc 的结果存储在一个 char 中(这会导致信息丢失)。第一次读取的是文件结尾,因此会失败(一旦流进入错误状态,就会保持在那里)。如果文件以文本模式打开,则无法可靠地对 ftell 返回的值进行算术运算(除非在 Unix 系统下)。

哦,还有没有“EOF字符”;'ÿ' 是一个完全有效的字符(在 Latin-1 中为 0xFF)。一旦您将 fgetc 的返回值分配给一个 char,就失去了测试文件结尾的可能性。

我还想补充一点,逐个字符反向读取效率极低。通常的解决方案是分配足够大的缓冲区,然后计算其中的 '\n' 数量。

编辑:

以下是一个快速的代码示例,以便了解思路:

std::string
getLastLines( std::string const& filename, int lineCount )
{
    size_t const granularity = 100 * lineCount;
    std::ifstream source( filename.c_str(), std::ios_base::binary );
    source.seekg( 0, std::ios_base::end );
    size_t size = static_cast<size_t>( source.tellg() );
    std::vector<char> buffer;
    int newlineCount = 0;
    while ( source 
            && buffer.size() != size
            && newlineCount < lineCount ) {
        buffer.resize( std::min( buffer.size() + granularity, size ) );
        source.seekg( -static_cast<std::streamoff>( buffer.size() ),
                      std::ios_base::end );
        source.read( buffer.data(), buffer.size() );
        newlineCount = std::count( buffer.begin(), buffer.end(), '\n');
    }
    std::vector<char>::iterator start = buffer.begin();
    while ( newlineCount > lineCount ) {
        start = std::find( start, buffer.end(), '\n' ) + 1;
        -- newlineCount;
    }
    std::vector<char>::iterator end = remove( start, buffer.end(), '\r' );
    return std::string( start, end );
}

这段代码在错误处理方面有些薄弱;特别是,你可能想要区分无法打开文件和其他任何错误。(不应该出现其他错误,但你永远不知道。)

此外,这仅适用于Windows,并假定实际文件只包含纯文本,不包含任何不属于CRLF的'\r'。(对于Unix,只需删除倒数第二行即可。)


实际上我只是为了练习编程,想要倒序读取一个文件。这并不是出于任何效率目的,而只是为了自信地处理文件。 我学到了很多以前不知道的东西,谢谢。 - Aseem Goyal
1
重要的是在使用读取结果之前始终检查错误(我在示例代码中没有这样做),fgetc(和istream::get())返回一个int,而不是一个char,以返回一个带外EOF(也用于错误),并且任何错误条件都是粘性的:如果您看到错误,则必须在流上进行任何进一步操作之前清除它。如果您正在使用C ++,最好学习iostream,因为它更加灵活和安全。 - James Kanze
正如你所指出的,“在int中使用ftell并不是一个很好的主意”。pos=ftell(pos);如果(pos<0) break;这段代码无法工作。那么在C语言中该怎么办呢?我之后会尝试C++。 - Aseem Goyal
使用int类型存储ftell返回值不是一个好主意,因为该函数返回一个long类型的值,将其放入int类型中可能会导致溢出。但如果您正在测试小文件,则这不是您的问题。另一件事是该long类型的值:在Windows(至少在二进制文件上)和Unix下,它是从文件开头算起的字节数;除非出现错误,否则它永远不会小于零。 - James Kanze
在你的代码中,不能将位置设置在文件开始之前,至少要使用 begin - James Kanze
还有一个要点:在Windows下,您无法以文本模式读取文件并倒序排列。这是不可能的。确保以二进制模式打开文件,然后手动处理CRLF序列中的'\r'。(请注意,大多数涉及寻址的操作都依赖于实现,并且在Windows和Unix下的行为不同。) - James Kanze

4
这可以通过使用循环数组来高效地完成。不需要额外的缓冲区。
void printlast_n_lines(char* fileName, int n){

    const int k = n;
    ifstream file(fileName);
    string l[k];
    int size = 0 ;

    while(file.good()){
        getline(file, l[size%k]); //this is just circular array
        cout << l[size%k] << '\n';
        size++;
    }

    //start of circular array & size of it 
    int start = size > k ? (size%k) : 0 ; //this get the start of last k lines 
    int count = min(k, size); // no of lines to print

    for(int i = 0; i< count ; i++){
        cout << l[(start+i)%k] << '\n' ; // start from in between and print from start due to remainder till all counts are covered
    }
}

请提供反馈。

1
我相信,你在使用fseek时有误。请查看谷歌上的man fseek
尝试这样做:
fseek(f1, -2, SEEK_CUR);
//1 to neutrialize change from fgect
//and 1 to move backward

同时,您应该将位置设置在最后一个元素的开头:
fseek(f1, -1, SEEK_END).

您不需要使用end变量。

您应该检查所有函数(fgetc, fseekftell)的返回值。这是一个好习惯。我不知道这段代码是否适用于空文件或类似情况。


$ man fseek 'man' 不是内部或外部命令,也不是可运行的程序或批处理文件。 - default
@默认使用Linux或互联网。 - Ari
@Ari 原帖明确说明他正在使用Windows。(即使在Unix下,如果您对可移植性感兴趣,我建议您转向Posix标准,而不是man。尽管许多man页面将指定什么是标准,什么是扩展。) - James Kanze
@JamesKanze 我犯了一个错误。已将更改人源从本地更改为 Google。 - Ari

1
int end = ftell(f1);
pos=ftell(f1);

这告诉您文件的最后一个点,即EOF。 当您读取时,会出现EOF错误,并且指针希望向前移动1个空格...
因此,我建议将当前位置减小1。 或者在while循环的开头放置fseek(f1,-2,SEEK_CUR),以弥补fread 1点并向后移动1点...

0
使用:fseek(f1,-2,SEEK_CUR); 可以返回。
我写了这段代码,它可以工作,你可以试一下:
#include "stdio.h"

int main()
{
        int count = 0;
        char * fileName = "count.c";
        char * outFileName = "out11.txt";
        FILE * fpIn;
        FILE * fpOut;
        if((fpIn = fopen(fileName,"r")) == NULL )
                printf(" file %s open error\n",fileName);
        if((fpOut = fopen(outFileName,"w")) == NULL )
                printf(" file %s open error\n",outFileName);
        fseek(fpIn,0,SEEK_END);
        while(count < 10)
        {
                fseek(fpIn,-2,SEEK_CUR);
                if(ftell(fpIn)<0L)
                        break;
                char now = fgetc(fpIn);
                printf("%c",now);
                fputc(now,fpOut);
                if(now == '\n')
                        ++count;
        }
        fclose(fpIn);
        fclose(fpOut);
}

它应该是 fseek(f1,-2,SEEK_CUR); - Aseem Goyal
@anon 这是因为我们使用了宽字符吗? - Lidong Guo
@LidongGuo,因为fgetc()会占用1个位置。 - David Ranieri

0

这是C++的解决方案。

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

int main(int argc, char *argv[])                                                
{                                                                               
    auto& file = std::cin;                                                      

    int n = 5;                                                                  
    if (argc > 1) {                                                             
        try {                                                                   
            n = std::stoi(argv[1]);                                             
        } catch (std::exception& e) {                                           
            std::cout << "Error: argument must be an int" << std::endl;         
            std::exit(EXIT_FAILURE);                                            
        }                                                                       
    }                                                                           

    file.seekg(0, file.end);                                                    

    n = n + 1; // Add one so the loop stops at the newline above                
    while (file.tellg() != 0 && n) {                                            
        file.seekg(-1, file.cur);                                               
        if (file.peek() == '\n')                                                
            n--;                                                                
    }                                                                           

    if (file.peek() == '\n') // If we stop in the middle we will be at a newline
        file.seekg(1, file.cur);                                                

    std::string line;                                                           
    while (std::getline(file, line))                                            
        std::cout << line << std::endl;                                         

    std::exit(EXIT_SUCCESS);                                                    
} 

构建:

$ g++ <SOURCE_NAME> -o last_n_lines

运行:

$ ./last_n_lines 10 < <SOME_FILE>

0
我会使用两个流来打印文件的最后n行: 这将在O(lines)运行时间O(lines)空间内运行。
#include<bits/stdc++.h>
using namespace std;

int main(){
  // read last n lines of a file
  ifstream f("file.in");
  ifstream g("file.in");

  // move f stream n lines down.
  int n;
  cin >> n;
  string line;
  for(int i=0; i<k; ++i) getline(f,line);

  // move f and g stream at the same pace.
  for(; getline(f,line); ){
    getline(g, line);
  }

  // g now has to go the last n lines.
  for(; getline(g,line); )
    cout << line << endl;
}

一个具有O(lines)运行时间O(N)空间复杂度的解决方案是使用队列:
ifstream fin("file.in");
int k;
cin >> k;
queue<string> Q;
string line;
for(; getline(fin, line); ){
  if(Q.size() == k){
    Q.pop();
  }
  Q.push(line);
}
while(!Q.empty()){
  cout << Q.front() << endl;
  Q.pop();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接