Przeczytaj plik / adres URL wiersz po wierszu w języku Swift

80

Próbuję odczytać plik podany w NSURLi załadować go do tablicy, z elementami oddzielonymi znakiem nowego wiersza \n.

Oto sposób, w jaki zrobiłem to do tej pory:

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
}

Nie jestem z tego zadowolony z kilku powodów. Po pierwsze, pracuję z plikami o wielkości od kilku kilobajtów do setek MB. Jak możesz sobie wyobrazić, praca z tak dużymi strunami jest powolna i nieporęczna. Po drugie, powoduje to zawieszenie interfejsu użytkownika podczas wykonywania - znowu, niedobrze.

Zajrzałem do uruchomienia tego kodu w osobnym wątku, ale miałem z tym problem, a poza tym nadal nie rozwiązuje problemu radzenia sobie z dużymi ciągami.

Chciałbym zrobić coś w stylu następującego pseudokodu:

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

Jak mógłbym to osiągnąć w Swift?

Kilka uwag na temat plików Czytam od: Wszystkie pliki składają się z krótkich (<255 znaków) ciągi oddzielone albo \nalbo \r\n. Długość plików waha się od ~ 100 do ponad 50 milionów wierszy. Mogą zawierać znaki europejskie i / lub znaki z akcentami.

Matt
źródło
Czy chcesz zapisywać tablicę na dysku w trakcie, czy po prostu pozwolić systemowi operacyjnemu obsłużyć ją z pamięcią? Czy komputer Mac, na którym działa, ma wystarczającą ilość pamięci RAM, aby można było zmapować plik i pracować z nim w ten sposób? Wiele zadań jest dość łatwych do wykonania i przypuszczam, że możesz mieć wiele zadań, które rozpoczynają się od czytania pliku w różnych miejscach.
macshome

Odpowiedzi:

150

(Kod jest teraz dla Swift 2.2 / Xcode 7.3. Starsze wersje można znaleźć w historii edycji, jeśli ktoś tego potrzebuje. Zaktualizowana wersja dla Swift 3 znajduje się na końcu.)

Poniższy kod Swift jest mocno zainspirowany różnymi odpowiedziami na temat Jak czytać dane z NSFileHandle wiersz po wierszu? . Czyta z pliku fragmentami i konwertuje całe linie na łańcuchy.

Domyślny separator linii ( \n), kodowanie ciągów (UTF-8) i rozmiar fragmentu (4096) można ustawić za pomocą parametrów opcjonalnych.

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

Stosowanie:

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

Możesz nawet użyć czytnika z pętlą for-in

for line in aStreamReader {
    print(line)
}

implementując SequenceTypeprotokół (porównaj http://robots.thoughtbot.com/swift-sequences ):

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

Aktualizacja dla Swift 3 / Xcode 8 beta 6: także „zmodernizowana” do użytku guardi nowy Datatyp wartości:

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.
źródło
1
@Matt: To nie ma znaczenia. Możesz umieścić rozszerzenie w tym samym pliku Swift co „główna klasa” lub w oddzielnym pliku. - Właściwie nie potrzebujesz przedłużenia. Możesz dodać generate()funkcję do klasy StreamReader i zadeklarować ją jako class StreamReader : Sequence { ... }. Ale wydaje się, że dobrym stylem Swift jest używanie rozszerzeń dla oddzielnych elementów funkcjonalności.
Martin R,
1
@zanzoken: Jakiego adresu URL używasz? Powyższy kod działa tylko dla adresów URL plików . Nie można go używać do odczytu z ogólnego adresu URL serwera. Porównaj stackoverflow.com/questions/26674182/ ... i moje komentarze pod pytaniem.
Martin R
2
@zanzoken: mój kod jest przeznaczony dla plików tekstowych i oczekuje, że plik będzie używał określonego kodowania (domyślnie UTF-8). Jeśli masz plik z dowolnymi bajtami binarnymi (na przykład plik obrazu), konwersja danych-> ciągów zakończy się niepowodzeniem.
Martin R
1
@zanzoken: Czytanie linii skanowania z obrazu to zupełnie inny temat i nie ma nic wspólnego z tym kodem, przepraszam. Jestem pewien, że można to zrobić na przykład metodami CoreGraphics, ale nie mam dla Ciebie natychmiastowej referencji.
Martin R
2
@DCDCwhile !aStreamReader.atEof { try autoreleasepool { guard let line = aStreamReader.nextLine() else { return } ...code... } }
Eporediese
26

Wydajna i wygodna klasa do czytania pliku tekstowego wiersz po wierszu (Swift 4, Swift 5)

Uwaga: ten kod jest niezależny od platformy (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
      }
   }
}

