web-dev-qa-db-fra.com

Lire un fichier/une URL ligne par ligne dans Swift

J'essaie de lire un fichier donné dans une NSURL et de le charger dans un tableau avec des éléments séparés par un caractère de nouvelle ligne \n

Voici comment je l'ai fait jusqu'à présent:

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
}

Je ne suis pas très heureux avec cela pour deux raisons. Premièrement, je travaille avec des fichiers allant de quelques kilo-octets à plusieurs centaines de Mo. Comme vous pouvez l'imaginer, travailler avec des chaînes de cette taille est lent et difficile à manier. Deuxièmement, cela bloque l'interface utilisateur lors de son exécution - encore une fois, ce n'est pas bien.

J'ai examiné la possibilité d'exécuter ce code dans un autre thread, mais cela me pose problème, et de plus, cela ne résout toujours pas le problème du traitement des énormes chaînes.

Ce que je voudrais faire est quelque chose comme le pseudocode suivant:

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

Comment pourrais-je accomplir cela à Swift?

Quelques notes sur les fichiers que je lis: Tous les fichiers sont composés de chaînes courtes (<255 caractères) séparées par \n ou \r\n. La longueur des fichiers varie de ~ 100 lignes à plus de 50 millions de lignes. Ils peuvent contenir des caractères européens et/ou des caractères accentués.

66
Matt

(Le code concerne maintenant Swift 2.2/Xcode 7.3. Les versions les plus anciennes peuvent être trouvées dans l'historique d'édition si quelqu'un en a besoin. Une version mise à jour pour Swift 3 est fournie à la fin.)}

Le code Swift suivant est fortement inspiré des diverses réponses à Comment lire les données de NSFileHandle ligne par ligne? . Il lit le fichier par morceaux et convertit des lignes complètes en chaînes.

Le délimiteur de ligne par défaut (\n), le codage de chaîne (UTF-8) et la taille de bloc (4096) .__ peuvent être définis avec des paramètres facultatifs. 

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

Usage:

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

Vous pouvez même utiliser le lecteur avec une boucle for-in

for line in aStreamReader {
    print(line)
}

en implémentant le protocole SequenceType (comparer http://robots.thoughtbot.com/Swift-sequences ):

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

Mise à jour pour Swift 3/Xcode 8 beta 6: Egalement "modernisé" en Utilisez guard et le nouveau type de valeur 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()
        }
    }
}
134
Martin R

J'ai enveloppé le code de la réponse d'algal dans une classe pratique (Swift 4.0)

UPD: Ce code est indépendant de la plate-forme (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
        }
    }
}

Usage:

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

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

Dépôt sur github

18
Andy C

Je suis en retard au jeu, mais voici la petite classe que j'ai écrite à cet effet. Après différentes tentatives (essayez de placer la sous-classe NSInputStream), j’ai trouvé cette approche simple et raisonnable.

N'oubliez pas de #import <stdio.h> dans votre en-tête de pontage.

// 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)
    }
}
4
Albin Stigo

Il s'avère que la bonne API C ancienne génération est assez confortable dans Swift une fois que vous avez joué à UnsafePointer. Voici un chat simple qui lit stdin et imprime ligne par ligne à stdout. Vous n'avez même pas besoin de Fondation. Darwin suffit:

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

Cette fonction prend un flux de fichier et retourne une AnyGenerator qui retourne chaque ligne du fichier:

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

Ainsi, par exemple, voici comment vous l'utiliseriez pour imprimer chaque ligne d'un fichier nommé "foo" dans votre bundle d'applications:

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

J'ai développé cette réponse en modifiant la réponse d'Alex Brown pour supprimer une fuite de mémoire mentionnée par le commentaire de Martin R, et en la mettant à jour pour qu'elle fonctionne avec Swift 2.2 (Xcode 7.3).

2
algal

Syntaxe de Swift 4.2 Safe

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

Usage:

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

