Leer un archivo / URL línea por línea en Swift


80

Estoy tratando de leer un archivo dado en NSURLy cargarlo en una matriz, con elementos separados por un carácter de nueva línea \n.

Así es como lo he hecho hasta ahora:

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
}

No estoy muy contento con esto por un par de razones. Uno, estoy trabajando con archivos que van desde unos pocos kilobytes hasta cientos de MB de tamaño. Como puede imaginar, trabajar con cadenas de este tamaño es lento y difícil de manejar. En segundo lugar, esto congela la interfaz de usuario cuando se está ejecutando, de nuevo, no es bueno.

He buscado ejecutar este código en un hilo separado, pero he tenido problemas con eso y, además, todavía no resuelve el problema de lidiar con cadenas enormes.

Lo que me gustaría hacer es algo similar al siguiente pseudocódigo:

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

¿Cómo lograría esto en Swift?

Algunas notas sobre los archivos de los que estoy leyendo: Todos los archivos constan de cadenas cortas (<255 caracteres) separadas por \no \r\n. La longitud de los archivos varía desde ~ 100 líneas hasta más de 50 millones de líneas. Pueden contener caracteres europeos y / o caracteres con acentos.


¿Quiere escribir la matriz en el disco sobre la marcha o simplemente dejar que el sistema operativo lo maneje con la memoria? ¿La Mac que lo ejecuta tendrá suficiente memoria RAM para que pueda asignar el archivo y trabajar con él de esa manera? Varias tareas son bastante fáciles de hacer, y supongo que podría tener varios trabajos que comiencen a leer el archivo en diferentes lugares.
macshome

Respuestas:


150

(El código es para Swift 2.2 / Xcode 7.3 ahora. Las versiones anteriores se pueden encontrar en el historial de edición si alguien lo necesita. Al final se proporciona una versión actualizada para Swift 3).

El siguiente código Swift está muy inspirado en las diversas respuestas a ¿Cómo leer datos de NSFileHandle línea por línea? . Lee el archivo en trozos y convierte líneas completas en cadenas.

El delimitador de línea predeterminado ( \n), la codificación de cadena (UTF-8) y el tamaño del fragmento (4096) se pueden configurar con parámetros opcionales.

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

Incluso puede usar el lector con un bucle for-in

for line in aStreamReader {
    print(line)
}

implementando el SequenceTypeprotocolo (compare http://robots.thoughtbot.com/swift-sequences ):

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

Actualización para Swift 3 / Xcode 8 beta 6: También "modernizado" para usar guardy el nuevo Datatipo de valor:

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

1
@ Matt: No importa. Puede poner la extensión en el mismo archivo Swift que la "clase principal", o en un archivo separado. - En realidad, no necesitas una extensión. Puede agregar la generate()función a la clase StreamReader y declararla como class StreamReader : Sequence { ... }. Pero parece ser un buen estilo Swift usar extensiones para funciones separadas.
Martin R

1
@zanzoken: ¿Qué tipo de URL estás usando? El código anterior funciona solo para URL de archivos . No se puede utilizar para leer desde una URL de servidor general. Compare stackoverflow.com/questions/26674182/… y mis comentarios bajo la pregunta.
Martin R

2
@zanzoken: Mi código está diseñado para archivos de texto y espera que el archivo use una codificación específica (UTF-8 por defecto). Si tiene un archivo con bytes binarios arbitrarios (como un archivo de imagen), la conversión de datos-> cadena fallará.
Martin R

1
@zanzoken: leer líneas de escaneo de una imagen es un tema completamente diferente y no tiene nada que ver con este código, lo siento. Estoy seguro de que se puede hacer, por ejemplo, con los métodos de CoreGraphics, pero no quiero tener una referencia inmediata para ti.
Martin R

2
@DCDCwhile !aStreamReader.atEof { try autoreleasepool { guard let line = aStreamReader.nextLine() else { return } ...code... } }
Eporediese

26

Clase eficiente y conveniente para leer archivos de texto línea por línea (Swift 4, Swift 5)

Nota: este código es independiente de la plataforma (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
      }
   }
}

Uso:

guard let reader = LineReader(path: "/Path/to/file.txt") else {
    return; // cannot open file
}

for line in reader {
    print(">" + line.trimmingCharacters(in: .whitespacesAndNewlines))      
}

Repositorio en github


6

Sintaxis segura de 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
        }
    }
}

Uso:

guard let reader = LineReader(path: "/Path/to/file.txt") else {
    return
}
reader.forEach { line in
    print(line.trimmingCharacters(in: .whitespacesAndNewlines))      
}

4

