Lesen Sie eine Datei / URL Zeile für Zeile in Swift

79

Ich versuche, eine in a angegebene Datei zu lesen NSURLund in ein Array zu laden, wobei die Elemente durch ein Zeilenumbruchzeichen getrennt sind \n.

So habe ich es bisher gemacht:

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
}

Ich bin aus mehreren Gründen nicht sehr zufrieden damit. Erstens arbeite ich mit Dateien, die von einigen Kilobyte bis zu Hunderten von MB reichen. Wie Sie sich vorstellen können, ist die Arbeit mit so großen Saiten langsam und unhandlich. Zweitens friert dies die Benutzeroberfläche ein, wenn sie ausgeführt wird - wiederum nicht gut.

Ich habe versucht, diesen Code in einem separaten Thread auszuführen, aber ich hatte Probleme damit, und außerdem löst es das Problem des Umgangs mit großen Zeichenfolgen immer noch nicht.

Was ich tun möchte, ist etwas in der Art des folgenden Pseudocodes:

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

Wie würde ich das in Swift erreichen?

Einige Anmerkungen zu den Dateien, aus denen ich lese: Alle Dateien bestehen aus kurzen Zeichenfolgen (<255 Zeichen), die durch entweder \noder getrennt sind \r\n. Die Länge der Dateien reicht von ~ 100 Zeilen bis zu über 50 Millionen Zeilen. Sie können europäische Zeichen und / oder Zeichen mit Akzenten enthalten.

Matt
quelle
Möchten Sie das Array unterwegs auf die Festplatte schreiben oder einfach das Betriebssystem mit Speicher behandeln lassen? Wird der Mac, auf dem er ausgeführt wird, über genügend RAM verfügen, um die Datei zuzuordnen und auf diese Weise damit zu arbeiten? Mehrere Aufgaben sind einfach zu erledigen, und ich nehme an, Sie könnten mehrere Jobs haben, die die Datei an verschiedenen Stellen lesen.
Macshome

Antworten:

150

(Der Code ist jetzt für Swift 2.2 / Xcode 7.3. Ältere Versionen finden Sie im Bearbeitungsverlauf, wenn jemand ihn benötigt. Eine aktualisierte Version für Swift 3 finden Sie am Ende.)

Der folgende Swift-Code ist stark von den verschiedenen Antworten auf das zeilenweise Lesen von Daten aus NSFileHandle inspiriert . . Es liest in Blöcken aus der Datei und konvertiert vollständige Zeilen in Zeichenfolgen.

Der Standardzeilentrenner ( \n), die Zeichenfolgencodierung (UTF-8) und die Blockgröße (4096) können mit optionalen Parametern festgelegt werden.

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

Verwendung:

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

Sie können den Reader sogar mit einer For-In-Schleife verwenden

for line in aStreamReader {
    print(line)
}

durch Implementierung des SequenceTypeProtokolls (vergleiche http://robots.thoughtbot.com/swift-sequences ):

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

Update für Swift 3 / Xcode 8 Beta 6: Ebenfalls "modernisiert" guardund der neue DataWertetyp:

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()
        }
    }
}
Martin R.
quelle
1
@ Matt: Es spielt keine Rolle. Sie können die Erweiterung in dieselbe Swift-Datei wie die "Hauptklasse" oder in eine separate Datei einfügen. - Eigentlich brauchst du eigentlich keine Erweiterung. Sie können die generate()Funktion zur StreamReader-Klasse hinzufügen und als deklarieren class StreamReader : Sequence { ... }. Es scheint jedoch ein guter Swift-Stil zu sein, Erweiterungen für separate Funktionen zu verwenden.
Martin R
1
@zanzoken: Welche Art von URL verwenden Sie? Der obige Code funktioniert nur für Datei- URLs. Es kann nicht zum Lesen von einer allgemeinen Server-URL verwendet werden. Vergleiche stackoverflow.com/questions/26674182/… und meine Kommentare unter der Frage.
Martin R
2
@zanzoken: Mein Code ist für Textdateien gedacht und erwartet, dass die Datei eine angegebene Codierung verwendet (standardmäßig UTF-8). Wenn Sie eine Datei mit beliebigen Binärbytes haben (z. B. eine Bilddatei), schlägt die Konvertierung von Daten-> Zeichenfolgen fehl.
Martin R
1
@zanzoken: Das Lesen von Scanlinien aus einem Bild ist ein völlig anderes Thema und hat leider nichts mit diesem Code zu tun. Ich bin sicher, dass dies zum Beispiel mit CoreGraphics-Methoden möglich ist, aber ich habe keine unmittelbare Referenz für Sie.
Martin R
2
@DCDCwhile !aStreamReader.atEof { try autoreleasepool { guard let line = aStreamReader.nextLine() else { return } ...code... } }
Eporediese
25

