【问题标题】:Read a file/URL line-by-line in Swift在 Swift 中逐行读取文件/URL
【发布时间】:2014-08-26 05:15:45
【问题描述】:

我正在尝试读取NSURL 中给出的文件并将其加载到数组中,其中项目由换行符\n 分隔。

这是我目前的做法:

var possList: NSString? = NSString.stringWithContentsOfURL(filePath.URL) as? NSString
if var list = possList {
    list = list.componentsSeparatedByString("\n") as NSString[]
    return list
}
else {
    //return empty list
}

出于几个原因,我对此不太满意。一,我正在处理大小从几千字节到数百 MB 的文件。可以想象,使用这么大的字符串既慢又笨重。其次,这会在 UI 执行时冻结 UI——再一次,不好。

我曾考虑在单独的线程中运行此代码,但我遇到了麻烦,此外,它仍然不能解决处理大字符串的问题。

我想做的是类似于以下伪代码:

var aStreamReader = new StreamReader(from_file_or_url)
while aStreamReader.hasNextLine == true {
    currentline = aStreamReader.nextLine()
    list.addItem(currentline)
}

我将如何在 Swift 中实现这一点?

关于我正在读取的文件的几点说明: 所有文件都包含由\n\r\n 分隔的短字符串(

【问题讨论】:

  • 您是想在执行过程中将阵列写入磁盘,还是让操作系统使用内存来处理它?运行它的 Mac 是否有足够的内存来映射文件并以这种方式使用它?多个任务很容易完成,我想你可以有多个工作在不同的地方开始读取文件。

标签: macos file input stream swift


【解决方案1】:

(代码现在适用于 Swift 2.2/Xcode 7.3。如果有人需要,可以在编辑历史记录中找到旧版本。最后提供了 Swift 3 的更新版本。)

以下 Swift 代码深受以下问题的各种答案的启发 How to read data from NSFileHandle line by line?。它以块的形式从文件中读取,并将完整的行转换为字符串。

默认行分隔符 (\n)、字符串编码 (UTF-8) 和块大小 (4096) 可以使用可选参数进行设置。

class StreamReader  {

    let encoding : UInt
    let chunkSize : Int

    var fileHandle : NSFileHandle!
    let buffer : NSMutableData!
    let delimData : NSData!
    var atEof : Bool = false

    init?(path: String, delimiter: String = "\n", encoding : UInt = NSUTF8StringEncoding, chunkSize : Int = 4096) {
        self.chunkSize = chunkSize
        self.encoding = encoding

        if let fileHandle = NSFileHandle(forReadingAtPath: path),
            delimData = delimiter.dataUsingEncoding(encoding),
            buffer = NSMutableData(capacity: chunkSize)
        {
            self.fileHandle = fileHandle
            self.delimData = delimData
            self.buffer = buffer
        } else {
            self.fileHandle = nil
            self.delimData = nil
            self.buffer = nil
            return nil
        }
    }

    deinit {
        self.close()
    }

    /// Return next line, or nil on EOF.
    func nextLine() -> String? {
        precondition(fileHandle != nil, "Attempt to read from closed file")

        if atEof {
            return nil
        }

        // Read data chunks from file until a line delimiter is found:
        var range = buffer.rangeOfData(delimData, options: [], range: NSMakeRange(0, buffer.length))
        while range.location == NSNotFound {
            let tmpData = fileHandle.readDataOfLength(chunkSize)
            if tmpData.length == 0 {
                // EOF or read error.
                atEof = true
                if buffer.length > 0 {
                    // Buffer contains last line in file (not terminated by delimiter).
                    let line = NSString(data: buffer, encoding: encoding)

                    buffer.length = 0
                    return line as String?
                }
                // No more lines.
                return nil
            }
            buffer.appendData(tmpData)
            range = buffer.rangeOfData(delimData, options: [], range: NSMakeRange(0, buffer.length))
        }

        // Convert complete line (excluding the delimiter) to a string:
        let line = NSString(data: buffer.subdataWithRange(NSMakeRange(0, range.location)),
            encoding: encoding)
        // Remove line (and the delimiter) from the buffer:
        buffer.replaceBytesInRange(NSMakeRange(0, range.location + range.length), withBytes: nil, length: 0)

        return line as String?
    }

    /// Start reading from the beginning of file.
    func rewind() -> Void {
        fileHandle.seekToFileOffset(0)
        buffer.length = 0
        atEof = false
    }

    /// Close the underlying file. No reading must be done after calling this method.
    func close() -> Void {
        fileHandle?.closeFile()
        fileHandle = nil
    }
}

用法:

if let aStreamReader = StreamReader(path: "/path/to/file") {
    defer {
        aStreamReader.close()
    }
    while let line = aStreamReader.nextLine() {
        print(line)
    }
}

您甚至可以将阅读器与 for-in 循环一起使用

for line in aStreamReader {
    print(line)
}

通过实现SequenceType 协议(比较http://robots.thoughtbot.com/swift-sequences):

extension StreamReader : SequenceType {
    func generate() -> AnyGenerator<String> {
        return AnyGenerator {
            return self.nextLine()
        }
    }
}

Swift 3/Xcode 8 beta 6 的更新:也“现代化”到 使用guard 和新的Data 值类型:

class StreamReader  {

    let encoding : String.Encoding
    let chunkSize : Int
    var fileHandle : FileHandle!
    let delimData : Data
    var buffer : Data
    var atEof : Bool

    init?(path: String, delimiter: String = "\n", encoding: String.Encoding = .utf8,
          chunkSize: Int = 4096) {

        guard let fileHandle = FileHandle(forReadingAtPath: path),
            let delimData = delimiter.data(using: encoding) else {
                return nil
        }
        self.encoding = encoding
        self.chunkSize = chunkSize
        self.fileHandle = fileHandle
        self.delimData = delimData
        self.buffer = Data(capacity: chunkSize)
        self.atEof = false
    }

    deinit {
        self.close()
    }

    /// Return next line, or nil on EOF.
    func nextLine() -> String? {
        precondition(fileHandle != nil, "Attempt to read from closed file")

        // Read data chunks from file until a line delimiter is found:
        while !atEof {
            if let range = buffer.range(of: delimData) {
                // Convert complete line (excluding the delimiter) to a string:
                let line = String(data: buffer.subdata(in: 0..<range.lowerBound), encoding: encoding)
                // Remove line (and the delimiter) from the buffer:
                buffer.removeSubrange(0..<range.upperBound)
                return line
            }
            let tmpData = fileHandle.readData(ofLength: chunkSize)
            if tmpData.count > 0 {
                buffer.append(tmpData)
            } else {
                // EOF or read error.
                atEof = true
                if buffer.count > 0 {
                    // Buffer contains last line in file (not terminated by delimiter).
                    let line = String(data: buffer as Data, encoding: encoding)
                    buffer.count = 0
                    return line
                }
            }
        }
        return nil
    }

    /// Start reading from the beginning of file.
    func rewind() -> Void {
        fileHandle.seek(toFileOffset: 0)
        buffer.count = 0
        atEof = false
    }

    /// Close the underlying file. No reading must be done after calling this method.
    func close() -> Void {
        fileHandle?.closeFile()
        fileHandle = nil
    }
}

extension StreamReader : Sequence {
    func makeIterator() -> AnyIterator<String> {
        return AnyIterator {
            return self.nextLine()
        }
    }
}

【讨论】:

  • @Matt:没关系。您可以将扩展名放在与“主类”相同的 Swift 文件中,也可以放在单独的文件中。 - 实际上你并不需要扩展。您可以将generate() 函数添加到StreamReader 类并将其声明为class StreamReader : Sequence { ... }。但是,为单独的功能块使用扩展似乎是一种很好的 Swift 风格。
  • @zanzoken:你使用的是什么类型的 URL?上述代码仅适用于 file URL。它不能用于从通用服务器 URL 读取。比较stackoverflow.com/questions/26674182/…和我的问题下的cmets。
  • @zanzoken:我的代码适用于文本文件,并希望文件使用指定的编码(默认为UTF-8)。如果您有一个包含任意二进制字节的文件(例如图像文件),则数据->字符串转换将失败。
  • @zanzoken:从图像中读取扫描线是一个完全不同的主题,与此代码无关,抱歉。我确信它可以通过 CoreGraphics 方法来完成,但我没有立即为您提供参考。
  • @DCDC while !aStreamReader.atEof { try autoreleasepool { guard let line = aStreamReader.nextLine() else { return } ...code... } }
【解决方案2】:

高效便捷的逐行读取文本文件类(Swift 4、Swift 5)

注意:此代码与平台无关(macOS、iOS、ubuntu)

import Foundation

/// Read text file line by line in efficient way
public class LineReader {
   public let path: String

   fileprivate let file: UnsafeMutablePointer<FILE>!

   init?(path: String) {
      self.path = path
      file = fopen(path, "r")
      guard file != nil else { return nil }
   }

   public var nextLine: String? {
      var line:UnsafeMutablePointer<CChar>? = nil
      var linecap:Int = 0
      defer { free(line) }
      return getline(&line, &linecap, file) > 0 ? String(cString: line!) : nil
   }

   deinit {
      fclose(file)
   }
}

extension LineReader: Sequence {
   public func  makeIterator() -> AnyIterator<String> {
      return AnyIterator<String> {
         return self.nextLine
      }
   }
}

用法:

guard let reader = LineReader(path: "/Path/to/file.txt") else {
    return; // cannot open file
}

for line in reader {
    print(">" + line.trimmingCharacters(in: .whitespacesAndNewlines))      
}

Repository on github

【讨论】:

    【解决方案3】:

    Swift 4.2安全语法

    class LineReader {
    
        let path: String
    
        init?(path: String) {
            self.path = path
            guard let file = fopen(path, "r") else {
                return nil
            }
            self.file = file
        }
        deinit {
            fclose(file)
        }
    
        var nextLine: String? {
            var line: UnsafeMutablePointer<CChar>?
            var linecap = 0
            defer {
                free(line)
            }
            let status = getline(&line, &linecap, file)
            guard status > 0, let unwrappedLine = line else {
                return nil
            }
            return String(cString: unwrappedLine)
        }
    
        private let file: UnsafeMutablePointer<FILE>
    }
    
    extension LineReader: Sequence {
        func makeIterator() -> AnyIterator<String> {
            return AnyIterator<String> {
                return self.nextLine
            }
        }
    }
    

    用法:

    guard let reader = LineReader(path: "/Path/to/file.txt") else {
        return
    }
    reader.forEach { line in
        print(line.trimmingCharacters(in: .whitespacesAndNewlines))      
    }
    

    【讨论】:

      【解决方案4】:

      我迟到了,但这是我为此编写的小班课程。经过一些不同的尝试(尝试继承NSInputStream),我发现这是一种合理且简单的方法。

      记住在你的桥接头中#import &lt;stdio.h&gt;

      // Use is like this:
      let readLine = ReadLine(somePath)
      while let line = readLine.readLine() {
          // do something...
      }
      
      class ReadLine {
      
          private var buf = UnsafeMutablePointer<Int8>.alloc(1024)
          private var n: Int = 1024
      
          let path: String
          let mode: String = "r"
      
          private lazy var filepointer: UnsafeMutablePointer<FILE> = {
              let csmode = self.mode.withCString { cs in return cs }
              let cspath = self.path.withCString { cs in return cs }
      
              return fopen(cspath, csmode)
          }()
      
          init(path: String) {
              self.path = path
          }
      
          func readline() -> String? {
              // unsafe for unknown input
              if getline(&buf, &n, filepointer) > 0 {
                  return String.fromCString(UnsafePointer<CChar>(buf))
              }
      
              return nil
          }
      
          deinit {
              buf.dealloc(n)
              fclose(filepointer)
          }
      }
      

      【讨论】:

      • 我喜欢这个,但它仍然可以改进。没有必要使用withCString 创建指针(实际上非​​常不安全),您可以简单地调用return fopen(self.path, self.mode)。可能会添加一个检查文件是否真的可以打开,目前readline() 只会崩溃。不需要UnsafePointer&lt;CChar&gt; 演员表。最后,您的使用示例无法编译。
      【解决方案5】:

      此函数接受一个文件 URL 并返回一个序列,该序列将返回文件的每一行,懒惰地读取它们。它适用于 Swift 5。它依赖于底层的getline

      typealias LineState = (
        // pointer to a C string representing a line
        linePtr:UnsafeMutablePointer<CChar>?,
        linecap:Int,
        filePtr:UnsafeMutablePointer<FILE>?
      )
      
      /// Returns a sequence which iterates through all lines of the the file at the URL.
      ///
      /// - Parameter url: file URL of a file to read
      /// - Returns: a Sequence which lazily iterates through lines of the file
      ///
      /// - warning: the caller of this function **must** iterate through all lines of the file, since aborting iteration midway will leak memory and a file pointer
      /// - precondition: the file must be UTF8-encoded (which includes, ASCII-encoded)
      func lines(ofFile url:URL) -> UnfoldSequence<String,LineState>
      {
        let initialState:LineState = (linePtr:nil, linecap:0, filePtr:fopen(url.path,"r"))
        return sequence(state: initialState, next: { (state) -> String? in
          if getline(&state.linePtr, &state.linecap, state.filePtr) > 0,
            let theLine = state.linePtr  {
            return String.init(cString:theLine)
          }
          else {
            if let actualLine = state.linePtr  { free(actualLine) }
            fclose(state.filePtr)
            return nil
          }
        })
      }
      

      例如,您可以使用它来打印应用程序包中名为“foo”的文件的每一行:

      let url = NSBundle.mainBundle().urlForResource("foo", ofType: nil)!
      for line in lines(ofFile:url) {
        // suppress print's automatically inserted line ending, since
        // lineGenerator captures each line's own new line character.
        print(line, separator: "", terminator: "")
      }
      

      我通过修改 Alex Brown 的答案以消除 Martin R 的评论中提到的内存泄漏,并将其更新为 Swift 5 来​​开发此答案。

      【讨论】:

        【解决方案6】:

        尝试this 回答,或阅读Mac OS Stream Programming Guide

        不过,您可能会发现使用 stringWithContentsOfURL 的性能实际上会更好,因为使用基于内存(或内存映射)的数据比使用基于磁盘的数据更快。

        在另一个线程上执行它是有据可查的,例如here

        更新

        如果您不想一次阅读所有内容,并且不想使用 NSStreams,那么您可能不得不使用 C 级文件 I/O。有许多理由不这样做 - 阻塞、字符编码、处理 I/O 错误、速度等等 - 这就是 Foundation 库的用途。我在下面草绘了一个简单的答案,它只处理 ACSII 数据:

        class StreamReader {
        
            var eofReached = false
            let fileHandle: UnsafePointer<FILE>
        
            init (path: String) {
                self.fileHandle = fopen(path.bridgeToObjectiveC().UTF8String, "rb".bridgeToObjectiveC().UTF8String)
            }
        
            deinit {
                fclose(self.fileHandle)
            }
        
            func nextLine() -> String {
                var nextChar: UInt8 = 0
                var stringSoFar = ""
                var eolReached = false
                while (self.eofReached == false) && (eolReached == false) {
                    if fread(&nextChar, 1, 1, self.fileHandle) == 1 {
                        switch nextChar & 0xFF {
                        case 13, 10 : // CR, LF
                            eolReached = true
                        case 0...127 : // Keep it in ASCII
                            stringSoFar += NSString(bytes:&nextChar, length:1, encoding: NSASCIIStringEncoding)
                        default :
                            stringSoFar += "<\(nextChar)>"
                        }
                    } else { // EOF or error
                        self.eofReached = true
                    }
                }
                return stringSoFar
            }
        }
        
        // OP's original request follows:
        var aStreamReader = StreamReader(path: "~/Desktop/Test.text".stringByStandardizingPath)
        
        while aStreamReader.eofReached == false { // Changed property name for more accurate meaning
            let currentline = aStreamReader.nextLine()
            //list.addItem(currentline)
            println(currentline)
        }
        

        【讨论】:

        • 感谢您的建议,但我特意在 Swift 中寻找代码。此外,我想一次处理一行,而不是一次处理所有行。
        • 那么您是否希望使用一行然后释放它并阅读下一行?我需要认为在内存中使用它会更快。它们需要按顺序处理吗?如果没有,您可以使用枚举块来显着加快数组的处理速度。
        • 我想一次抓取多行,但我不一定需要加载所有行。至于是否有序,这并不重要,但会有所帮助。
        • 如果将case 0...127 扩展到非ASCII 字符会怎样?
        • 这真的取决于你文件中的字符编码。如果它们是 Unicode 的多种格式之一,则需要为此编码,如果它们是许多 Unicode 之前的 PC“代码页”系统之一,则需要对其进行解码。 Foundation 库会为您完成所有这些工作,这需要您自己完成很多工作。
        【解决方案7】:

        事实证明,一旦您了解 UnsafePointer,好的老式 C API 在 Swift 中会非常舒适。这是一个简单的猫,它从标准输入读取并逐行打印到标准输出。你甚至不需要基金会。达尔文就够了:

        import Darwin
        let bufsize = 4096
        // let stdin = fdopen(STDIN_FILENO, "r") it is now predefined in Darwin
        var buf = UnsafePointer<Int8>.alloc(bufsize)
        while fgets(buf, Int32(bufsize-1), stdin) {
            print(String.fromCString(CString(buf)))
        }
        buf.destroy()
        

        【讨论】:

        • 根本无法处理“按行”。它将输入数据blits 输出,并且不识别普通字符和行尾字符之间的差异。显然,输出包含与输入相同的行,但这是因为换行符也被 blitted。
        • @AlexBrown:这不是真的。 fgets() 读取最多(包括)换行符(或 EOF)的字符。还是我误解了你的评论?
        • @Martin R,请问这在 Swift 4/5 中看起来如何?我需要这么简单的东西来逐行读取文件——
        【解决方案8】:

        或者你可以简单地使用Generator:

        let stdinByLine = GeneratorOf({ () -> String? in
            var input = UnsafeMutablePointer<Int8>(), lim = 0
            return getline(&input, &lim, stdin) > 0 ? String.fromCString(input) : nil
        })
        

        我们来试试吧

        for line in stdinByLine {
            println(">>> \(line)")
        }
        

        它简单、懒惰且易于与枚举器和函子(如 map、reduce、filter)等其他 swift 事物链接;使用 lazy() 包装器。


        它概括为所有FILE

        let byLine = { (file:UnsafeMutablePointer<FILE>) in
            GeneratorOf({ () -> String? in
                var input = UnsafeMutablePointer<Int8>(), lim = 0
                return getline(&input, &lim, file) > 0 ? String.fromCString(input) : nil
            })
        }
        

        像这样称呼

        for line in byLine(stdin) { ... }
        

        【讨论】:

        • 非常感谢一个现在已经离开的答案,它给了我 getline 代码!
        • 显然我完全忽略了编码。留给读者作为练习。
        • 请注意,您的代码会泄漏内存,因为getline() 为数据分配了一个缓冲区。
        【解决方案9】:

        跟进@dankogai 的answer,我对Swift 4+ 做了一些修改,

            let bufsize = 4096
            let fp = fopen(jsonURL.path, "r");
            var buf = UnsafeMutablePointer<Int8>.allocate(capacity: bufsize)
        
            while (fgets(buf, Int32(bufsize-1), fp) != nil) {
                print( String(cString: buf) )
             }
            buf.deallocate()
        

        这对我有用。

        谢谢

        【讨论】:

          【解决方案10】:

          (注意:我在 Xcode 8.2.1 和 macOS Sierra 10.12.3 上使用 Swift 3.0.1)

          我在这里看到的所有答案都错过了他可能正在寻找 LF 或 CRLF。如果一切顺利,他/她可以在 LF 上进行匹配并检查返回的字符串是否在末尾有额外的 CR。但一般查询涉及多个搜索字符串。换句话说,分隔符必须是Set&lt;String&gt;,其中集合既不为空也不包含空字符串,而不是单个字符串。

          在我去年的第一次尝试中,我尝试做“正确的事情”并搜索一组通用的字符串。太难了;你需要一个成熟的解析器和状态机等。我放弃了它和它所属的项目。

          现在我又在做这个项目,再次面临同样的挑战。现在我将在 CR 和 LF 上进行硬编码搜索。我认为没有人需要在 CR/LF 解析之外搜索像这样的两个半独立和半依赖字符。

          我使用的是Data提供的搜索方法,所以我这里不做字符串编码之类的。只是原始的二进制处理。假设我在这里有一个 ASCII 超集,例如 ISO Latin-1 或 UTF-8。您可以在下一个更高的层处理字符串编码,并且您可以决定附加辅助代码点的 CR/LF 是否仍算作 CR 或 LF。

          算法:从当前字节偏移量继续搜索下一个 CR 下一个 LF。

          • 如果两者都没有找到,则认为下一个数据字符串是从当前偏移量到数据结尾。请注意,终止符长度为 0。将其标记为阅读循环的结尾。
          • 如果先找到 LF,或者只找到 LF,则认为下一个数据字符串是从当前偏移量到 LF 的位置。请注意,终止符长度为 1。将偏移量移到 LF 之后。
          • 如果只找到一个 CR,请像 LF 的情况一样(只是使用不同的字节值)。
          • 否则,我们得到一个 CR 后跟一个 LF。
            • 如果两者相邻,则像 LF 情况一样处理,除了终止符长度为 2。
            • 如果它们之间有一个字节,并且该字节也是 CR,那么我们会遇到“Windows 开发人员在文本模式下编写了二进制文件\r\n,给出了\r\r\n”的问题。也像 LF 情况一样处理它,除了终止符长度为 3。
            • 否则 CR 和 LF 不连接,像 just-CR 一样处理。

          这里有一些代码:

          struct DataInternetLineIterator: IteratorProtocol {
          
              /// Descriptor of the location of a line
              typealias LineLocation = (offset: Int, length: Int, terminatorLength: Int)
          
              /// Carriage return.
              static let cr: UInt8 = 13
              /// Carriage return as data.
              static let crData = Data(repeating: cr, count: 1)
              /// Line feed.
              static let lf: UInt8 = 10
              /// Line feed as data.
              static let lfData = Data(repeating: lf, count: 1)
          
              /// The data to traverse.
              let data: Data
              /// The byte offset to search from for the next line.
              private var lineStartOffset: Int = 0
          
              /// Initialize with the data to read over.
              init(data: Data) {
                  self.data = data
              }
          
              mutating func next() -> LineLocation? {
                  guard self.data.count - self.lineStartOffset > 0 else { return nil }
          
                  let nextCR = self.data.range(of: DataInternetLineIterator.crData, options: [], in: lineStartOffset..<self.data.count)?.lowerBound
                  let nextLF = self.data.range(of: DataInternetLineIterator.lfData, options: [], in: lineStartOffset..<self.data.count)?.lowerBound
                  var location: LineLocation = (self.lineStartOffset, -self.lineStartOffset, 0)
                  let lineEndOffset: Int
                  switch (nextCR, nextLF) {
                  case (nil, nil):
                      lineEndOffset = self.data.count
                  case (nil, let offsetLf):
                      lineEndOffset = offsetLf!
                      location.terminatorLength = 1
                  case (let offsetCr, nil):
                      lineEndOffset = offsetCr!
                      location.terminatorLength = 1
                  default:
                      lineEndOffset = min(nextLF!, nextCR!)
                      if nextLF! < nextCR! {
                          location.terminatorLength = 1
                      } else {
                          switch nextLF! - nextCR! {
                          case 2 where self.data[nextCR! + 1] == DataInternetLineIterator.cr:
                              location.terminatorLength += 1  // CR-CRLF
                              fallthrough
                          case 1:
                              location.terminatorLength += 1  // CRLF
                              fallthrough
                          default:
                              location.terminatorLength += 1  // CR-only
                          }
                      }
                  }
                  self.lineStartOffset = lineEndOffset + location.terminatorLength
                  location.length += self.lineStartOffset
                  return location
              }
          
          }
          

          当然,如果您的Data 块的长度至少是 1 GB 的很大一部分,那么只要当前字节偏移量不再存在 CR 或 LF,您就会受到打击;在每次迭代中总是无果而终地搜索直到结束。分块读取数据会有所帮助:

          struct DataBlockIterator: IteratorProtocol {
          
              /// The data to traverse.
              let data: Data
              /// The offset into the data to read the next block from.
              private(set) var blockOffset = 0
              /// The number of bytes remaining.  Kept so the last block is the right size if it's short.
              private(set) var bytesRemaining: Int
              /// The size of each block (except possibly the last).
              let blockSize: Int
          
              /// Initialize with the data to read over and the chunk size.
              init(data: Data, blockSize: Int) {
                  precondition(blockSize > 0)
          
                  self.data = data
                  self.bytesRemaining = data.count
                  self.blockSize = blockSize
              }
          
              mutating func next() -> Data? {
                  guard bytesRemaining > 0 else { return nil }
                  defer { blockOffset += blockSize ; bytesRemaining -= blockSize }
          
                  return data.subdata(in: blockOffset..<(blockOffset + min(bytesRemaining, blockSize)))
              }
          
          }
          

          您必须自己将这些想法混合在一起,因为我还没有这样做。考虑:

          • 当然,您必须考虑完全包含在块中的行。
          • 但您必须处理行尾位于相邻块中的情况。
          • 或者当端点之间至少有一个块时
          • 最复杂的情​​况是行以多字节序列结尾,但该序列跨越两个块! (以 CR 结尾的行也是块中的最后一个字节是等价的情况,因为您需要读取下一个块以查看您的 just-CR 实际上是 CRLF 还是 CR-CRLF。当块以 CR-CR 结尾。)
          • 当您的当前偏移量不再有终止符时,您需要进行处理,但数据末尾位于稍后的块中。

          祝你好运!

          【讨论】:

            【解决方案11】:

            我想要一个不会不断修改缓冲区或重复代码的版本,因为两者效率低下,并且允许任何大小的缓冲区(包括 1 个字节)和任何分隔符。它有一个公共方法:readline()。调用此方法将返回下一行的 String 值或在 EOF 处返回 nil。

            import Foundation
            
            // LineStream(): path: String, [buffSize: Int], [delim: String] -> nil | String
            // ============= --------------------------------------------------------------
            // path:     the path to a text file to be parsed
            // buffSize: an optional buffer size, (1...); default is 4096
            // delim:    an optional delimiter String; default is "\n"
            // ***************************************************************************
            class LineStream {
                let path: String
                let handle: NSFileHandle!
            
                let delim: NSData!
                let encoding: NSStringEncoding
            
                var buffer = NSData()
                var buffSize: Int
            
                var buffIndex = 0
                var buffEndIndex = 0
            
                init?(path: String,
                  buffSize: Int = 4096,
                  delim: String = "\n",
                  encoding: NSStringEncoding = NSUTF8StringEncoding)
                {
                  self.handle = NSFileHandle(forReadingAtPath: path)
                  self.path = path
                  self.buffSize = buffSize < 1 ? 1 : buffSize
                  self.encoding = encoding
                  self.delim = delim.dataUsingEncoding(encoding)
                  if handle == nil || self.delim == nil {
                    print("ERROR initializing LineStream") /* TODO use STDERR */
                    return nil
                  }
                }
            
              // PRIVATE
              // fillBuffer(): _ -> Int [0...buffSize]
              // ============= -------- ..............
              // Fill the buffer with new data; return with the buffer size, or zero
              // upon reaching end-of-file
              // *********************************************************************
              private func fillBuffer() -> Int {
                buffer = handle.readDataOfLength(buffSize)
                buffIndex = 0
                buffEndIndex = buffer.length
            
                return buffEndIndex
              }
            
              // PRIVATE
              // delimLocation(): _ -> Int? nil | [1...buffSize]
              // ================ --------- ....................
              // Search the remaining buffer for a delimiter; return with the location
              // of a delimiter in the buffer, or nil if one is not found.
              // ***********************************************************************
              private func delimLocation() -> Int? {
                let searchRange = NSMakeRange(buffIndex, buffEndIndex - buffIndex)
                let rangeToDelim = buffer.rangeOfData(delim,
                                                      options: [], range: searchRange)
                return rangeToDelim.location == NSNotFound
                    ? nil
                    : rangeToDelim.location
              }
            
              // PRIVATE
              // dataStrValue(): NSData -> String ("" | String)
              // =============== ---------------- .............
              // Attempt to convert data into a String value using the supplied encoding; 
              // return the String value or empty string if the conversion fails.
              // ***********************************************************************
                private func dataStrValue(data: NSData) -> String? {
                  if let strVal = NSString(data: data, encoding: encoding) as? String {
                      return strVal
                  } else { return "" }
            }
            
              // PUBLIC
              // readLine(): _ -> String? nil | String
              // =========== ____________ ............
              // Read the next line of the file, i.e., up to the next delimiter or end-of-
              // file, whichever occurs first; return the String value of the data found, 
              // or nil upon reaching end-of-file.
              // *************************************************************************
              func readLine() -> String? {
                guard let line = NSMutableData(capacity: buffSize) else {
                    print("ERROR setting line")
                    exit(EXIT_FAILURE)
                }
            
                // Loop until a delimiter is found, or end-of-file is reached
                var delimFound = false
                while !delimFound {
                    // buffIndex will equal buffEndIndex in three situations, resulting
                    // in a (re)filling of the buffer:
                    //   1. Upon the initial call;
                    //   2. If a search for a delimiter has failed
                    //   3. If a delimiter is found at the end of the buffer
                    if buffIndex == buffEndIndex {
                        if fillBuffer() == 0 {
                            return nil
                        }
                    }
            
                    var lengthToDelim: Int
                    let startIndex = buffIndex
            
                    // Find a length of data to place into the line buffer to be
                    // returned; reset buffIndex
                    if let delim = delimLocation() {
                        // SOME VALUE when a delimiter is found; append that amount of
                        // data onto the line buffer,and then return the line buffer
                        delimFound = true
                        lengthToDelim = delim - buffIndex
                        buffIndex = delim + 1   // will trigger a refill if at the end
                                                // of the buffer on the next call, but
                                                // first the line will be returned
                    } else {
                        // NIL if no delimiter left in the buffer; append the rest of
                        // the buffer onto the line buffer, refill the buffer, and
                        // continue looking
                        lengthToDelim = buffEndIndex - buffIndex
                        buffIndex = buffEndIndex    // will trigger a refill of buffer
                                                    // on the next loop
                    }
            
                    line.appendData(buffer.subdataWithRange(
                        NSMakeRange(startIndex, lengthToDelim)))
                }
            
                return dataStrValue(line)
              }
            }
            

            如下调用:

            guard let myStream = LineStream(path: "/path/to/file.txt")
            else { exit(EXIT_FAILURE) }
            
            while let s = myStream.readLine() {
              print(s)
            }
            

            【讨论】:

              猜你喜欢
              • 2015-10-25
              • 2022-01-25
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-06-15
              • 2010-12-24
              相关资源
              最近更新 更多