Чтение файла / URL-адреса построчно в Swift
Я пытаюсь прочитать файл в 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
}
Я не очень доволен этим по нескольким причинам. Во-первых, я работаю с файлами размером от нескольких килобайт до сотен МБ. Как вы можете себе представить, работа со струнами такого размера является медленной и громоздкой. Во-вторых, это замораживает пользовательский интерфейс, когда он исполнение-опять же, не очень хорошо.
Я изучил запуск этого кода в отдельном потоке, но у меня были проблемы с этим, и кроме того, он все еще не решает проблему работы с огромными строками.
то, что я хотел бы сделать, это что-то вроде следующего псевдокода:
var aStreamReader = new StreamReader(from_file_or_url)
while aStreamReader.hasNextLine == true {
currentline = aStreamReader.nextLine()
list.addItem(currentline)
}
как бы я сделал это в Swift?
несколько заметок о файлах, которые я читаю: все файлы состоят коротких (n или rn. Длина файлов колеблется от ~100 строк до более чем 50 миллионов строк. Они могут содержать европейские символы и / или символы с акцентами.
9 ответов:
(код теперь для 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):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() } } }
я завернул код из ответа algal в удобный класс (Swift 4.0)
UPD: этот код не зависит от платформы (macOS, iOS, ubuntu)
import Foundation /// Read text file line by line 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)) }
Я опаздываю на игру, но вот небольшой класс я написал для этой цели. После нескольких различных попыток (попробуйте подкласс
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) } }
оказывается, старый добрый C API довольно удобен в Swift, как только вы Грок UnsafePointer. Вот простой кот, который читает из stdin и печатает в stdout построчно. Вам даже не нужен фундамент. Достаточно Дарвина:
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()
эта функция принимает поток файлов и возвращает
AnyGeneratorэто возвращает каждую строку файла:func lineGenerator(file:UnsafeMutablePointer<FILE>) -> AnyGenerator<String> { return AnyGenerator { () -> String? in var line:UnsafeMutablePointer<CChar> = nil var linecap:Int = 0 defer { free(line) } return getline(&line, &linecap, file) > 0 ? String.fromCString(line) : nil } }например, вот как вы могли бы использовать его для печати каждой строки файла с именем " foo " в вашем пакете приложений:
let path = NSBundle.mainBundle().pathForResource("foo", ofType: nil)! let file = fopen(path,"r") // open the file stream for line in lineGenerator(file) { // suppress print's automatically inserted line ending, since // lineGenerator captures each line's own new line character. print(line, separator: "", terminator: "") } fclose(file) // cleanup the file streamЯ разработал этот ответ, изменив ответ Алекса Брауна, чтобы удалить утечку памяти, упомянутую комментарием Мартина р., и обновив его для работы с Swift 2.2 (Xcode 7.3).
попробовать этой ответьте или прочитайте Mac OS Руководство По Программированию Потока.
Вы можете обнаружить, что производительность будет лучше использовать
stringWithContentsOfURL, хотя, поскольку это будет быстрее работать с данными на основе памяти (или с отображением памяти), чем с дисковыми данными.выполнение его в другом потоке также хорошо документировано, например здесь.
обновление
Если вы не хотите читать все это сразу же, и вы не хотите использовать NSStreams, тогда вам, вероятно, придется использовать файловый ввод-вывод уровня C. есть много причины не делать этого - блокировка, кодировка символов, обработка ошибок ввода/вывода, скорость, чтобы назвать, но несколько-это то, для чего предназначены библиотеки 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 3.0.1 на Xcode 8.2.1 с macOS Sierra 10.12.3)
все ответы, которые я видел здесь, пропустили, что он может искать LF или CRLF. Если все пойдет хорошо, s / he может просто соответствовать на LF и проверить возвращенную строку для дополнительного CR в конце. Но общий запрос включает в себя несколько строк поиска. Другими словами, разделитель должен быть
Set<String>, где набор не пуст и не содержит пустую строку, а не одну строка.С моей первой попытки в этом прошлом году я попытался сделать "правильную вещь" и искать общий набор строк. Это было слишком сложно; вам нужен полномасштабный парсер и государственные машины и тому подобное. Я отказался от него и от проекта, частью которого он был.
теперь я снова делаю проект и снова сталкиваюсь с той же проблемой. Теперь я собираюсь искать жесткий код на CR и LF. Я не думаю, что кому-то нужно будет искать на двух полунезависимых и полузависимых персонажах, таких как это вне разбора CR/LF.
я использую методы поиска, представленной
Data, Так что я не делаю кодировки строк и прочее здесь. Просто необработанная двоичная обработка. Просто предположим, что у меня есть надмножество ASCII, например ISO Latin-1 или UTF-8, здесь. Вы можете обрабатывать строковое кодирование на следующем более высоком уровне, и вы определяете, будет ли CR/LF с присоединенными вторичными кодовыми точками по-прежнему считаться CR или LF.алгоритм: просто продолжайте искать следующий CR и в если рядом с вашей текущей смещения.
- если ни один из них не найден, то считайте, что следующая строка данных находится от текущего смещения до конца данных. Обратите внимание, что длина Терминатор 0. Отметьте это как конец цикла чтения.
- если LF найден первым или найден только LF, считайте, что следующая строка данных находится от текущего смещения до LF. Обратите внимание, что длина Терминатор 1. Переместите смещение в положение после НЧ.
- если только 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блок длины, которая составляет по крайней мере значительную часть гигабайта, вы будете принимать удар всякий раз, когда больше не существует 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.)
- и вам нужно обрабатывать, когда больше нет терминаторов из вашего текущего смещения, но конец данных находится в более позднем фрагменте.
удачи!
или вы могли бы просто использовать
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; используя
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) { ... }
мне нужна была версия, которая не постоянно изменяла буфер или дублировала код, поскольку оба они неэффективны и допускали бы любой размер буфера (включая 1 байт) и любой разделитель. Он имеет один открытый метод:
readline(). Вызов этого метода вернет строковое значение следующей строки или nil в EOF.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) }
Comments