Построчное чтение файла / URL в Swift

80

Я пытаюсь прочитать файл, указанный в, NSURLи загрузить его в массив с элементами, разделенными символом новой строки \n.

Вот как я это делал до сих пор:

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
}

Я не очень доволен этим по нескольким причинам. Во-первых, я работаю с файлами размером от нескольких килобайт до сотен МБ. Как вы понимаете, работа со строками такого размера выполняется медленно и громоздко. Во-вторых, это приводит к зависанию пользовательского интерфейса при его выполнении - опять же, не очень хорошо.

Я изучал возможность запуска этого кода в отдельном потоке, но у меня возникли проблемы с этим, и, кроме того, он все еще не решает проблему работы с огромными строками.

Я бы хотел сделать что-то вроде следующего псевдокода:

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

Как мне это сделать в Swift?

Несколько примечаний о файлах, из которых я читаю: Все файлы состоят из коротких (<255 символов) строк, разделенных символом \nили \r\n. Длина файлов составляет от ~ 100 строк до более 50 миллионов строк. Они могут содержать европейские символы и / или символы с диакритическими знаками.

Мэтт
источник
Вы хотите записывать массив на диск по ходу работы или просто позволить ОС обрабатывать его с помощью памяти? Будет ли на компьютере Mac достаточно оперативной памяти, чтобы вы могли сопоставить файл и работать с ним таким образом? Выполнить несколько задач достаточно просто, и я полагаю, что у вас может быть несколько заданий, которые начнут читать файл в разных местах.
macshome 07

Ответы:

150

(Сейчас код предназначен для Swift 2.2 / Xcode 7.3. Более старые версии можно найти в истории редактирования, если это кому-то понадобится. Обновленная версия для Swift 3 предоставляется в конце.)

Следующий код Swift во многом вдохновлен различными ответами на вопрос, как читать данные из NSFileHandle построчно? . Он читает из файла по частям и преобразует полные строки в строки.

Разделитель строк по умолчанию ( \n), кодировка строки (UTF-8) и размер блока (4096) могут быть установлены с помощью дополнительных параметров.

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

Применение:

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

Вы даже можете использовать ридер с циклом for-in

for line in aStreamReader {
    print(line)
}

путем реализации SequenceTypeпротокола (сравните http://robots.oughttbot.com/swift-sequences ):

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

Обновление для Swift 3 / Xcode 8 beta 6: Также «модернизировано» для использования guardи новый 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()
        }
    }
}
Мартин Р
источник
1
@Matt: Это не имеет значения. Вы можете поместить расширение в тот же файл Swift, что и «основной класс», или в отдельный файл. - На самом деле расширение вам не нужно. Вы можете добавить generate()функцию в класс StreamReader и объявить ее как class StreamReader : Sequence { ... }. Но кажется хорошим стилем Swift использовать расширения для отдельных частей функциональности.
Martin R
1
@zanzoken: Какой URL вы используете? Приведенный выше код работает только с URL-адресами файлов . Его нельзя использовать для чтения с общего URL-адреса сервера. Сравните stackoverflow.com/questions/26674182/… и мои комментарии под вопросом.
Martin R
2
@zanzoken: Мой код предназначен для текстовых файлов и ожидает, что файл будет использовать указанную кодировку (по умолчанию UTF-8). Если у вас есть файл с произвольными двоичными байтами (например, файл изображения), преобразование data-> string завершится ошибкой.
Martin R
1
@zanzoken: Чтение строк развертки с изображения - это совершенно другая тема и не имеет ничего общего с этим кодом, извините. Я уверен, что это можно сделать, например, с помощью методов CoreGraphics, но у меня не будет для вас немедленной ссылки.
Martin R
2
@DCDCwhile !aStreamReader.atEof { try autoreleasepool { guard let line = aStreamReader.nextLine() else { return } ...code... } }
Eporediese
26

Эффективный и удобный класс для построчного чтения текстовых файлов (Swift 4, Swift 5)

Примечание. Этот код не зависит от платформы (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
      }
   }
}

Применение:

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

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

Репозиторий на github

Энди С
источник
6

Swift 4.2 Безопасный синтаксис

class LineReader {

    let path: String

    init?(path: String) {
        self.path = path
        guard let file = fopen(path, "r") else {
            return nil
        }
        self.file = file
    }
    deinit {
        fclose(file)
    }

    var nextLine: String? {
        var line: UnsafeMutablePointer<CChar>?
        var linecap = 0
        defer {
            free(line)
        }
        let status = getline(&line, &linecap, file)
        guard status > 0, let unwrappedLine = line else {
            return nil
        }
        return String(cString: unwrappedLine)
    }

    private let file: UnsafeMutablePointer<FILE>
}

extension LineReader: Sequence {
    func makeIterator() -> AnyIterator<String> {
        return AnyIterator<String> {
            return self.nextLine
        }
    }
}

