Leggi un file / URL riga per riga in Swift

Sto cercando di leggere un file fornito in un NSURL e di caricarlo in un array, con elementi separati da un carattere di nuova riga \n .

Ecco come l’ho fatto finora:

 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 } 

Non sono molto contento di questo per un paio di motivi. Uno, sto lavorando con file che vanno da pochi kilobyte a centinaia di MB di dimensioni. Come puoi immaginare, lavorare con stringhe così grandi è lento e poco maneggevole. In secondo luogo, questo blocca l’interfaccia utente quando è in esecuzione – ancora una volta, non va bene.

Ho cercato di eseguire questo codice in un thread separato, ma ho avuto problemi con questo, e inoltre, non risolve ancora il problema di gestire stringhe enormi.

Quello che mi piacerebbe fare è qualcosa sulla falsariga del seguente pseudocodice:

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

Come potrei realizzare questo in Swift?

Alcune note sui file che sto leggendo: Tutti i file sono costituiti da stringhe brevi (<255 caratteri) separate da \n o \r\n . La lunghezza dei file varia da ~ 100 linee a oltre 50 milioni di righe. Possono contenere caratteri europei e / o caratteri con accenti.

(Il codice è ora per Swift 2.2 / Xcode 7.3. Le versioni precedenti possono essere trovate nella cronologia delle modifiche se qualcuno ne ha bisogno. Alla fine viene fornita una versione aggiornata di Swift 3.)

Il seguente codice Swift è fortemente ispirato dalle varie risposte a Come leggere i dati da NSFileHandle riga per riga? . Legge dal file in blocchi e converte le linee complete in stringhe.

Il delimitatore di riga predefinito ( \n ), la codifica di stringa (UTF-8) e la dimensione del blocco (4096) possono essere impostati con parametri facoltativi.

 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 } } 

Uso:

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

Puoi persino usare il lettore con un ciclo for-in

 for line in aStreamReader { print(line) } 

implementando il protocollo SequenceType (confronta http://robots.thoughtbot.com/swift-sequences ):

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

Aggiornamento per Swift 3 / Xcode 8 beta 6: anche “modernizzato” per usare guard e il nuovo tipo di valore 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.. 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 { return AnyIterator { return self.nextLine() } } } 

Ho spostato il codice dalla risposta di algal in una class conveniente (Swift 4.0)

UPD: Questo codice è indipendente dalla piattaforma (macOS, iOS, ubuntu)

 import Foundation /// Read text file line by line public class LineReader { public let path: String fileprivate let file: UnsafeMutablePointer! init?(path: String) { self.path = path file = fopen(path, "r") guard file != nil else { return nil } } public var nextLine: String? { var line:UnsafeMutablePointer? = 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 { return AnyIterator { return self.nextLine } } } 

Uso:

 guard let reader = LineReader(path: "/Path/to/file.txt") else { return; // cannot open file } for line in reader { print(">" + line.trimmingCharacters(in: .whitespacesAndNewlines)) } 

Repository su github

Sono in ritardo per il gioco, ma qui è una piccola class che ho scritto per quello scopo. Dopo alcuni tentativi diversi (provare a sottoclass NSInputStream ) ho trovato che questo è un approccio ragionevole e semplice.

Ricorda di #import nell’intestazione del bridging.

 // Use is like this: let readLine = ReadLine(somePath) while let line = readLine.readLine() { // do something... } class ReadLine { private var buf = UnsafeMutablePointer.alloc(1024) private var n: Int = 1024 let path: String let mode: String = "r" private lazy var filepointer: UnsafeMutablePointer = { 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(buf)) } return nil } deinit { buf.dealloc(n) fclose(filepointer) } } 

Si scopre che una buona C-API vecchio stile è piuttosto comoda in Swift una volta che si esegue UnsafePointer. Ecco un semplice gatto che legge da stdin e stampa per stdout riga per riga. Non hai nemmeno bisogno di Fondazione. Darwin è sufficiente:

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

Questa funzione accetta un stream di file e restituisce un AnyGenerator che restituisce ogni riga del file:

 func lineGenerator(file:UnsafeMutablePointer) -> AnyGenerator { return AnyGenerator { () -> String? in var line:UnsafeMutablePointer = nil var linecap:Int = 0 defer { free(line) } return getline(&line, &linecap, file) > 0 ? String.fromCString(line) : nil } } 

Quindi, ad esempio, ecco come lo useresti per stampare ogni riga di un file chiamato “foo” nel tuo pacchetto di app:

 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 

Ho sviluppato questa risposta modificando la risposta di Alex Brown per rimuovere una perdita di memoria menzionata dal commento di Martin R e aggiornandola per funzionare con Swift 2.2 (Xcode 7.3).

Prova questa risposta o leggi la Guida alla programmazione del stream Mac OS.

Potresti scoprire che le prestazioni saranno effettivamente migliori utilizzando stringWithContentsOfURL , dato che sarà più veloce lavorare con dati basati su memoria (o mappati in memoria) rispetto ai dati basati su disco.

L’esecuzione su un altro thread è ben documentata, anche, per esempio qui .

Aggiornare

Se non vuoi leggerlo tutto in una volta, e non vuoi usare NSStreams, probabilmente dovrai usare I / O di file di livello C. Ci sono molte ragioni per non farlo – blocco, codifica dei caratteri, gestione degli errori di I / O, velocità per citarne solo alcuni – questo è ciò che le librerie della Fondazione servono. Ho abbozzato una semplice risposta di seguito che tratta solo i dati di ACSII:

 class StreamReader { var eofReached = false let fileHandle: UnsafePointer 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) } 