Effiziente und bequeme Klasse zum zeilenweisen Lesen von Textdateien (Swift 4, Swift 5)

Hinweis: Dieser Code ist plattformunabhängig (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
      }
   }
}

Verwendung:

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

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

Repository auf Github

Andy C.
quelle
5

Swift 4.2 Sichere Syntax

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

Verwendung:

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

Ich bin zu spät zum Spiel, aber hier ist eine kleine Klasse, die ich zu diesem Zweck geschrieben habe. Nach einigen verschiedenen Versuchen (versuchen Sie eine UnterklasseNSInputStream zu erstellen) fand ich, dass dies ein vernünftiger und einfacher Ansatz ist.

Denken #import <stdio.h>Sie daran, in Ihrem Bridging-Header.

// 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)
    }
}
Albin Stigo
quelle
Ich mag das, aber es kann noch verbessert werden. Das Erstellen von Zeigern mit withCStringist nicht erforderlich (und tatsächlich sehr unsicher), Sie können einfach aufrufen return fopen(self.path, self.mode). Man könnte eine Prüfung hinzufügen, ob die Datei wirklich geöffnet werden kann, derzeit readline()nur abstürzt. Die UnsafePointer<CChar>Besetzung wird nicht benötigt. Schließlich wird Ihr Verwendungsbeispiel nicht kompiliert.
Martin R
4

Diese Funktion verwendet eine Datei-URL und gibt eine Sequenz zurück, die jede Zeile der Datei zurückgibt und sie träge liest. Es funktioniert mit Swift 5. Es basiert auf dem Basiswert 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
    }
  })
}

So würden Sie beispielsweise jede Zeile einer Datei mit dem Namen "foo" in Ihrem App-Bundle drucken:

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

Ich habe diese Antwort entwickelt, indem ich die Antwort von Alex Brown geändert habe, um einen in Martin Rs Kommentar erwähnten Speicherverlust zu beseitigen, und indem ich sie auf Swift 5 aktualisiert habe.

Algen
quelle
2

Versuchen Sie diese Antwort oder lesen Sie das Mac OS Stream-Programmierhandbuch .

Möglicherweise stellen Sie jedoch fest, dass die Leistung mit dem tatsächlich besser stringWithContentsOfURList, da die Arbeit mit speicherbasierten (oder speicherabgebildeten) Daten schneller ist als mit disk-basierten Daten.

Die Ausführung in einem anderen Thread ist beispielsweise hier auch gut dokumentiert .

Aktualisieren

Wenn Sie nicht alles auf einmal lesen möchten und keine NSStreams verwenden möchten, müssen Sie wahrscheinlich Datei-E / A auf C-Ebene verwenden. Es gibt viele Gründe, dies nicht zu tun - Blockieren, Zeichenkodierung, Behandlung von E / A-Fehlern, Geschwindigkeit bei der Benennung, aber nur einige - dafür sind die Foundation-Bibliotheken gedacht. Ich habe unten eine einfache Antwort skizziert, die sich nur mit ACSII-Daten befasst:

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)
}
Grimxn
quelle
Ich schätze die Vorschläge, aber ich suche speziell nach dem Code in Swift. Außerdem möchte ich mit jeweils einer Zeile und nicht mit allen Zeilen gleichzeitig arbeiten.
Matt
Möchten Sie also mit einer Zeile arbeiten, diese dann freigeben und die nächste einlesen? Ich müsste denken, dass es schneller sein wird, damit im Speicher zu arbeiten. Müssen sie in der richtigen Reihenfolge verarbeitet werden? Wenn nicht, können Sie einen Aufzählungsblock verwenden, um die Verarbeitung des Arrays erheblich zu beschleunigen.
Macshome
Ich möchte mehrere Zeilen gleichzeitig erfassen, muss aber nicht unbedingt alle Zeilen laden. Es ist nicht kritisch, in Ordnung zu sein, aber es wäre hilfreich.
Matt
Was passiert, wenn Sie die case 0...127Zeichen auf Nicht-ASCII-Zeichen erweitern?
Matt
1
Nun, das hängt wirklich davon ab, welche Zeichenkodierung Sie in Ihren Dateien haben. Wenn es sich um eines der vielen Unicode-Formate handelt, müssen Sie dafür codieren. Wenn es sich um eines der vielen Pre-Unicode-PC-Codepage-Systeme handelt, müssen Sie dies dekodieren. Die Foundation-Bibliotheken erledigen das alles für Sie, es ist eine Menge Arbeit für sich.
Grimxn
2