Stosowanie:

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

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

Repozytorium na github

Andy C.
źródło
6

Swift 4.2 Bezpieczna składnia

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

Stosowanie:

guard let reader = LineReader(path: "/Path/to/file.txt") else {
    return
}
reader.forEach { line in
    print(line.trimmingCharacters(in: .whitespacesAndNewlines))      
}
Wiaczesław
źródło
4

Spóźniłem się na mecz, ale oto mała klasa, którą napisałem w tym celu. Po kilku różnych próbach (spróbuj podklasy NSInputStream) stwierdziłem, że jest to rozsądne i proste podejście.

Pamiętaj o tym #import <stdio.h>w swoim nagłówku mostkowania.

// 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
źródło
Podoba mi się to, ale nadal można to poprawić. Tworzenie wskaźników za pomocą withCString nie jest konieczne (i naprawdę niebezpieczne), możesz po prostu zadzwonić return fopen(self.path, self.mode). Można dodać sprawdzenie, czy plik naprawdę można otworzyć, obecnie readline()po prostu się zawiesi. UnsafePointer<CChar>Nie potrzebna jest obsada. Wreszcie przykład użycia nie kompiluje się.
Martin R
4

Ta funkcja pobiera adres URL pliku i zwraca sekwencję, która zwróci każdy wiersz pliku, leniwie je czytając. Działa ze Swift 5. Opiera się na podstawowym 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
    }
  })
}

Na przykład, oto jak można go użyć do wydrukowania każdego wiersza pliku o nazwie „foo” w pakiecie aplikacji:

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

Opracowałem tę odpowiedź, modyfikując odpowiedź Alexa Browna, aby usunąć wyciek pamięci wspomniany w komentarzu Martina R., oraz aktualizując ją do wersji Swift 5.

glony
źródło
2

Wypróbuj odpowiedź lub przeczytaj Podręcznik programowania strumienia w systemie Mac OS .

Może się okazać, że wydajność będzie faktycznie lepsza przy użyciu stringWithContentsOfURL , ponieważ praca z danymi opartymi na pamięci (lub mapowanymi w pamięci) będzie szybsza niż z danymi na dysku.

Wykonywanie go na innym wątku jest dobrze udokumentowane, również np. Tutaj . .

Aktualizacja

Jeśli nie chcesz czytać wszystkiego od razu i nie chcesz używać NSStreams, prawdopodobnie będziesz musiał użyć wejścia / wyjścia pliku na poziomie C. Jest wiele powodów, aby tego nie robić - blokowanie, kodowanie znaków, obsługa błędów we / wy, szybkość, by wymienić tylko kilka - po to są biblioteki Foundation. Poniżej naszkicowałem prostą odpowiedź, która dotyczy tylko danych 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)
}
Grimxn
źródło
Doceniam sugestie, ale szczególnie szukam kodu w języku Swift. Poza tym chcę pracować z jedną linią naraz, a nie wszystkimi liniami naraz.
Matt,
Czy chcesz pracować z jedną linią, a następnie zwolnić ją i przeczytać następną? Musiałbym pomyśleć, że szybciej będzie pracować z nim w pamięci. Czy muszą być przetwarzane w kolejności? Jeśli nie, możesz użyć bloku wyliczenia, aby radykalnie przyspieszyć przetwarzanie tablicy.
macshome
Chciałbym pobrać kilka wierszy naraz, ale niekoniecznie będę musiał wczytywać wszystkie. Jeśli chodzi o porządek, nie jest to krytyczne, ale byłoby pomocne.
Matt,
Co się stanie, jeśli rozszerzysz case 0...127znaki spoza zestawu ASCII?
Matt
1
Cóż, to naprawdę zależy od tego, jakie kodowanie znaków masz w swoich plikach. Jeśli są jednym z wielu formatów Unicode, musisz go zakodować, jeśli są jednym z wielu systemów „stron kodowych” komputerów przed Unicode, musisz to zdekodować. Biblioteki Fundacji robią to wszystko za Ciebie, dużo pracy na własną rękę.
Grimxn,
2

