Прочитайте файл / 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)
}

Як би я це зробив у Свіфті?

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


Ви хочете записати масив на диск під час руху або просто дозволити ОС обробляти його з пам'яттю? Чи буде у Mac, на якому він працює, достатньо оперативної пам'яті, щоб ви могли зіставити файл і працювати з ним таким чином? Зробити кілька завдань досить просто, і я припускаю, що у вас може бути кілька завдань, які починають читати файл у різних місцях.
macshome

Відповіді:


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 line in aStreamReader {
    print(line)
}

впроваджуючи SequenceTypeпротокол (порівняйте http://robots.thoughtbot.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 за замовчуванням). Якщо у вас є файл із довільними двійковими байтами (наприклад, файл зображення), тоді перетворення даних-> рядка не вдасться.
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, тоді вам, мабуть, доведеться використовувати файл-рівень рівня введення-виведення. Є багато причин, щоб цього не робити - блокування, кодування символів, обробка помилок вводу-виводу, швидкість, щоб назвати, але декілька - для цього призначені бібліотеки 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)
}

Я ціную пропозицію, але я спеціально шукаю код у Swift. Крім того, я хочу працювати з одним рядком за раз, а не з усіма рядками одночасно.
Matt

Отже, ви хочете працювати з одним рядком, а потім випустити його і прочитати наступний у? Мені потрібно було б подумати, що швидше працювати з цим у пам’яті. Чи потрібно їх обробляти в порядку? Якщо ні, ви можете використовувати блок переліку, щоб різко прискорити обробку масиву.
macshome

Я хотів би взяти кілька рядків одночасно, але мені не обов'язково потрібно буде завантажувати всі рядки. Що стосується порядку, це не критично, але було б корисно.
Matt

Що станеться, якщо розширити case 0...127символи, що не належать до ASCII?
Matt

1
Ну, це насправді залежить від того, яке кодування символів у вас є у файлах. Якщо вони є одним із багатьох форматів Unicode, вам потрібно буде кодувати це, якщо вони є однією з багатьох систем "кодової сторінки" до ПК перед Unicode, вам потрібно буде це декодувати. Бібліотеки Фонду роблять все це за вас, це багато роботи самостійно.
Grimxn

2

Виявляється, добрий старомодний API C досить зручний у Swift, коли ви зачепили UnsafePointer. Ось простий котик, який читає з stdin і друкує до stdout по черзі. Вам навіть не потрібен Фонд. Дарвіна достатньо:

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
Не вдається обробити "за рядком" взагалі. Він перекриває вихідні дані і не розпізнає різницю між звичайними символами та символами кінця рядка. Очевидно, що вихідні дані складаються з тих самих рядків, що і вхідні, але це тому, що новий рядок також є блітим.
Алекс Браун

3
@AlexBrown: Це неправда. fgets()читає символи до (включаючи) символу нового рядка (або EOF). Або я неправильно розумію ваш коментар?
Martin R

@Martin R, будь ласка, як би це виглядало в Swift 4/5? Мені потрібно щось таке просте, щоб прочитати файл, рядок за рядком -
gbenroscience

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
Очевидно, що я повністю ігнорую кодування. Залишилося вправою для читача.
Алекс Браун

Зверніть увагу, що ваш код втрачає пам’ять, оскільки getline()виділяє буфер для даних.
Martin R

1

(Примітка: я використовую Swift 3.0.1 на Xcode 8.2.1 з macOS Sierra 10.12.3)

Всі відповіді, які я тут бачив, пропускали, що він міг шукати НЧ чи КРЛ. Якщо все піде добре, він / вона може просто збігатися на 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. Перемістіть зміщення на значення після НЧ.
  • Якщо знайдено лише CR, зробіть так, як LF (просто з іншим байтовим значенням).
  • В іншому випадку ми отримали CR, за яким слідує LF.
    • Якщо ці два суміжні, то обробляйте, як корпус НЧ, за винятком того, що довжина термінатора буде 2.
    • Якщо між ними є один байт, і зазначений байт також є CR, то ми отримали "Розробник Windows написав двійковий файл \ r \ n, перебуваючи в текстовому режимі, даючи проблему \ r \ r \ n". Також обробляйте це як корпус НЧ, за винятком того, що довжина термінатора буде 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.)
  • І вам потрібно обробляти, коли з вашого поточного зміщення більше не буде термінаторів, але кінець даних буде пізніше.

Удачі!


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)
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.