Es stellt sich heraus, dass eine gute C-API mit alten Funktionen in Swift ziemlich komfortabel ist, wenn Sie UnsafePointer verwenden. Hier ist eine einfache Katze, die von stdin liest und Zeile für Zeile auf stdout druckt. Sie brauchen nicht einmal Foundation. Darwin genügt:

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()
dankogai
quelle
1
Kann "by line" überhaupt nicht verarbeiten. Es gibt Eingabedaten für die Ausgabe aus und erkennt den Unterschied zwischen normalen Zeichen und Zeilenendezeichen nicht. Natürlich besteht die Ausgabe aus den gleichen Zeilen wie die Eingabe, aber das liegt daran, dass die Zeilenumbruch ebenfalls unterbrochen ist.
Alex Brown
3
@ AlexBrown: Das stimmt nicht. fgets()liest Zeichen bis zu (und einschließlich) einem Zeilenumbruchzeichen (oder EOF). Oder verstehe ich Ihren Kommentar falsch?
Martin R
@ Martin R, bitte wie würde das in Swift 4/5 aussehen? Ich brauche etwas so Einfaches, um eine Datei Zeile für Zeile zu lesen -
gbenroscience
1

Oder Sie könnten einfach ein Generator:

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

Probieren wir es aus

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

Es ist einfach, faul und leicht mit anderen schnellen Dingen wie Enumeratoren und Funktoren wie Map, Reduce, Filter zu verketten. mit dem lazy()Wrapper.


Es verallgemeinert für alle FILEals:

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

genannt wie

for line in byLine(stdin) { ... }
Alex Brown
quelle
Vielen Dank an eine jetzt verstorbene Antwort, die mir den getline-Code gegeben hat!
Alex Brown
1
Offensichtlich ignoriere ich die Codierung vollständig. Links als Übung für den Leser.
Alex Brown
Beachten Sie, dass Ihr Code Speicher verliert, da getline()ein Puffer für die Daten zugewiesen wird.
Martin R.
1

(Hinweis: Ich verwende Swift 3.0.1 unter Xcode 8.2.1 mit macOS Sierra 10.12.3.)

Alle Antworten, die ich hier gesehen habe, haben übersehen, dass er nach LF oder CRLF suchen könnte. Wenn alles gut geht, könnte er / sie einfach mit LF übereinstimmen und die zurückgegebene Zeichenfolge am Ende auf eine zusätzliche CR überprüfen. Die allgemeine Abfrage umfasst jedoch mehrere Suchzeichenfolgen. Mit anderen Worten, das Trennzeichen muss a sein Set<String>, wobei die Menge weder leer ist noch die leere Zeichenfolge enthält, sondern eine einzelne Zeichenfolge.

Bei meinem ersten Versuch in diesem letzten Jahr habe ich versucht, das "Richtige" zu tun und nach einem allgemeinen Satz von Zeichenfolgen zu suchen. Es war zu schwer; Sie benötigen einen ausgewachsenen Parser und Zustandsautomaten und so weiter. Ich habe es aufgegeben und das Projekt, an dem es beteiligt war.

Jetzt mache ich das Projekt wieder und stehe wieder vor der gleichen Herausforderung. Jetzt gehe ich zur Hardcode-Suche nach CR und LF. Ich glaube nicht, dass irgendjemand außerhalb der CR / LF-Analyse nach zwei halbunabhängigen und halbabhängigen Zeichen wie diesem suchen müsste.

