我正在尝试读取中给定的文件NSURL并将其加载到数组中,其中各项之间用换行符分隔\n。
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,这同样是不好的。
我已经考虑过在单独的线程中运行此代码,但是我一直在遇到麻烦,此外,它仍然不能解决处理巨大字符串的问题。
我想做的事情与以下伪代码类似:
var aStreamReader = new StreamReader(from_file_or_url) while aStreamReader.hasNextLine == true { currentline = aStreamReader.nextLine() list.addItem(currentline) }
我将如何在Swift中完成此任务?
关于我正在读取的文件的一些注意事项: 所有文件都由短字符串(<255个字符)组成,用\n或分隔\r\n。文件的长度从大约100行到超过5000万行。它们可能包含欧洲字符和/或带有重音符号的字符。
\r\n
(该代码现在适用于Swift 2.2 / Xcode 7.3。如果有人需要,可以在编辑历史记录中找到旧版本。最后提供了Swift 3的更新版本。)
以下Swift代码从如何逐行从NSFileHandle中读取数据的各种答案中获得了很大的启发 。它从块中读取文件,并将完整的行转换为字符串。
\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):
SequenceType
extension StreamReader : SequenceType { func generate() -> AnyGenerator<String> { return AnyGenerator { return self.nextLine() } } }
Swift 3 / Xcode 8 beta 6更新: 还要“现代化”使用guard和新 Data值类型:
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() } } }