在 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 <stdio.h>
。
// 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<CChar>
演员表。最后,您的使用示例无法编译。【参考方案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<String>
,其中集合既不为空也不包含空字符串,而不是单个字符串。
在我去年的第一次尝试中,我尝试做“正确的事情”并搜索一组通用的字符串。太难了;你需要一个成熟的解析器和状态机等。我放弃了它和它所属的项目。
现在我又在做这个项目,再次面临同样的挑战。现在我将在 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的主要内容,如果未能解决你的问题,请参考以下文章