【问题标题】:Quickly read the last line of a text file?快速读取文本文件的最后一行?
【发布时间】:2010-10-15 17:36:26
【问题描述】:

在 Java 中从 [非常、非常大] 文件中读取最后一行文本的最快和最有效的方法是什么?

【问题讨论】:

    标签: java file io


    【解决方案1】:

    为了避免与恢复字符串(或 StringBuilder)相关的 Unicode 问题,如Eric Leschinski 优秀答案中所述,可以从文件末尾读取字节列表,将其恢复为字节数组,然后然后从字节数组创建字符串。

    以下是对Eric Leschinski 答案代码的更改,以使用字节数组进行。代码更改在注释的代码行下方:

    static public String tail2(File file, int lines) {
        java.io.RandomAccessFile fileHandler = null;
        try {
            fileHandler = new java.io.RandomAccessFile( file, "r" );
            long fileLength = fileHandler.length() - 1;
            //StringBuilder sb = new StringBuilder();
            List<Byte> sb = new ArrayList<>();
            int line = 0;
    
            for(long filePointer = fileLength; filePointer != -1; filePointer--){
                fileHandler.seek( filePointer );
                int readByte = fileHandler.readByte();
    
                if( readByte == 0xA ) {
                    if (filePointer < fileLength) {
                        line = line + 1;
                    }
                } else if( readByte == 0xD ) {
                    if (filePointer < fileLength-1) {
                        line = line + 1;
                    }
                }
                if (line >= lines) {
                    break;
                }
                //sb.add( (char) readByte );
                sb.add( (byte) readByte );
            }
    
            //String lastLine = sb.reverse().toString();
            //Revert byte array and create String
            byte[] bytes = new byte[sb.size()];
            for (int i=0; i<sb.size(); i++) bytes[sb.size()-1-i] = sb.get(i);
            String lastLine = new String(bytes);
            return lastLine;
        } catch( java.io.FileNotFoundException e ) {
            e.printStackTrace();
            return null;
        } catch( java.io.IOException e ) {
            e.printStackTrace();
            return null;
        }
        finally {
            if (fileHandler != null )
                try {
                    fileHandler.close();
                } catch (IOException e) {
                }
        }
    }
    

    【讨论】:

      【解决方案2】:

      下面是两个函数,一个返回文件的最后一个非空行而不加载或单步执行整个文件,另一个返回文件的最后 N 行而不单步执行整个文件:

      tail 的作用是直接缩放到文件的最后一个字符,然后逐个字符地后退,记录它看到的内容,直到找到换行符。一旦找到换行符,它就会跳出循环。反转记录的内容并将其放入字符串并返回。 0xA 是新行,0xD 是回车。

      如果您的行结尾是\r\ncrlf 或其他一些“双换行样式换行”,那么您必须指定n*2 行来获取最后n 行,因为它每行计算2 行。

      public String tail( File file ) {
          RandomAccessFile fileHandler = null;
          try {
              fileHandler = new RandomAccessFile( file, "r" );
              long fileLength = fileHandler.length() - 1;
              StringBuilder sb = new StringBuilder();
      
              for(long filePointer = fileLength; filePointer != -1; filePointer--){
                  fileHandler.seek( filePointer );
                  int readByte = fileHandler.readByte();
      
                  if( readByte == 0xA ) {
                      if( filePointer == fileLength ) {
                          continue;
                      }
                      break;
                      
                  } else if( readByte == 0xD ) {
                      if( filePointer == fileLength - 1 ) {
                          continue;
                      }
                      break;
                  }
      
                  sb.append( ( char ) readByte );
              }
      
              String lastLine = sb.reverse().toString();
              return lastLine;
          } catch( java.io.FileNotFoundException e ) {
              e.printStackTrace();
              return null;
          } catch( java.io.IOException e ) {
              e.printStackTrace();
              return null;
          } finally {
              if (fileHandler != null )
                  try {
                      fileHandler.close();
                  } catch (IOException e) {
                      /* ignore */
                  }
          }
      }
      

      但你可能不想要最后一行,你想要最后 N 行,所以改用这个:

      public String tail2( File file, int lines) {
          java.io.RandomAccessFile fileHandler = null;
          try {
              fileHandler = 
                  new java.io.RandomAccessFile( file, "r" );
              long fileLength = fileHandler.length() - 1;
              StringBuilder sb = new StringBuilder();
              int line = 0;
      
              for(long filePointer = fileLength; filePointer != -1; filePointer--){
                  fileHandler.seek( filePointer );
                  int readByte = fileHandler.readByte();
      
                   if( readByte == 0xA ) {
                      if (filePointer < fileLength) {
                          line = line + 1;
                      }
                  } else if( readByte == 0xD ) {
                      if (filePointer < fileLength-1) {
                          line = line + 1;
                      }
                  }
                  if (line >= lines) {
                      break;
                  }
                  sb.append( ( char ) readByte );
              }
      
              String lastLine = sb.reverse().toString();
              return lastLine;
          } catch( java.io.FileNotFoundException e ) {
              e.printStackTrace();
              return null;
          } catch( java.io.IOException e ) {
              e.printStackTrace();
              return null;
          }
          finally {
              if (fileHandler != null )
                  try {
                      fileHandler.close();
                  } catch (IOException e) {
                  }
          }
      }
      

      像这样调用上述方法:

      File file = new File("D:\\stuff\\huge.log");
      System.out.println(tail(file));
      System.out.println(tail2(file, 10));
      

      警告 在 unicode 的狂野西部,此代码可能会导致此函数的输出出错。例如“Mary?s”而不是“Mary's”。带有hats, accents, Chinese characters 等的字符可能会导致输出错误,因为重音符号作为修饰符添加在字符之后。反转复合字符会在反转时改变字符身份的性质。您必须对计划使用的所有语言进行全面测试。

      有关此 Unicode 反转问题的更多信息,请阅读以下内容: https://codeblog.jonskeet.uk/2009/11/02/omg-ponies-aka-humanity-epic-fail/

      【讨论】:

      • 以上没有考虑到以 CR 和 LF 结尾的行。
      • 您的多行实现在 filePointer == fileLength 的特殊情况下不起作用,行将保持不变,因此条件行 == 行将不会触发,代码将读取整个文件.
      • 关于 Unicode 狂野西部的警告是承认错误。使用的算法是有缺陷的,因此是错误,因此是警告。在 UTF8 中有一个最大长度的多字节编码;该长度为 N=4B×组合字符的最大长度。对于文件中的任何给定字节位置,该字节可能是非规范化 Unicode 字符中的非初始字节/字素。当上面的代码认为它发现了一个事实时,它必须将其视为可疑事实,直到检查前面的 N 个字节以确定可疑事实是否由于在 UTF8 多字节序列内而实际上不是事实。
      • @AndreasZUERCHER 我承认这个错误,正如你所说,接下来我希望你分析并编写一个程序来解决全球时区问题,正如计算机爱好者在这里描述的那样:youtube.com/watch?v=-5wpm-gesOY
      • 为了避免与恢复字符串(或 StringBuilder)相关的 Unicode 问题,可以从文件末尾读取字节列表,将其恢复为字节数组,然后创建字符串来自字节数组。
      【解决方案3】:
      Path path = Paths.get(pathString);
            List<String> allLines = Files.readAllLines(path);
            return allLines.get(allLines.size()-1);
      

      【讨论】:

      • 我认为您错过了 OP 所说的“有效”=v 将整个文件加载到内存中似乎不是 efficient 只获取最后一行的方式..
      【解决方案4】:

      据我所知,读取文本文件最后一行的最快方法是使用“org.apache.commons.io”中的 FileUtils Apache 类。我有一个 200 万行的文件,通过使用这个类,我用了不到一秒钟的时间找到了最后一行。这是我的代码:

      LineIterator lineIterator = FileUtils.lineIterator(newFile(filePath),"UTF-8");
      String lastLine="";
      while (lineIterator.hasNext()){
       lastLine=  lineIterator.nextLine();
      }
      

      【讨论】:

      • Lorenzo 上面的相同评论也适用于这里:这可行,但可能不是最有效的解决方案。
      【解决方案5】:
      try(BufferedReader reader = new BufferedReader(new FileReader(reqFile))) {
      
          String line = null;
      
          System.out.println("======================================");
      
          line = reader.readLine();       //Read Line ONE
          line = reader.readLine();       //Read Line TWO
          System.out.println("first line : " + line);
      
          //Length of one line if lines are of even length
          int len = line.length();       
      
          //skip to the end - 3 lines
          reader.skip((reqFile.length() - (len*3)));
      
          //Searched to the last line for the date I was looking for.
      
          while((line = reader.readLine()) != null){
      
              System.out.println("FROM LINE : " + line);
              String date = line.substring(0,line.indexOf(","));
      
              System.out.println("DATE : " + date);      //BAM!!!!!!!!!!!!!!
          }
      
          System.out.println(reqFile.getName() + " Read(" + reqFile.length()/(1000) + "KB)");
          System.out.println("======================================");
      } catch (IOException x) {
          x.printStackTrace();
      }
      

      【讨论】:

        【解决方案6】:

        Apache Commons 有一个使用 RandomAccessFile 的实现。

        它叫ReversedLinesFileReader

        【讨论】:

        • 我认为这是逆序读取文件最快的方法
        • @JuanToroMarty 可以循环 readLine() 方法。
        • 这在我看来是最优雅的方法。
        【解决方案7】:

        您可以轻松更改以下代码以打印最后一行。

        用于打印最后 5 行的 MemoryMappedFile:

        private static void printByMemoryMappedFile(File file) throws FileNotFoundException, IOException{
                FileInputStream fileInputStream=new FileInputStream(file);
                FileChannel channel=fileInputStream.getChannel();
                ByteBuffer buffer=channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
                buffer.position((int)channel.size());
                int count=0;
                StringBuilder builder=new StringBuilder();
                for(long i=channel.size()-1;i>=0;i--){
                    char c=(char)buffer.get((int)i);
                    builder.append(c);
                    if(c=='\n'){
                        if(count==5)break;
                        count++;
                        builder.reverse();
                        System.out.println(builder.toString());
                        builder=null;
                        builder=new StringBuilder();
                    }
                }
                channel.close();
            }
        

        RandomAccessFile 打印最后 5 行:

        private static void printByRandomAcessFile(File file) throws FileNotFoundException, IOException{
                RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
                int lines = 0;
                StringBuilder builder = new StringBuilder();
                long length = file.length();
                length--;
                randomAccessFile.seek(length);
                for(long seek = length; seek >= 0; --seek){
                    randomAccessFile.seek(seek);
                    char c = (char)randomAccessFile.read();
                    builder.append(c);
                    if(c == '\n'){
                        builder = builder.reverse();
                        System.out.println(builder.toString());
                        lines++;
                        builder = null;
                        builder = new StringBuilder();
                        if (lines == 5){
                            break;
                        }
                    }
        
                }
            }
        

        【讨论】:

        • 为我工作。谢谢。那样有什么不方便的吗?
        【解决方案8】:

        C# 中,您应该能够设置流的位置:

        发件人:http://bytes.com/groups/net-c/269090-streamreader-read-last-line-text-file

        using(FileStream fs = File.OpenRead("c:\\file.dat"))
        {
            using(StreamReader sr = new StreamReader(fs))
            {
                sr.BaseStream.Position = fs.Length - 4;
                if(sr.ReadToEnd() == "DONE")
                    // match
            }
        }
        

        【讨论】:

        • 在Java的FileInputStream(FileReader所基于)中,不能设置位置;您只能向前跳过,这可能不会读取您跳过的部分,但仍然是单向操作,因此不适合在距末尾未知偏移量处寻找换行符。
        • 您可以使用 mark() 来解决这个问题,具体取决于流 markLimit() 是什么。
        【解决方案9】:

        看看我对similar question for C# 的回答。代码将非常相似,尽管 Java 中的编码支持有所不同。

        总的来说,这并不是一件非常容易的事情。正如 MSalter 所指出的,UTF-8 确实可以很容易地发现 \r\n,因为这些字符的 UTF-8 表示与 ASCII 相同,并且这些字节不会出现在多字节字符中。

        所以基本上,取一个(比如说)2K 的缓冲区,然后逐步向后读取(跳到之前的 2K,读取下一个 2K)检查行终止。然后跳到流中正确的位置,在顶部创建一个InputStreamReader,并在其顶部创建一个BufferedReader。然后只需拨打BufferedReader.readLine()

        【讨论】:

        • UTF-8 无关紧要 - 您需要最后一个 CR 或 LF 字符,它在 ASCII 和 UTF-8 中都是一个字节。
        【解决方案10】:

        使用 FileReader 或 FileInputStream 将不起作用 - 您必须使用 FileChannelRandomAccessFile 从末尾向后循环文件。正如 Jon 所说,编码将是一个问题。

        【讨论】:

        • 注意,RandomAccessFile 的性能对于单个操作来说很糟糕——读取缓冲区的合理大小也是如此。
        猜你喜欢
        • 2012-08-06
        • 2012-07-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多