Okazuje się, że dobre, stare, fascynujące C API jest całkiem wygodne w Swift, gdy już poznasz UnsafePointer. Oto prosty kot, który czyta ze standardowego wejścia i drukuje na standardowe wyjście wiersz po wierszu. Nie potrzebujesz nawet Fundacji. Darwin wystarczy:

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
źródło
1
W ogóle nie radzi sobie z obsługą „po linii”. Czyści dane wejściowe na wyjście i nie rozpoznaje różnic między zwykłymi znakami a znakami końca linii. Oczywiście dane wyjściowe składają się z tych samych wierszy, co wejście, ale to dlatego, że znak nowej linii również jest pomijany.
Alex Brown
3
@AlexBrown: To nieprawda. fgets()czyta znaki do (włącznie) znaku nowej linii (lub EOF). A może źle zrozumiałem twój komentarz?
Martin R
@Martin R, proszę, jak by to wyglądało w Swift 4/5? Potrzebuję czegoś tak prostego do odczytania pliku wiersz po wierszu -
gbenroscience
1

Lub możesz po prostu użyć Generator:

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

Wypróbujmy to

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

Jest prosty, leniwy i łatwy do połączenia z innymi szybkimi elementami, takimi jak wyliczanie i funktory, takie jak mapowanie, redukcja, filtrowanie; za pomocą lazy()opakowania.


Uogólnia się to dla wszystkich FILEjako:

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

zwany jak

for line in byLine(stdin) { ... }
Alex Brown
źródło
Wielkie dzięki dla nieobecnej odpowiedzi, która dała mi kod getline!
Alex Brown
1
Oczywiście całkowicie ignoruję kodowanie. Pozostawione jako ćwiczenie dla czytelnika.
Alex Brown
Zauważ, że twój kod przecieka pamięć, ponieważ getline()przydziela bufor dla danych.
Martin R
1

(Uwaga: używam Swift 3.0.1 na Xcode 8.2.1 z macOS Sierra 10.12.3)

Wszystkie odpowiedzi, które tu widziałem, pominęły, że mógł szukać LF lub CRLF. Jeśli wszystko pójdzie dobrze, może po prostu dopasować się do LF i sprawdzić, czy na końcu zwrócony ciąg jest dodatkowy CR. Ale ogólne zapytanie obejmuje wiele ciągów wyszukiwania. Innymi słowy, separatorem musi być a Set<String>, gdzie zestaw nie jest pusty ani nie zawiera pustego ciągu zamiast pojedynczego ciągu.

Podczas mojej pierwszej próby w zeszłym roku próbowałem zrobić „właściwą rzecz” i poszukać ogólnego zestawu ciągów. To było zbyt trudne; potrzebujesz pełnego parsera i maszyn stanowych i tym podobnych. Zrezygnowałem z tego i projektu, którego był częścią.

Teraz ponownie wykonuję projekt i znów stoję przed tym samym wyzwaniem. Teraz przejdę do wyszukiwania twardego kodu na CR i LF. Nie sądzę, żeby ktokolwiek musiał szukać takich dwóch częściowo niezależnych i częściowo zależnych znaków poza analizą CR / LF.