(Nota: sto utilizzando Swift 3.0.1 su Xcode 8.2.1 con macOS Sierra 10.12.3)

Tutte le risposte che ho visto qui hanno mancato che potesse cercare LF o CRLF. Se tutto va bene, lui / lei potrebbe semplicemente abbinare su LF e controllare la stringa restituita per un CR in più alla fine. Ma la query generale riguarda più stringhe di ricerca. In altre parole, il delimitatore deve essere un Set , dove il set non è né vuoto né contiene la stringa vuota, anziché una singola stringa.

Al mio primo tentativo in quest’ultimo anno, ho cercato di fare la “cosa giusta” e cercare un insieme generale di stringhe. Era troppo difficile; hai bisogno di un parser completo e di macchine di stato e simili. Ho rinunciato a questo e al progetto di cui faceva parte.

Ora sto facendo di nuovo il progetto e affrontando di nuovo la stessa sfida. Ora sto andando alla ricerca di hard-code su CR e LF. Non penso che qualcuno avrebbe bisogno di cercare su due personaggi semi-indipendenti e semi-dipendenti come questo al di fuori del parsing CR / LF.

Sto usando i metodi di ricerca forniti da Data , quindi non sto facendo codifiche di stringhe e roba qui. Solo elaborazione binaria grezza. Supponiamo di avere un superset ASCII, come ISO Latin-1 o UTF-8, qui. È ansible gestire la codifica delle stringhe al livello immediatamente superiore e puntare sul fatto che un CR / LF con punti di codice secondari collegati conti ancora come CR o LF.

L’algoritmo: continua a cercare il prossimo CR e il prossimo LF dal tuo attuale offset di byte.

  • Se nessuno dei due viene trovato, considera la stringa di dati successiva come dall’offset corrente alla fine dei dati. Si noti che la lunghezza del terminatore è 0. Segnalo come la fine del ciclo di lettura.
  • Se viene trovato un LF per primo o viene trovato solo un LF, considerare la successiva stringa di dati che si trova dall’offset corrente a LF. Notare che la lunghezza del terminatore è 1. Spostare l’offset dopo l’LF.
  • Se viene trovato solo un CR, fare come il caso LF (solo con un valore byte diverso).
  • Altrimenti, abbiamo una CR seguita da una LF.
    • Se i due sono adiacenti, gestisci come il caso LF, tranne che la lunghezza del terminatore sarà 2.
    • Se c’è un byte tra loro e detto byte è anche CR, allora abbiamo ottenuto che lo “sviluppatore di Windows ha scritto un binario \ r \ n mentre era in modalità testo, dando un \ r \ r \ n” problema. Anche gestirlo come il caso LF, tranne che la lunghezza del terminatore sarà 3.
    • In caso contrario, CR e LF non sono collegati e gestiscono come il caso CR giusto.

Ecco un codice per questo:

 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.. 

Naturalmente, se si dispone di un blocco Data di una lunghezza che è almeno una frazione significativa di un gigabyte, si riceverà un hit ogni volta che non esistono più CR o LF dall'offset di byte corrente; ricerca sempre inutilmente fino alla fine durante ogni iterazione. Leggere i dati in blocchi potrebbe aiutare:

 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))) } } 

Devi mescolare queste idee insieme, dato che non l'ho ancora fatto. Prendere in considerazione:

  • Naturalmente, devi considerare le linee completamente contenute in un blocco.
  • Ma devi gestire quando le estremità di una linea si trovano in blocchi adiacenti.
  • O quando gli endpoint hanno almeno un blocco tra loro
  • La grande complicazione è quando la linea termina con una sequenza multi-byte, ma detta sequenza si trova a cavallo di due blocchi! (Una riga che termina in solo CR che è anche l'ultimo byte nel blocco è un caso equivalente, poiché è necessario leggere il prossimo blocco per vedere se il tuo just-CR è in realtà un CRLF o CR-CRLF. Ci sono shenanigans simili quando il chunk termina con CR-CR.)
  • E devi gestire quando non ci sono più terminatori del tuo attuale offset, ma la fine dei dati si trova in un blocco successivo.

In bocca al lupo!

O potresti semplicemente usare un Generator :

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

Proviamolo

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

È semplice, pigro e facile da concatenare con altre cose veloci come gli enumeratori e i funtori come la mappa, ridurre, filtrare; usando il wrapper lazy() .


Si generalizza a tutto il FILE come:

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

chiamato come

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

Volevo una versione che non modificasse continuamente il buffer o il codice duplicato, in quanto entrambi sono inefficienti e consentirebbe un buffer di qualsiasi dimensione (incluso 1 byte) e qualsiasi delimitatore. Ha un metodo pubblico: readline() . La chiamata a questo metodo restituirà il valore di stringa della riga successiva o nullo a 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, ie, 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) } } 

Si chiama come segue:

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