【问题标题】:Get last 10 lines of very large text file > 10GB获取最后 10 行非常大的文本文件 > 10GB
【发布时间】:2010-09-28 17:48:56
【问题描述】:

显示一个非常大的文本文件(这个特定文件超过 10GB)的最后 10 行的最有效方法是什么。我想写一个简单的 C# 应用程序,但我不确定如何有效地做到这一点。

【问题讨论】:

  • “有效”?你到底什么意思?快速执行?内存占用小?
  • 以上所有? :D
  • 快速执行是重中之重。谢谢!

标签: c# text large-files


【解决方案1】:

读到文件末尾,然后向后查找,直到找到十个换行符,然后考虑到各种编码,向前读到末尾。请务必处理文件中的行数少于 10 行的情况。下面是一个实现(在 C# 中,正如您标记的那样),概括为在位于 path 的文件中找到最后一个 numberOfTokens,在 encoding 中编码,其中标记分隔符由 tokenSeparator 表示;结果以string 的形式返回(这可以通过返回枚举令牌的IEnumerable<string> 来改进)。

public static string ReadEndTokens(string path, Int64 numberOfTokens, Encoding encoding, string tokenSeparator) {

    int sizeOfChar = encoding.GetByteCount("\n");
    byte[] buffer = encoding.GetBytes(tokenSeparator);


    using (FileStream fs = new FileStream(path, FileMode.Open)) {
        Int64 tokenCount = 0;
        Int64 endPosition = fs.Length / sizeOfChar;

        for (Int64 position = sizeOfChar; position < endPosition; position += sizeOfChar) {
            fs.Seek(-position, SeekOrigin.End);
            fs.Read(buffer, 0, buffer.Length);

            if (encoding.GetString(buffer) == tokenSeparator) {
                tokenCount++;
                if (tokenCount == numberOfTokens) {
                    byte[] returnBuffer = new byte[fs.Length - fs.Position];
                    fs.Read(returnBuffer, 0, returnBuffer.Length);
                    return encoding.GetString(returnBuffer);
                }
            }
        }

        // handle case where number of tokens in file is less than numberOfTokens
        fs.Seek(0, SeekOrigin.Begin);
        buffer = new byte[fs.Length];
        fs.Read(buffer, 0, buffer.Length);
        return encoding.GetString(buffer);
    }
}

【讨论】:

  • 假定字符大小始终相同的编码。在其他编码中可能会变得棘手。
  • 而且,正如 Skeet 曾经告诉我的,Read 方法不能保证读取请求的字节数。您必须检查返回值以确定您是否已完成阅读...
  • @Jon:可变长度字符编码。哦,快乐。
  • @Will:有几个地方应该在代码中添加错误检查。不过,谢谢你提醒我有关 Stream.Read 的一个令人讨厌的事实。
  • 我注意到这个过程在大约 4MB 的文件上执行时非常及时。有什么改进建议吗?还是其他关于尾文件的 C# 示例?
【解决方案2】:

我可能只是将它作为二进制流打开,寻找到最后,然后返回寻找换行符。备份 10 行(或 11 行,取决于最后一行)以找到您的 10 行,然后阅读到最后并使用 Encoding.GetString 对您阅读的内容将其转换为字符串格式。根据需要拆分。

【讨论】:

    【解决方案3】:

    尾巴? Tail 是一个 unix 命令,将显示文件的最后几行。 Windows 2003 Server resource kit中有Windows版本。

    【讨论】:

    • 他的标签表明他正在寻求 C# 解决方案
    • 我注意到了。我只是想无论如何我都会把它扔掉。
    • 使用 PowerShell:Get-Content bigfile.txt -Tail 10
    【解决方案4】:

    正如其他人所建议的那样,您可以有效地转到文件末尾并向后阅读。但是,这有点棘手 - 特别是因为如果您使用可变长度编码(例如 UTF-8),您需要巧妙地确保获得“完整”字符。

    【讨论】:

    • 嗯? \r\n 是 UTF-8 中的单字节。可能存在问题,但仅限于奇怪的遗留编码。
    • @CodesInChaos:我没有说\r\n 不是单个字节...但是 other 字符占用更多字节(任何超过 U+ 0080)所以你需要考虑到这一点 - 如果你在文件中寻找任意点,你可能是“中间人物”并且必须考虑到这一点。 UTF-8 使它变得可行(但并不容易),因为当你是中间字符时,你总是可以 tell ......但其他编码可能不会。我编写了代码来向后读取文件,这是一项痛苦的工作。
    【解决方案5】:

    您应该能够使用FileStream.Seek() 移动到文件的末尾,然后向后工作,寻找 \n 直到您有足够的行。

    【讨论】:

      【解决方案6】:

      我不确定它的效率如何,但在 Windows PowerShell 中获取文件的最后十行就像

      Get-Content file.txt | Select-Object -last 10
      

      【讨论】:

      • 从 PowerShell v5 开始,Get-Content 命令支持-Tail 参数,没有有这种方法的性能问题。这应该是Get-Content file.txt -Tail 10。此外,您可以指定-Wait 参数以在文件更新时将更新输出到文件,类似于tail -f。所以Get-Content file -Tail 10 -Wait 会输出文件的最后 10 行,然后等待并追加随后添加到文件中的新行。
      【解决方案7】:

      这就是 unix tail 命令的作用。见http://en.wikipedia.org/wiki/Tail_(Unix)

      互联网上有很多开源实现,这里有一个针对 win32 的:Tail for WIn32

      【讨论】:

        【解决方案8】:

        我认为以下代码将通过重新编码的细微变化来解决问题

        StreamReader reader = new StreamReader(@"c:\test.txt"); //pick appropriate Encoding
        reader.BaseStream.Seek(0, SeekOrigin.End);
        int count = 0;
        while ((count < 10) && (reader.BaseStream.Position > 0))
        {
            reader.BaseStream.Position--;
            int c = reader.BaseStream.ReadByte();
            if (reader.BaseStream.Position > 0)
                reader.BaseStream.Position--;
            if (c == Convert.ToInt32('\n'))
            {
                ++count;
            }
        }
        string str = reader.ReadToEnd();
        string[] arr = str.Replace("\r", "").Split('\n');
        reader.Close();
        

        【讨论】:

        • 一些经过简短测试的东西,将 reader.Read() 更改为 reader.BaseStream.ReadByte(),同时应该检查 Position>0 和 2nd Position--应该检查 Position>0 .最后,在最后,每个换行符都是 "\r\n" 而不仅仅是 '\n',所以将 Split('\n') 更改为 Replace("\r", "").Split('\n' )。它需要一些微调,但如果您有时间抱怨“不起作用”,请找出问题所在并实际批评它。
        【解决方案9】:

        您可以使用 Windows 版本的 tail 命令,然后将其输出到带有 > 符号的文本文件中,或者根据您的需要在屏幕上查看它。

        【讨论】:

        • 我认为这有点像 Eric Ness 所说的。但有时我真的很喜欢 Linux 命令——针对命令行上的文本操作进行了优化,不,抱歉,终端...
        【解决方案10】:

        这是我的版本。高温

        using (StreamReader sr = new StreamReader(path))
        {
          sr.BaseStream.Seek(0, SeekOrigin.End);
        
          int c;
          int count = 0;
          long pos = -1;
        
          while(count < 10)
          {
            sr.BaseStream.Seek(pos, SeekOrigin.End);
            c = sr.Read();
            sr.DiscardBufferedData();
        
            if(c == Convert.ToInt32('\n'))
              ++count;
            --pos;
          }
        
          sr.BaseStream.Seek(pos, SeekOrigin.End);
          string str = sr.ReadToEnd();
          string[] arr = str.Split('\n');
        }
        

        【讨论】:

        • 如果您的文件少于 10 行,您的代码将会崩溃。改用这个while-sentence while (count &lt; 10 &amp;&amp; -pos &lt; sr.BaseStream.Length)
        【解决方案11】:

        如果您使用 FileMode.Append 打开文件,它将为您查找文件末尾。然后你可以寻找你想要的字节数并读取它们。不管你做什么,它都可能不会很快,因为这是一个相当大的文件。

        【讨论】:

          【解决方案12】:

          一个有用的方法是FileInfo.Length。它以字节为单位给出文件的大小。

          您的文件是什么结构?您确定最后 10 行将在文件末尾附近吗?如果您有一个包含 12 行文本和 10GB 的 0 的文件,那么查看结尾不会真的那么快。再说一遍,您可能需要查看整个文件。

          如果您确定文件包含许多短字符串,每个字符串都在一个新行上,请查找到末尾,然后再检查,直到您计算出 11 行末尾。然后你可以继续阅读接下来的 10 行。

          【讨论】:

            【解决方案13】:

            我认为其他海报都表明没有真正的捷径。

            您可以使用诸如tail(或powershell)之类的工具,也可以编写一些寻找文件结尾然后查找n个换行符的愚蠢代码。

            网络上有很多 tail 的实现 - 查看源代码以了解 他们 是如何做到的。 Tail 非常高效(即使在非常大的文件上),所以他们在编写它时一定是正确的!

            【讨论】:

              【解决方案14】:

              使用 Sisutil 的答案作为起点,您可以逐行读取文件并将它们加载到 Queue&lt;String&gt; 中。它确实从一开始就读取文件,但它具有不尝试向后读取文件的优点。正如 Jon Skeet 指出的那样,如果您有一个具有可变字符宽度编码(如 UTF-8)的文件,这可能会非常困难。它也没有对行长做出任何假设。

              我针对一个 1.7GB 的文件(手头没有 10GB 的文件)对此进行了测试,大约需要 14 秒。当然,在比较计算机之间的加载和读取时间时,通常需要注意一些事项。

              int numberOfLines = 10;
              string fullFilePath = @"C:\Your\Large\File\BigFile.txt";
              var queue = new Queue<string>(numberOfLines);
              
              using (FileStream fs = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) 
              using (BufferedStream bs = new BufferedStream(fs))  // May not make much difference.
              using (StreamReader sr = new StreamReader(bs)) {
                  while (!sr.EndOfStream) {
                      if (queue.Count == numberOfLines) {
                          queue.Dequeue();
                      }
              
                      queue.Enqueue(sr.ReadLine());
                  }
              }
              
              // The queue now has our set of lines. So print to console, save to another file, etc.
              do {
                  Console.WriteLine(queue.Dequeue());
              } while (queue.Count > 0);    
              

              【讨论】:

                【解决方案15】:

                使用 PowerShell,Get-Content big_file_name.txt -Tail 10,其中 10 是要检索的底线数。

                这没有性能问题。我在一个超过 100 GB 的文本文件上运行它并立即获得了结果。

                【讨论】:

                  【解决方案16】:

                  打开文件并开始读取行。读完 10 行后,打开另一个指针,从文件的开头开始,因此第二个指针比第一个指针滞后 10 行。继续阅读,同时移动两个指针,直到第一个指针到达文件末尾。然后使用第二个指针读取结果。它适用于任何大小的文件,包括空文件和短于尾长的文件。而且很容易适应任何长度的尾巴。 当然,缺点是您最终会读取整个文件,而这可能正是您想要避免的。

                  【讨论】:

                  • 如果文件是 10GB,我认为可以肯定地说这正是他试图避免的 :-)
                  【解决方案17】:

                  如果您有一个每行格式相同的文件(例如 daq 系统),您只需使用 streamreader 获取文件的长度,然后取其中一行 (readline())。

                  将总长度除以字符串的长度。现在您有了一个通用的长数字来表示文件中的行数。

                  关键是您在获取数组或其他数据之前使用readline()。这将确保您从新行的开头开始,并且不会从前一行获取任何剩余数据。

                  StreamReader leader = new StreamReader(GetReadFile);
                  leader.BaseStream.Position = 0;
                  StreamReader follower = new StreamReader(GetReadFile);
                  
                  int count = 0;
                  string tmper = null;
                  while (count <= 12)
                  {
                      tmper = leader.ReadLine();
                      count++;
                  }
                  
                  long total = follower.BaseStream.Length; // get total length of file
                  long step = tmper.Length; // get length of 1 line
                  long size = total / step; // divide to get number of lines
                  long go = step * (size - 12); // get the bit location
                  
                  long cut = follower.BaseStream.Seek(go, SeekOrigin.Begin); // Go to that location
                  follower.BaseStream.Position = go;
                  
                  string led = null;
                  string[] lead = null ;
                  List<string[]> samples = new List<string[]>();
                  
                  follower.ReadLine();
                  
                  while (!follower.EndOfStream)
                  {
                      led = follower.ReadLine();
                      lead = Tokenize(led);
                      samples.Add(lead);
                  }
                  

                  【讨论】:

                    【解决方案18】:

                    我也遇到了同样的问题,一个巨大的日志文件,应该通过 REST 接口访问。当然,将其加载到任何内存中并通过 http 将其完整发送是没有解决方案的。

                    正如 Jon 所指出的,这个解决方案有一个非常具体的用例。就我而言,我确定(并检查)编码是 utf-8(带有 BOM!),因此可以从 UTF 的所有好处中受益。这肯定不是通用解决方案。

                    这对我来说非常好和快速(我忘了关闭流 - 现在已修复):

                        private string tail(StreamReader streamReader, long numberOfBytesFromEnd)
                        {
                            Stream stream = streamReader.BaseStream;
                            long length = streamReader.BaseStream.Length;
                            if (length < numberOfBytesFromEnd)
                                numberOfBytesFromEnd = length;
                            stream.Seek(numberOfBytesFromEnd * -1, SeekOrigin.End);
                    
                            int LF = '\n';
                            int CR = '\r';
                            bool found = false;
                    
                            while (!found) {
                                int c = stream.ReadByte();
                                if (c == LF)
                                    found = true;
                            }
                    
                            string readToEnd = streamReader.ReadToEnd();
                            streamReader.Close();
                            return readToEnd;
                        }
                    

                    我们首先使用 BaseStream 寻找接近末尾的某个地方,当我们有正确的流位置时,使用通常的 StreamReader 读取到末尾。

                    这实际上并不允许指定从末尾开始的行数,这无论如何都不是一个好主意,因为行可能会任意长,因此会再次破坏性能。所以我指定字节的数量,读取直到我们到达第一个换行符,然后舒适地读取到最后。 从理论上讲,您也可以查找 CarriageReturn,但在我的情况下,这不是必需的。

                    如果我们使用这段代码,它不会干扰编写器线程:

                            FileStream fileStream = new FileStream(
                                filename,
                                FileMode.Open,
                                FileAccess.Read,
                                FileShare.ReadWrite);
                    
                            StreamReader streamReader = new StreamReader(fileStream);
                    

                    【讨论】:

                    • 请注意,这假定'\n' 将作为字符的单个字节出现,并且它不能以任何其他方式出现。这对于某些编码是可以的,但肯定不是全部。此外,从最后加载“一些行数”(可能为 0)可能对您来说没问题,但这并不是问题中真正要问的。最后,您可能应该调用streamReader.DiscardBufferedData(),这样如果它缓冲了任何内容,它就不会在下一次读取调用时使用该信息,而是查询流。
                    • 感谢您的评论,让我说,我现在完全疯了:我的第一条评论来自 Jon Skeet hinself :-)
                    • 我编辑了答案,希望这样会更好。在我的情况下,答案应该通过 http 传输并显示在浏览器中。所以我真的不想使用行号,因为很多长行可以迅速改变整个情况。通过指定字节数,我总能保证答案很快。哦,男孩这么快。我要做一些测试(在实际工作之后:-))因为我真的很好奇。它似乎优于所有其他解决方案,但这有点牵强。我想知道操作系统到底在做什么......谢谢你让我开心☃
                    【解决方案19】:

                    如果您需要从文本文件中反向读取任意数量的行,您可以使用以下与 LINQ 兼容的类。它侧重于性能和对大文件的支持。您可以阅读几行并调用 Reverse() 以按正序获取最后几行:

                    用法

                    var reader = new ReverseTextReader(@"C:\Temp\ReverseTest.txt");
                    while (!reader.EndOfStream)
                        Console.WriteLine(reader.ReadLine());
                    

                    ReverseTextReader 类

                    /// <summary>
                    /// Reads a text file backwards, line-by-line.
                    /// </summary>
                    /// <remarks>This class uses file seeking to read a text file of any size in reverse order.  This
                    /// is useful for needs such as reading a log file newest-entries first.</remarks>
                    public sealed class ReverseTextReader : IEnumerable<string>
                    {
                        private const int BufferSize = 16384;   // The number of bytes read from the uderlying stream.
                        private readonly Stream _stream;        // Stores the stream feeding data into this reader
                        private readonly Encoding _encoding;    // Stores the encoding used to process the file
                        private byte[] _leftoverBuffer;         // Stores the leftover partial line after processing a buffer
                        private readonly Queue<string> _lines;  // Stores the lines parsed from the buffer
                    
                        #region Constructors
                    
                        /// <summary>
                        /// Creates a reader for the specified file.
                        /// </summary>
                        /// <param name="filePath"></param>
                        public ReverseTextReader(string filePath)
                            : this(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.Default)
                        { }
                    
                        /// <summary>
                        /// Creates a reader using the specified stream.
                        /// </summary>
                        /// <param name="stream"></param>
                        public ReverseTextReader(Stream stream)
                            : this(stream, Encoding.Default)
                        { }
                    
                        /// <summary>
                        /// Creates a reader using the specified path and encoding.
                        /// </summary>
                        /// <param name="filePath"></param>
                        /// <param name="encoding"></param>
                        public ReverseTextReader(string filePath, Encoding encoding)
                            : this(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), encoding)
                        { }
                    
                        /// <summary>
                        /// Creates a reader using the specified stream and encoding.
                        /// </summary>
                        /// <param name="stream"></param>
                        /// <param name="encoding"></param>
                        public ReverseTextReader(Stream stream, Encoding encoding)
                        {          
                            _stream = stream;
                            _encoding = encoding;
                            _lines = new Queue<string>(128);            
                            // The stream needs to support seeking for this to work
                            if(!_stream.CanSeek)
                                throw new InvalidOperationException("The specified stream needs to support seeking to be read backwards.");
                            if (!_stream.CanRead)
                                throw new InvalidOperationException("The specified stream needs to support reading to be read backwards.");
                            // Set the current position to the end of the file
                            _stream.Position = _stream.Length;
                            _leftoverBuffer = new byte[0];
                        }
                    
                        #endregion
                    
                        #region Overrides
                    
                        /// <summary>
                        /// Reads the next previous line from the underlying stream.
                        /// </summary>
                        /// <returns></returns>
                        public string ReadLine()
                        {
                            // Are there lines left to read? If so, return the next one
                            if (_lines.Count != 0) return _lines.Dequeue();
                            // Are we at the beginning of the stream? If so, we're done
                            if (_stream.Position == 0) return null;
                    
                            #region Read and Process the Next Chunk
                    
                            // Remember the current position
                            var currentPosition = _stream.Position;
                            var newPosition = currentPosition - BufferSize;
                            // Are we before the beginning of the stream?
                            if (newPosition < 0) newPosition = 0;
                            // Calculate the buffer size to read
                            var count = (int)(currentPosition - newPosition);
                            // Set the new position
                            _stream.Position = newPosition;
                            // Make a new buffer but append the previous leftovers
                            var buffer = new byte[count + _leftoverBuffer.Length];
                            // Read the next buffer
                            _stream.Read(buffer, 0, count);
                            // Move the position of the stream back
                            _stream.Position = newPosition;
                            // And copy in the leftovers from the last buffer
                            if (_leftoverBuffer.Length != 0)
                                Array.Copy(_leftoverBuffer, 0, buffer, count, _leftoverBuffer.Length);
                            // Look for CrLf delimiters
                            var end = buffer.Length - 1;
                            var start = buffer.Length - 2;
                            // Search backwards for a line feed
                            while (start >= 0)
                            {
                                // Is it a line feed?
                                if (buffer[start] == 10)
                                {
                                    // Yes.  Extract a line and queue it (but exclude the \r\n)
                                    _lines.Enqueue(_encoding.GetString(buffer, start + 1, end - start - 2));
                                    // And reset the end
                                    end = start;
                                }
                                // Move to the previous character
                                start--;
                            }
                            // What's left over is a portion of a line. Save it for later.
                            _leftoverBuffer = new byte[end + 1];
                            Array.Copy(buffer, 0, _leftoverBuffer, 0, end + 1);
                            // Are we at the beginning of the stream?
                            if (_stream.Position == 0)
                                // Yes.  Add the last line.
                                _lines.Enqueue(_encoding.GetString(_leftoverBuffer, 0, end - 1));
                    
                            #endregion
                    
                            // If we have something in the queue, return it
                            return _lines.Count == 0 ? null : _lines.Dequeue();
                        }
                    
                        #endregion
                    
                        #region IEnumerator<string> Interface
                    
                        public IEnumerator<string> GetEnumerator()
                        {
                            string line;
                            // So long as the next line isn't null...
                            while ((line = ReadLine()) != null)
                                // Read and return it.
                                yield return line;
                        }
                    
                        IEnumerator IEnumerable.GetEnumerator()
                        {
                            throw new NotImplementedException();
                        }
                    
                        #endregion
                    }
                    

                    【讨论】:

                      【解决方案20】:

                      前段时间我用这个代码做一个小工具,希望对你有帮助!

                      private string ReadRows(int offset)     /*offset: how many lines it reads from the end (10 in your case)*/
                      {
                          /*no lines to read*/
                          if (offset == 0)
                              return result;
                      
                          using (FileStream fs = new FileStream(FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 2048, true))
                          {
                              List<char> charBuilder = new List<char>(); /*StringBuilder doesn't work with Encoding: example char ? */
                              StringBuilder sb = new StringBuilder();
                      
                              int count = 0;
                      
                              /*tested with utf8 file encoded by notepad-pp; other encoding may not work*/
                      
                              var decoder = ReaderEncoding.GetDecoder();
                              byte[] buffer;
                              int bufferLength;
                      
                              fs.Seek(0, SeekOrigin.End);
                      
                              while (true)
                              {
                                  bufferLength = 1;
                                  buffer = new byte[1];
                      
                                  /*for encoding with variable byte size, every time I read a byte that is part of the character and not an entire character the decoder returns '�' (invalid character) */
                      
                                  char[] chars = { '�' }; //� 65533
                                  int iteration = 0;
                      
                                  while (chars.Contains('�'))
                                  {
                                      /*at every iteration that does not produce character, buffer get bigger, up to 4 byte*/
                                      if (iteration > 0)
                                      {
                                          bufferLength = buffer.Length + 1;
                      
                                          byte[] newBuffer = new byte[bufferLength];
                      
                                          Array.Copy(buffer, newBuffer, bufferLength - 1);
                      
                                          buffer = newBuffer;
                                      }
                      
                                      /*there are no characters with more than 4 bytes in utf-8*/
                                      if (iteration > 4)
                                          throw new Exception();
                      
                      
                                      /*if all is ok, the last seek return IOError with chars = empty*/
                                      try
                                      {
                                          fs.Seek(-(bufferLength), SeekOrigin.Current);
                                      }
                                      catch
                                      {
                                          chars = new char[] { '\0' };
                                          break;
                                      }
                      
                                      fs.Read(buffer, 0, bufferLength);
                      
                                      var charCount = decoder.GetCharCount(buffer, 0, bufferLength);
                                      chars = new char[charCount];
                      
                                      decoder.GetChars(buffer, 0, bufferLength, chars, 0);
                      
                                      ++iteration;
                                  }
                      
                                  /*when i get a char*/
                                  charBuilder.InsertRange(0, chars);
                      
                                  if (chars.Length > 0 && chars[0] == '\n')
                                      ++count;
                      
                                  /*exit when i get the correctly number of line (*last row is in interval)*/
                                  if (count == offset + 1)
                                      break;
                      
                                  /*the first search goes back, the reading goes on then we come back again, except the last */
                                  try
                                  {
                                      fs.Seek(-(bufferLength), SeekOrigin.Current);
                                  }
                                  catch (Exception)
                                  {
                                      break;
                                  }
                      
                              }
                          }
                      
                          /*everithing must be reversed, but not \0*/
                          charBuilder.RemoveAt(0);
                      
                          /*yuppi!*/
                          return new string(charBuilder.ToArray());
                      }
                      

                      我为速度附加了一个屏幕

                      【讨论】:

                        【解决方案21】:

                        为什么不使用返回字符串[]的file.readalllines?

                        然后您可以获得最后 10 行(或数组的成员),这将是一项微不足道的任务。

                        这种方法没有考虑任何编码问题,我不确定这种方法的确切效率(完成方法所花费的时间等)。

                        【讨论】:

                        • 在给出答案之前请阅读问题!这种方法会花费 FAR 太多时间。
                        • 你在这里留下了相当不错的足迹!我希望你现在是更好的程序员! ;-)
                        猜你喜欢
                        • 1970-01-01
                        • 2014-10-22
                        • 1970-01-01
                        • 2023-01-11
                        • 2013-08-29
                        • 2013-10-03
                        • 2019-04-30
                        • 1970-01-01
                        • 1970-01-01
                        相关资源
                        最近更新 更多