Używam metod wyszukiwania dostarczonych przez Data , więc nie robię tutaj kodowania ciągów i innych rzeczy. Tylko surowe przetwarzanie binarne. Załóżmy tylko, że mam tutaj superset ASCII, taki jak ISO Latin-1 lub UTF-8. Możesz obsługiwać kodowanie ciągów w następnej wyższej warstwie i zastanawiasz się, czy CR / LF z dołączonymi dodatkowymi punktami kodowymi nadal liczy się jako CR czy LF.

Algorytm: po prostu szukaj dalej następnej CR i następnej LF z bieżącego przesunięcia bajtów.

  • Jeśli żadne z nich nie zostanie znalezione, rozważ następny ciąg danych od bieżącego przesunięcia do końca danych. Zwróć uwagę, że długość terminatora wynosi 0. Oznacz to jako koniec pętli odczytu.
  • Jeśli LF zostanie znaleziony jako pierwszy lub tylko LF zostanie znaleziony, rozważ następny ciąg danych od bieżącego przesunięcia do LF. Zwróć uwagę, że długość terminatora wynosi 1. Przesuń przesunięcie za LF.
  • Jeśli zostanie znaleziona tylko CR, zrób jak przypadek LF (tylko z inną wartością bajtu).
  • W przeciwnym razie otrzymaliśmy CR, a następnie LF.
    • Jeśli oba sąsiadują ze sobą, uchwyt jak w przypadku LF, z tym że długość terminatora będzie wynosić 2.
    • Jeśli między nimi jest jeden bajt, a wspomniany bajt to również CR, otrzymaliśmy komunikat „Programista Windows napisał plik binarny \ r \ n w trybie tekstowym, co daje problem \ r \ r \ n". Obsługuj to również jak w przypadku LF, z wyjątkiem terminatora o długości 3.
    • W przeciwnym razie CR i LF nie są połączone i działają jak obudowa just-CR.

Oto kod:

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
    }

}

Oczywiście, jeśli masz Datablok o długości, która stanowi co najmniej znaczący ułamek gigabajta, otrzymasz trafienie, gdy nie będzie już CR ani LF z bieżącego przesunięcia bajtów; zawsze bezowocne poszukiwanie do końca podczas każdej iteracji. Odczytywanie danych w fragmentach pomogłoby:

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

}

Musisz samodzielnie wymieszać te pomysły, ponieważ jeszcze tego nie zrobiłem. Rozważać:

  • Oczywiście musisz wziąć pod uwagę linie całkowicie zawarte w porcji.
  • Ale musisz sobie z tym poradzić, gdy końce linii znajdują się w sąsiednich fragmentach.
  • Lub gdy punkty końcowe mają co najmniej jeden fragment między nimi
  • Dużą komplikacją jest sytuacja, gdy wiersz kończy się sekwencją wielobajtową, ale ta sekwencja obejmuje dwa fragmenty! (Wiersz kończący się po prostu CR, który jest również ostatnim bajtem w porcji, jest równoważnym przypadkiem, ponieważ musisz przeczytać następną porcję, aby sprawdzić, czy twój just-CR to w rzeczywistości CRLF lub CR-CRLF. fragment kończy się na CR-CR.)
  • I musisz sobie z tym poradzić, gdy nie ma już terminatorów z bieżącego przesunięcia, ale koniec danych znajduje się w późniejszym fragmencie.

Powodzenia!

CTMacUser
źródło
1

Idąc za odpowiedzią @ dankogai , wprowadziłem kilka modyfikacji dla 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()

To zadziałało dla mnie.

Dzięki

gbenroscience
źródło
0

Chciałem wersji, która nie modyfikuje ciągle bufora lub duplikatu kodu, ponieważ oba są nieefektywne i pozwalałyby na bufor dowolnego rozmiaru (w tym 1 bajt) i dowolny ogranicznik. Posiada jedną metodę publiczną: readline(). Wywołanie tej metody zwróci wartość String następnego wiersza lub zero w 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)
  }
}

Nazywa się to następująco:

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

while let s = myStream.readLine() {
  print(s)
}
Szyszka
źródło