Применение:

guard let reader = LineReader(path: "/Path/to/file.txt") else {
    return
}
reader.forEach { line in
    print(line.trimmingCharacters(in: .whitespacesAndNewlines))      
}
Вячеслав
источник
4

Я опаздываю в игру, но вот небольшой класс, который я написал для этой цели. После нескольких разных попыток (попробуйте NSInputStreamсоздать подкласс ) я обнаружил, что это разумный и простой подход.

Не забудьте #import <stdio.h>в заголовке моста.

// 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)
    }
}
Альбин Стиго
источник
Мне это нравится, но все еще можно улучшить. Создавать указатели с использованием withCStringне обязательно (и действительно небезопасно), вы можете просто вызвать return fopen(self.path, self.mode). Можно добавить проверку, действительно ли файл может быть открыт, в настоящее время readline()просто вылетает. UnsafePointer<CChar>Бросок не нужен. Наконец, ваш пример использования не компилируется.
Martin R
4

Эта функция принимает URL-адрес файла и возвращает последовательность, которая будет возвращать каждую строку файла, лениво читая их. Он работает со Swift 5. Он зависит от базового 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
    }
  })
}

Так, например, вот как вы могли бы использовать его для печати каждой строки файла с именем «foo» в вашем пакете приложений:

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

Я разработал этот ответ, изменив ответ Алекса Брауна, чтобы удалить утечку памяти, упомянутую в комментарии Мартина Р., и обновив его до Swift 5.

водоросль
источник
2

Попробуйте этот ответ или прочитайте Руководство по программированию Mac OS Stream. .

Вы можете обнаружить, что производительность на самом деле будет лучше, если stringWithContentsOfURL , так как с данными на основе памяти (или отображенными в память) будет быстрее работать, чем с данными на диске.

Выполнение его в другом потоке также хорошо документировано, например, здесь .

Обновить

Если вы не хотите читать все сразу и не хотите использовать NSStreams, вам, вероятно, придется использовать файловый ввод-вывод C-уровня. Есть много причин не делать этого - блокировка, кодировка символов, обработка ошибок ввода-вывода, скорость называния, но некоторые из них - это то, для чего нужны библиотеки Foundation. Ниже я набросал простой ответ, который касается только данных 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
источник
Я ценю предложения, но я специально ищу код в Swift. Кроме того, я хочу работать с одной строкой за раз, а не со всеми строками сразу.
Мэтт
Итак, вы хотите работать с одной строкой, а затем отпустить ее и прочитать следующую? Я бы подумал, что с ним будет быстрее работать в памяти. Нужно ли их обрабатывать по порядку? В противном случае вы можете использовать блок перечисления, чтобы значительно ускорить обработку массива.
macshome 07
Я хотел бы захватить сразу несколько строк, но мне не обязательно загружать все строки. Что касается порядка, то это не критично, но было бы полезно.
Мэтт
Что произойдет, если расширить case 0...127символы на символы, отличные от ASCII?
Мэтт
1
Ну, это действительно зависит от того, какая кодировка символов у вас в файлах. Если они являются одним из многих форматов Unicode, вам нужно будет кодировать для этого, если они являются одной из многих систем «кодовых страниц» ПК до Unicode, вам нужно будет его декодировать. Библиотеки Foundation сделают все это за вас, это большая работа самостоятельно.
Grimxn 09
2

Оказывается, старый добрый C API довольно удобен в Swift, если вы попробуете UnsafePointer. Вот простой кот, который читает из стандартного ввода и выводит его построчно. Вам даже не нужен Foundation. Дарвина достаточно:

import Darwin
let bufsize = 4096
// let stdin = fdopen(STDIN_FILENO, "r") it is now predefined in Darwin
var buf = UnsafePointer<Int8>.alloc(bufsize)
while fgets(buf, Int32(bufsize-1), stdin) {
    print(String.fromCString(CString(buf)))
}
buf.destroy()
данкогай
источник
1
Совершенно не справляется "построчно". Он преобразует входные данные в выходные и не распознает разницу между обычными символами и символами конца строки. Очевидно, что вывод состоит из тех же строк, что и ввод, но это потому, что новая строка также переносится.
Alex Brown
3
@AlexBrown: Это неправда. fgets()читает символы до (включительно) символа новой строки (или EOF). Или я неправильно понимаю ваш комментарий?
Martin R
@Martin R, пожалуйста, как это будет выглядеть в Swift 4/5? Мне нужно что-то
настолько
1

Или вы можете просто использовать Generator:

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

Давай попробуем

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

Это просто, лениво и легко связать с другими быстрыми вещами, такими как перечислители и функторы, такие как map, reduce, filter; используя lazy()обертку.


Он обобщается на всех FILEкак:

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

называется как

