在 Swift 中逐行读取文件/URL

Posted

技术标签:

【中文标题】在 Swift 中逐行读取文件/URL【英文标题】:Read a file/URL line-by-line in Swift 【发布时间】: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 是否有足够的内存来映射文件并以这种方式使用它?多个任务很容易完成,我想你可以有多个工作在不同的地方开始读取文件。 【参考方案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 读取。比较***.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)

【讨论】:

以上是关于在 Swift 中逐行读取文件/URL的主要内容,如果未能解决你的问题,请参考以下文章

在Swift中逐行读取文本文件?

如何在Shell脚本中逐行读取文件

在 Go 中逐行读取文件

在 C# 中逐行读取文件

如何在 Julia 中逐行读取文件?

在 VBA 中逐行读取/解析文本文件