Ich verwende die Suchmethoden von Data, also mache ich hier keine String-Codierungen und so. Nur rohe Binärverarbeitung. Nehmen wir einfach an, ich habe hier eine ASCII-Obermenge wie ISO Latin-1 oder UTF-8. Sie können die Zeichenfolgencodierung auf der nächsthöheren Ebene verarbeiten und prüfen, ob ein CR / LF mit angehängten sekundären Codepunkten weiterhin als CR oder LF zählt.

Der Algorithmus: Suchen Sie einfach weiter nach dem nächsten CR und dem nächsten LF aus Ihrem aktuellen Byte-Offset.

  • Wenn beides nicht gefunden wird, wird davon ausgegangen, dass die nächste Datenzeichenfolge vom aktuellen Offset bis zum Datenende reicht. Beachten Sie, dass die Terminatorlänge 0 ist. Markieren Sie dies als das Ende Ihrer Leseschleife.
  • Wenn zuerst ein LF gefunden wird oder nur ein LF gefunden wird, betrachten Sie die nächste Datenzeichenfolge als vom aktuellen Offset zum LF. Beachten Sie, dass die Terminatorlänge 1 beträgt. Verschieben Sie den Versatz nach nach dem LF.
  • Wenn nur ein CR gefunden wird, mögen Sie den LF-Fall (nur mit einem anderen Bytewert).
  • Ansonsten haben wir eine CR gefolgt von einer LF.
    • Wenn die beiden benachbart sind, behandeln Sie sie wie im LF-Fall, außer dass die Terminatorlänge 2 beträgt.
    • Wenn sich zwischen ihnen ein Byte befindet und das Byte auch CR ist, haben wir den Fehler "Windows-Entwickler hat im Textmodus eine Binärdatei \ r \ n geschrieben, was ein \ r \ r \ n" -Problem verursacht. Behandeln Sie es auch wie das LF-Gehäuse, außer dass die Terminatorlänge 3 beträgt.
    • Andernfalls sind CR und LF nicht verbunden und werden wie der Just-CR-Fall behandelt.

Hier ist ein Code dafür:

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
    }

}

Wenn Sie einen DataBlock mit einer Länge haben, die mindestens einen signifikanten Bruchteil eines Gigabytes ausmacht , werden Sie natürlich immer dann einen Treffer erzielen, wenn vom aktuellen Byte-Offset kein CR oder LF mehr vorhanden ist. Immer erfolglos bis zum Ende bei jeder Iteration suchen. Das Lesen der Daten in Blöcken würde helfen:

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

}

Sie müssen diese Ideen selbst mischen, da ich es noch nicht getan habe. Erwägen:

  • Natürlich müssen Sie Linien berücksichtigen, die vollständig in einem Block enthalten sind.
  • Sie müssen jedoch damit umgehen, wenn sich die Enden einer Linie in benachbarten Abschnitten befinden.
  • Oder wenn zwischen den Endpunkten mindestens ein Block liegt
  • Die große Komplikation besteht darin, dass die Zeile mit einer Mehrbyte-Sequenz endet, diese Sequenz jedoch zwei Blöcke überspannt! (Eine Zeile, die nur mit CR endet und gleichzeitig das letzte Byte im Block ist, ist ein äquivalenter Fall, da Sie den nächsten Block lesen müssen, um festzustellen, ob Ihr just-CR tatsächlich eine CRLF oder CR-CRLF ist. Es gibt ähnliche Spielereien, wenn die Chunk endet mit CR-CR.)
  • Und Sie müssen damit umgehen, wenn Ihr aktueller Offset keine Terminatoren mehr enthält, das Datenende jedoch in einem späteren Abschnitt liegt.

Viel Glück!

CTMacUser
quelle
1

Nach der Antwort von @ dankogai habe ich einige Änderungen für Swift 4+ vorgenommen.

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

Das hat bei mir funktioniert.

Vielen Dank

Gbenroscience
quelle
0

Ich wollte eine Version, die den Puffer oder den doppelten Code nicht kontinuierlich ändert, da beide ineffizient sind und Puffer jeder Größe (einschließlich 1 Byte) und Trennzeichen zulassen. Es gibt eine öffentliche Methode : readline(). Wenn Sie diese Methode aufrufen, wird der String-Wert der nächsten Zeile oder Null bei EOF zurückgegeben.

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

Es heißt wie folgt:

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

while let s = myStream.readLine() {
  print(s)
}
Tannenzapfen
quelle