Llego tarde al juego, pero aquí hay una pequeña clase que escribí para ese propósito. Después de algunos intentos diferentes (intentar NSInputStreamcrear una subclase ), encontré que este era un enfoque simple y razonable.

Recuerde hacerlo #import <stdio.h>en su encabezado puente.

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

Me gusta esto, pero aún se puede mejorar. withCStringNo es necesario crear punteros usando (y en realidad es realmente inseguro), simplemente puede llamar return fopen(self.path, self.mode). Se podría agregar una marca de verificación si el archivo realmente se pudo abrir, actualmente readline()simplemente se bloqueará. El UnsafePointer<CChar>yeso no es necesario. Finalmente, su ejemplo de uso no se compila.
Martin R

4

Esta función toma la URL de un archivo y devuelve una secuencia que devolverá cada línea del archivo, leyéndolos perezosamente. Funciona con Swift 5. Se basa en el subyacente 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
    }
  })
}

Entonces, por ejemplo, así es como lo usaría para imprimir cada línea de un archivo llamado "foo" en su paquete de aplicaciones:

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

Desarrollé esta respuesta modificando la respuesta de Alex Brown para eliminar una fuga de memoria mencionada en el comentario de Martin R, y actualizándola para Swift 5.


2

Pruebe esta respuesta o lea la Guía de programación de secuencias de Mac OS .

Sin stringWithContentsOfURLembargo, es posible que el rendimiento sea mejor usando el , ya que será más rápido trabajar con datos basados ​​en memoria (o mapeados en memoria) que con datos basados ​​en disco.

Ejecutarlo en otro hilo está bien documentado, también, por ejemplo aquí .

Actualizar

Si no quiere leerlo todo a la vez y no quiere usar NSStreams, probablemente tendrá que usar E / S de archivos de nivel C. Hay muchas razones para no hacer esto: bloqueo, codificación de caracteres, manejo de errores de E / S, velocidad, por nombrar solo algunas, para eso están las bibliotecas de Foundation. He esbozado una respuesta simple a continuación que solo trata con datos 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)
}

Aprecio la (s) sugerencia (s), pero estoy buscando específicamente el código en Swift. Además, quiero trabajar con una línea a la vez, en lugar de todas las líneas a la vez.
Matt

Entonces, ¿está buscando trabajar con una línea, luego suéltela y lea la siguiente? Necesitaría pensar que va a ser más rápido trabajar con él en la memoria. ¿Necesitan ser procesados ​​en orden? Si no, puede utilizar un bloque de enumeración para acelerar drásticamente el procesamiento de la matriz.
macshome

Me gustaría tomar varias líneas a la vez, pero no necesariamente necesitaré cargar todas las líneas. En cuanto a estar en orden, no es fundamental, pero sería útil.
Matt

¿Qué sucede si extiende el case 0...127a caracteres que no son ASCII?
Matt

1
Bueno, eso realmente depende de la codificación de caracteres que tenga en sus archivos. Si es uno de los muchos formatos de Unicode, necesitará codificarlo, si es uno de los muchos sistemas de "página de códigos" de PC anteriores a Unicode, deberá decodificarlo. Las bibliotecas de la Fundación hacen todo esto por usted, es mucho trabajo por su cuenta.
Grimxn

2

Resulta que la buena API C de la vieja usanza es bastante cómoda en Swift una vez que asimila UnsafePointer. Aquí hay un gato simple que lee de stdin y lo imprime en stdout línea por línea. Ni siquiera necesitas Foundation. Darwin es suficiente:

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

1
No logra manejar "por línea" en absoluto. Transmite los datos de entrada a la salida y no reconoce la diferencia entre los caracteres normales y los caracteres de final de línea. Obviamente, la salida consta de las mismas líneas que la entrada, pero eso se debe a que la nueva línea también se borra.
Alex Brown

3
@AlexBrown: Eso no es cierto. fgets()lee caracteres hasta (incluido) un carácter de nueva línea (o EOF). ¿O estoy entendiendo mal tu comentario?
Martin R

@Martin R, ¿cómo se vería esto en Swift 4/5? Necesito algo así de simple para leer un archivo línea por línea -
gbenroscience

1

O simplemente podría usar un Generator:

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

Vamos a probarlo

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

Es simple, lento y fácil de encadenar con otras cosas rápidas como enumeradores y functores como mapear, reducir, filtrar; usando la lazy()envoltura.


Se generaliza a todos FILEcomo:

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

llamado como

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

¡Muchas gracias a una respuesta ahora difunta que me dio el código getline!
Alex Brown

1
Obviamente estoy ignorando completamente la codificación. Dejado como ejercicio para el lector.
Alex Brown

Tenga en cuenta que su código pierde memoria ya que getline()asigna un búfer para los datos.
Martin R