(Remarque: j'utilise Swift 3.0.1 sur Xcode 8.2.1 avec macOS Sierra 10.12.3)

Toutes les réponses que j'ai vues ici manquaient, à savoir qu'il pourrait rechercher LF ou CRLF. Si tout se passe bien, il/elle pourrait simplement correspondre sur LF et vérifier la chaîne renvoyée pour un CR supplémentaire à la fin. Mais la requête générale implique plusieurs chaînes de recherche. En d'autres termes, le délimiteur doit être un Set<String>, où l'ensemble n'est ni vide ni contient la chaîne vide, au lieu d'une seule chaîne.

Lors de mon premier essai en cette dernière année, j'ai essayé de faire ce qu'il fallait et de chercher un ensemble général de chaînes. C'était trop dur. vous avez besoin d'un analyseur syntaxique complet et de machines d'état, etc. J'ai abandonné et le projet dont il faisait partie.

Maintenant, je refais le projet et je fais face au même défi. Maintenant, je vais faire une recherche en code dur sur CR et LF. Je ne pense pas que quiconque aurait besoin de chercher deux personnages semi-indépendants et semi-dépendants comme celui-ci en dehors de l'analyse syntaxique CR/LF.

J'utilise les méthodes de recherche fournies par Data, je ne fais donc pas d'encodage de chaîne ni d'autres choses ici. Juste traitement binaire brut. Supposons simplement que j'ai ici un sur-ensemble ASCII, tel que ISO Latin-1 ou UTF-8. Vous pouvez gérer le codage de chaîne sur la couche immédiatement supérieure, et vous déterminez si un CR/LF auquel des points de code secondaires sont attachés compte toujours comme CR ou LF.

L'algorithme: continuez simplement à rechercher le prochain CR et le prochain LF à partir de votre décalage d'octet actuel.

  • Si aucun n'est trouvé, alors considérez que la chaîne de données suivante va du décalage actuel à la fin des données. Notez que la longueur du terminateur est 0. Marquez ceci comme la fin de votre boucle de lecture.
  • Si un LF est trouvé en premier ou si seul un LF est trouvé, considérez la chaîne de données suivante comme allant du décalage actuel au LF. Notez que la longueur du terminateur est 1. Déplacez le décalage après le BF.
  • Si seul un CR est trouvé, faites comme dans le cas LF (juste avec une valeur d'octet différente).
  • Sinon, nous avons un CR suivi d'un LF .
    • Si les deux sont adjacents, manipulez comme dans le cas LF, sauf que la longueur du terminateur sera 2.
    • S'il y a un octet entre eux et que cet octet est également CR, nous avons le "Le développeur Windows a écrit un binaire\r\n en mode texte, ce qui pose un problème\r\r\n". Traitez-le également comme dans le cas LF, sauf que la longueur du terminateur sera 3.
    • Sinon, le CR et LF ne sont pas connectés et se traitent comme le cas just-CR.

Voici un code pour cela:

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
    }

}

Bien sûr, si vous avez un bloc Data d’une longueur d’au moins une fraction de gigaoctet, vous en aurez un succès chaque fois qu’il n’y aura plus de CR ou LF à partir du décalage d’octet actuel; toujours en vain chercher à la fin à chaque itération. La lecture des données en morceaux aiderait:

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

}

Vous devez mélanger ces idées vous-même, car je ne l'ai pas encore fait. Considérer:

  • Bien sûr, vous devez considérer les lignes complètement contenues dans un bloc.
  • Mais vous devez gérer lorsque les extrémités d'une ligne se trouvent dans des morceaux adjacents.
  • Ou lorsque les points finaux ont au moins un morceau entre eux
  • La grande complication se produit lorsque la ligne se termine par une séquence multi-octets, mais que cette séquence chevauche deux morceaux! (Une ligne se terminant par juste CR, qui est également le dernier octet du bloc, est un cas équivalent, car vous devez lire le bloc suivant pour voir si votre Just-CR est en fait un CRLF ou un CR-CRLF. le bloc se termine par CR-CR.)
  • Et vous devez gérer quand il n'y a plus de terminateurs de votre décalage actuel, mais que la fin des données se trouve dans un bloc ultérieur.

Bonne chance!

1
CTMacUser

Essayez ceci répondez, ou lisez le Guide de programmation Mac OS Stream

Vous constaterez peut-être que les performances seront meilleures si vous utilisez la variable stringWithContentsOfURL, car il sera plus rapide de travailler avec des données basées sur la mémoire (ou mappées en mémoire) qu'avec des données basées sur disque.

L'exécuter sur un autre thread est bien documenté, aussi, par exemple ici .

Mettre à jour

Si vous ne voulez pas tout lire en une fois et que vous ne voulez pas utiliser NSStreams, vous devrez probablement utiliser des E/S sur fichier au niveau C. Il y a beaucoup raisons de ne pas le faire - blocage, codage de caractères, gestion des erreurs d'E/S, rapidité pour n'en nommer que quelques-unes - c'est à cela que servent les bibliothèques Foundation. J'ai esquissé ci-dessous une réponse simple qui traite uniquement des données 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)
}
1
Grimxn

Ou vous pouvez simplement utiliser un Generator:

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

Essayons-le

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

Il est simple, paresseux et facile d’enchaîner avec d’autres éléments de Swift tels que des énumérateurs et des foncteurs tels que map, reduction, filter; en utilisant le wrapper lazy().


Il se généralise à tous FILE comme:

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

appelé comme

for line in byLine(stdin) { ... }
0
Alex Brown

Je voulais une version qui ne modifie pas continuellement le tampon ou le code en double, car les deux sont inefficaces et permettraient d'utiliser n'importe quel tampon de taille (y compris 1 octet) et tout délimiteur. Il a une méthode publique: readline(). L'appel de cette méthode renverra la valeur de chaîne de la ligne suivante ou 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)
  }
}

Il s'appelle comme suit:

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

while let s = myStream.readLine() {
  print(s)
}
0
Pinecone