for line in byLine(stdin) { ... }
Алекс Браун
источник
Большое спасибо уже ушедшему ответу, который дал мне код getline!
Алекс Браун
1
Очевидно, я полностью игнорирую кодировку. Оставил в качестве упражнения для читателя.
Alex Brown
Обратите внимание, что ваш код getline()приводит к утечке памяти, поскольку выделяет буфер для данных.
Martin R
1

(Примечание: я использую Swift 3.0.1 в Xcode 8.2.1 с macOS Sierra 10.12.3)

Все ответы, которые я здесь видел, упускали из виду, что он мог искать LF или CRLF. Если все пойдет хорошо, он / она может просто сопоставить LF и проверить возвращенную строку на наличие лишнего CR в конце. Но общий запрос включает несколько строк поиска. Другими словами, разделитель должен быть a Set<String>, где набор не пуст и не содержит пустую строку, а не одну строку.

С моей первой попытки в прошлом году я попытался поступить «правильно» и найти общий набор строк. Это было слишком сложно; вам нужен полноценный парсер, конечные автоматы и все такое. Я отказался от этого и от проекта, частью которого он был.

Теперь я снова занимаюсь этим проектом и снова сталкиваюсь с той же проблемой. Теперь я собираюсь выполнить жесткий поиск по CR и LF. Я не думаю, что кому-то понадобится искать два полунезависимых и полузависимых символа, подобных этому, вне анализа CR / LF.

Я использую методы поиска, предоставленные Data , поэтому я не использую здесь строковые кодировки и прочее. Просто необработанная двоичная обработка. Предположим, у меня есть надмножество ASCII, например ISO Latin-1 или UTF-8. Вы можете обрабатывать строковое кодирование на следующем, более высоком уровне, и вы можете понять, считается ли CR / LF с присоединенными вторичными кодовыми точками как CR или LF.

Алгоритм: просто продолжайте поиск следующего CR и следующего LF от вашего текущего байтового смещения.

  • Если ни один из них не найден, считайте, что следующая строка данных находится от текущего смещения до конца данных. Обратите внимание, что длина терминатора равна 0. Отметьте это как конец цикла чтения.
  • Если сначала найден LF или найден только LF, считайте, что следующая строка данных будет от текущего смещения до LF. Обратите внимание, что длина ограничителя равна 1. Переместите смещение после LF.
  • Если найден только CR, действуйте как в случае LF (только с другим значением байта).
  • В противном случае мы получили CR, за которым следует LF.
    • Если они находятся рядом, то обработайте как в случае LF, за исключением того, что длина терминатора будет равна 2.
    • Если между ними есть один байт, и указанный байт также является CR, то мы получили «разработчик Windows написал двоичный файл \ r \ n в текстовом режиме, что дало проблему \ r \ r \ n». Также обращайтесь с ним, как с корпусом LF, за исключением того, что длина терминатора будет 3.
    • В противном случае CR и LF не подключены и обрабатываются как в случае с просто CR.

Вот код для этого:

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
    }

}

Конечно, если у вас есть Dataблок, длина которого составляет, по крайней мере, значительную долю гигабайта, вы получите удар всякий раз, когда из текущего байтового смещения больше не существует CR или LF; всегда бесплодно ищет до конца на каждой итерации. Чтение данных по частям может помочь:

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

}

Вы должны сами смешать эти идеи, так как я еще этого не сделал. Рассматривать:

  • Конечно, вы должны учитывать строки, полностью содержащиеся в чанке.
  • Но вам нужно обрабатывать, когда концы строки находятся в соседних блоках.
  • Или когда между конечными точками есть хотя бы один кусок
  • Большая сложность возникает, когда строка заканчивается многобайтовой последовательностью, но указанная последовательность состоит из двух частей! (Строка, заканчивающаяся только на CR, которая также является последним байтом в фрагменте, является эквивалентным случаем, поскольку вам нужно прочитать следующий фрагмент, чтобы увидеть, действительно ли ваш just-CR является CRLF или CR-CRLF. Есть аналогичные махинации, когда чанк заканчивается CR-CR.)
  • И вам нужно обработать, когда от вашего текущего смещения больше нет терминаторов, но конец данных находится в более позднем фрагменте.

Удачи!

CTMacUser
источник
1

После ответа @dankogai я внес несколько изменений для 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()

Это сработало для меня.

благодаря

наука
источник
0

Мне нужна была версия, которая не изменяла бы буфер или дублированный код постоянно, поскольку оба они неэффективны и позволяли бы использовать буфер любого размера (включая 1 байт) и любой разделитель. Он имеет один публичный метод: readline(). Вызов этого метода вернет строковое значение следующей строки или ноль в 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)
  }
}

Он называется следующим образом:

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

while let s = myStream.readLine() {
  print(s)
}
Шишка
источник