1

(Nota: estoy usando Swift 3.0.1 en Xcode 8.2.1 con macOS Sierra 10.12.3)

Todas las respuestas que he visto aquí pasaron por alto que podría estar buscando LF o CRLF. Si todo va bien, él / ella podría simplemente coincidir en LF y verificar la cadena devuelta para ver si hay un CR adicional al final. Pero la consulta general implica varias cadenas de búsqueda. En otras palabras, el delimitador debe ser a Set<String>, donde el conjunto no está vacío ni contiene la cadena vacía, en lugar de una sola cadena.

En mi primer intento en este último año, traté de hacer "lo correcto" y buscar un conjunto general de cadenas. Fue demasiado duro; necesita un analizador sintáctico completo y máquinas de estado y demás. Renuncié a él y al proyecto del que formaba parte.

Ahora estoy haciendo el proyecto de nuevo y me enfrento al mismo desafío nuevamente. Ahora voy a realizar búsquedas de código rígido en CR y LF. No creo que nadie necesite buscar dos caracteres semi-independientes y semi-dependientes como este fuera del análisis de CR / LF.

Estoy usando los métodos de búsqueda proporcionados por Data , lo que no estoy haciendo codificaciones de cadenas y esas cosas aquí. Solo procesamiento binario sin procesar. Suponga que tengo un superconjunto ASCII, como ISO Latin-1 o UTF-8, aquí. Puede manejar la codificación de cadenas en la siguiente capa superior, y puede decidir si un CR / LF con puntos de código secundarios adjuntos todavía cuenta como CR o LF.

El algoritmo: siga buscando el siguiente CR y el siguiente LF de su desplazamiento de bytes actual.

  • Si no se encuentra ninguno, considere que la siguiente cadena de datos es desde el desplazamiento actual hasta el final de los datos. Tenga en cuenta que la longitud del terminador es 0. Marque esto como el final de su ciclo de lectura.
  • Si primero se encuentra un LF, o solo se encuentra un LF, considere que la siguiente cadena de datos es desde el desplazamiento actual al LF. Tenga en cuenta que la longitud del terminador es 1. Mueva el desplazamiento a después del LF.
  • Si solo se encuentra un CR, haga como el caso LF (solo con un valor de byte diferente).
  • De lo contrario, obtuvimos un CR seguido de un LF.
    • Si los dos son adyacentes, maneje como el caso LF, excepto que la longitud del terminador será 2.
    • Si hay un byte entre ellos, y dicho byte también es CR, entonces tenemos el problema "El desarrollador de Windows escribió un \ r \ n binario mientras estaba en modo texto, dando un \ r \ r \ n". También manipúlelo como el caso LF, excepto que la longitud del terminador será 3.
    • De lo contrario, CR y LF no están conectados y se manejan como el caso de solo CR.

Aquí hay un código para eso:

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
    }

}

Por supuesto, si tiene un Databloque de una longitud que es al menos una fracción significativa de un gigabyte, recibirá un golpe siempre que no exista más CR o LF del desplazamiento de bytes actual; siempre buscando infructuosamente hasta el final durante cada iteración. Leer los datos en fragmentos ayudaría:

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

}

Tienes que mezclar estas ideas tú mismo, ya que todavía no lo he hecho. Considerar:

  • Por supuesto, debe considerar las líneas completamente contenidas en un fragmento.
  • Pero tienes que manejar cuando los extremos de una línea están en trozos adyacentes.
  • O cuando los puntos finales tienen al menos un fragmento entre ellos
  • La gran complicación es cuando la línea termina con una secuencia de varios bytes, ¡pero dicha secuencia se extiende a ambos lados! (Una línea que termina solo en CR que también es el último byte en el fragmento es un caso equivalente, ya que necesita leer el siguiente fragmento para ver si su solo-CR es realmente un CRLF o CR-CRLF. Hay travesuras similares cuando el fragmento termina con CR-CR.)
  • Y necesita manejar cuando no hay más terminadores de su desplazamiento actual, pero el final de los datos está en un fragmento posterior.

¡Buena suerte!


1

Siguiendo la respuesta de @ dankogai , hice algunas modificaciones para 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()

Esto funcionó para mí.

Gracias


0

Quería una versión que no modificara continuamente el búfer ni duplicara el código, ya que ambos son ineficientes y permitirían cualquier tamaño de búfer (incluido 1 byte) y cualquier delimitador. Tiene un método público: readline(). Llamar a este método devolverá el valor de cadena de la siguiente línea o nulo en 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)
  }
}

Se llama de la siguiente manera:

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

while let s = myStream.readLine() {
  print(s)
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.