Đọc từng dòng một tệp / URL trong Swift


80

Tôi đang cố gắng đọc một tệp được cung cấp trong một NSURLvà tải nó vào một mảng, với các mục được phân tách bằng ký tự dòng mới \n.

Đây là cách tôi đã làm cho đến nay:

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
}

Tôi không hài lòng với điều này vì một vài lý do. Thứ nhất, tôi đang làm việc với các tệp có kích thước từ vài kilobyte đến hàng trăm MB. Như bạn có thể tưởng tượng, làm việc với các chuỗi lớn như thế này rất chậm và khó sử dụng. Thứ hai, điều này đóng băng giao diện người dùng khi nó đang thực thi - một lần nữa, không tốt.

Tôi đã xem xét việc chạy mã này trong một chuỗi riêng biệt, nhưng tôi đang gặp sự cố với điều đó và bên cạnh đó, nó vẫn không giải quyết được vấn đề xử lý các chuỗi lớn.

Những gì tôi muốn làm là một cái gì đó dọc theo các dòng của mã giả sau:

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

Làm cách nào để thực hiện điều này trong Swift?

Một vài lưu ý về các tệp mà tôi đang đọc: Tất cả các tệp đều bao gồm các chuỗi ngắn (<255 ký tự) được phân tách bằng một trong hai \nhoặc \r\n. Độ dài của tệp từ ~ 100 dòng đến hơn 50 triệu dòng. Chúng có thể chứa các ký tự Châu Âu và / hoặc các ký tự có dấu.


Bạn muốn ghi mảng ra đĩa khi di chuyển hay chỉ để hệ điều hành xử lý bằng bộ nhớ? Liệu máy Mac đang chạy nó có đủ ram để bạn có thể ánh xạ tệp và làm việc với nó theo cách đó không? Nhiều công việc đủ dễ dàng để thực hiện và tôi cho rằng bạn có thể có nhiều công việc bắt đầu đọc tệp ở những nơi khác nhau.
macshome

Câu trả lời:


150

(Hiện tại, mã dành cho Swift 2.2 / Xcode 7.3. Các phiên bản cũ hơn có thể được tìm thấy trong lịch sử chỉnh sửa nếu ai đó cần. Phiên bản cập nhật cho Swift 3 được cung cấp ở cuối.)

Đoạn mã Swift sau đây được truyền cảm hứng rất nhiều từ các câu trả lời khác nhau cho Cách đọc dữ liệu từ NSFileHandle từng dòng? . Nó đọc từ tệp theo từng đoạn và chuyển đổi các dòng hoàn chỉnh thành chuỗi.

Dấu phân cách dòng mặc định ( \n), mã hóa chuỗi (UTF-8) và kích thước đoạn (4096) có thể được đặt bằng các tham số tùy chọn.

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

Sử dụng:

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

Bạn thậm chí có thể sử dụng trình đọc với vòng lặp for-in

for line in aStreamReader {
    print(line)
}

bằng cách triển khai SequenceTypegiao thức (so sánh http://robots.thoughtbot.com/swift-sequences ):

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

Bản cập nhật cho Swift 3 / Xcode 8 beta 6: Cũng được "hiện đại hóa" để sử dụng guardDataloại giá trị mới :

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: Không thành vấn đề. Bạn có thể đặt phần mở rộng trong cùng một tệp Swift với "lớp chính" hoặc trong một tệp riêng biệt. - Thực ra bạn không thực sự cần một phần mở rộng. Bạn có thể thêm generate()chức năng vào lớp StreamReader và khai báo rằng class StreamReader : Sequence { ... }. Nhưng có vẻ là phong cách Swift tốt khi sử dụng các phần mở rộng cho các phần chức năng riêng biệt.
Martin R

1
@zanzoken: Bạn đang sử dụng loại URL nào? Đoạn mã trên chỉ hoạt động cho URL của tệp . Nó không thể được sử dụng để đọc từ một URL máy chủ chung. So sánh stackoverflow.com/questions/26674182/… và nhận xét của tôi dưới câu hỏi.
Martin R

2
@zanzoken: Mã của tôi dành cho các tệp văn bản và mong đợi tệp sử dụng mã hóa được chỉ định (UTF-8 theo mặc định). Nếu bạn có tệp có byte nhị phân tùy ý (chẳng hạn như tệp hình ảnh) thì quá trình chuyển đổi chuỗi dữ liệu-> sẽ không thành công.
Martin R

1
@zanzoken: Đọc dòng quét từ một hình ảnh là một chủ đề hoàn toàn khác và không liên quan gì đến mã này, xin lỗi. Tôi chắc chắn rằng nó có thể được thực hiện chẳng hạn với các phương pháp CoreGraphics, nhưng tôi không có tài liệu tham khảo ngay lập tức cho bạn.
Martin R

2
@DCDCwhile !aStreamReader.atEof { try autoreleasepool { guard let line = aStreamReader.nextLine() else { return } ...code... } }
Eporediese

26

Lớp hiệu quả và thuận tiện để đọc từng dòng tệp văn bản (Swift 4, Swift 5)

Lưu ý: Mã này độc lập với nền tảng (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
      }
   }
}

Sử dụng:

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

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

Kho lưu trữ trên github


6

Swift 4.2 Cú pháp an toàn

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

Sử dụng:

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

4

Tôi đến muộn với trò chơi, nhưng đây là lớp học nhỏ tôi đã viết cho mục đích đó. Sau một số lần thử khác nhau (cố gắng phân lớp NSInputStream), tôi thấy đây là một cách tiếp cận hợp lý và đơn giản.

Hãy nhớ #import <stdio.h>trong tiêu đề bắc cầu của bạn.

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

Tôi thích điều này, nhưng nó vẫn có thể được cải thiện. Việc tạo con trỏ bằng cách sử dụng withCStringlà không cần thiết (và thực sự không an toàn), bạn có thể chỉ cần gọi return fopen(self.path, self.mode). Người ta có thể thêm kiểm tra xem tệp thực sự có thể mở được hay không, hiện tại readline()sẽ chỉ bị sập. Diễn UnsafePointer<CChar>viên không cần thiết. Cuối cùng, ví dụ sử dụng của bạn không biên dịch.
Martin R,

4

Hàm này lấy một URL của tệp và trả về một chuỗi sẽ trả về mọi dòng của tệp, đọc chúng một cách lười biếng. Nó hoạt động với Swift 5. Nó dựa trên cơ sở 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
    }
  })
}

Ví dụ: đây là cách bạn sẽ sử dụng nó để in mọi dòng của tệp có tên "foo" trong gói ứng dụng của bạn:

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

Tôi đã phát triển câu trả lời này bằng cách sửa đổi câu trả lời của Alex Brown để xóa lỗi rò rỉ bộ nhớ được đề cập bởi bình luận của Martin R và bằng cách cập nhật nó lên cho Swift 5.


2

Hãy thử câu trả lời này hoặc đọc Hướng dẫn lập trình dòng Mac OS .

Tuy nhiên, bạn có thể thấy rằng hiệu suất thực sự sẽ tốt hơn khi sử dụng stringWithContentsOfURLdữ liệu dựa trên bộ nhớ (hoặc ánh xạ bộ nhớ) nhanh hơn so với dữ liệu dựa trên đĩa.

Việc thực thi nó trên một luồng khác cũng được ghi chép lại, chẳng hạn ở đây .

Cập nhật

Nếu bạn không muốn đọc tất cả cùng một lúc và bạn không muốn sử dụng NSStreams, thì có thể bạn sẽ phải sử dụng I / O tệp cấp C. Có nhiều lý do để không làm điều này - chặn, mã hóa ký tự, xử lý lỗi I / O, tốc độ sang tên nhưng một vài lý do - đây là những gì các thư viện Foundation dành cho. Tôi đã phác thảo một câu trả lời đơn giản bên dưới chỉ xử lý dữ liệu 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)
}

Tôi đánh giá cao (các) đề xuất, nhưng tôi đặc biệt đang tìm kiếm mã trong Swift. Ngoài ra, tôi muốn làm việc với từng dòng một, thay vì tất cả các dòng cùng một lúc.
Matt,

Vì vậy, bạn đang muốn làm việc với một dòng sau đó thả nó ra và đọc đoạn tiếp theo? Tôi cần nghĩ rằng làm việc với nó trong bộ nhớ sẽ nhanh hơn. Chúng có cần được xử lý theo thứ tự không? Nếu không, bạn có thể sử dụng một khối liệt kê để tăng tốc đáng kể việc xử lý mảng.
macshome

Tôi muốn lấy một số dòng cùng một lúc, nhưng tôi không nhất thiết phải tải tất cả các dòng. Về thứ tự, nó không quan trọng, nhưng nó sẽ hữu ích.
Matt,

Điều gì xảy ra nếu bạn mở rộng các case 0...127ký tự không phải ASCII?
Matt,

1
Điều đó thực sự phụ thuộc vào việc bạn có mã hóa ký tự nào trong các tệp của mình. Nếu chúng là một trong nhiều định dạng của Unicode, bạn sẽ cần viết mã cho định dạng đó, nếu chúng là một trong nhiều hệ thống "trang mã" PC trước Unicode, bạn sẽ cần giải mã. Các thư viện Foundation làm tất cả những điều này cho bạn, còn rất nhiều công việc của riêng bạn.
Grimxn,

2

Hóa ra API C phiên bản cũ tốt khá thoải mái trong Swift khi bạn tìm kiếm UnsafePointer. Đây là một con mèo đơn giản đọc từ stdin và in ra stdout từng dòng một. Bạn thậm chí không cần Foundation. Darwin có đủ:

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
Không xử lý được "theo dòng". Nó chuyển dữ liệu đầu vào thành đầu ra và không nhận ra sự khác biệt giữa các ký tự bình thường và các ký tự cuối dòng. Rõ ràng, đầu ra bao gồm các dòng giống như đầu vào, nhưng đó là bởi vì dòng mới cũng bị mờ.
Alex Brown,

3
@AlexBrown: Điều đó không đúng. fgets()đọc các ký tự lên đến (và bao gồm) một ký tự dòng mới (hoặc EOF). Hay tôi đang hiểu sai nhận xét của bạn?
Martin R

@Martin R, làm ơn làm thế nào điều này sẽ trông như thế nào trong Swift 4/5? Tôi cần một cái gì đó đơn giản này để đọc một dòng tập tin bằng dòng -
gbenroscience

1

Hoặc bạn có thể đơn giản sử dụng Generator:

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

Hãy thử nó ra

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

Nó đơn giản, lười biếng và dễ dàng liên kết với những thứ nhanh chóng khác như điều tra viên và bộ chức năng như bản đồ, thu gọn, bộ lọc; bằng cách sử dụng lazy()trình bao bọc.


Nó nói chung cho tất cả FILElà:

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

được gọi là

for line in byLine(stdin) { ... }

Rất cảm ơn câu trả lời hiện đã khởi hành đã cho tôi mã getline!
Alex Brown

1
Rõ ràng là tôi đang hoàn toàn bỏ qua mã hóa. Còn lại như một bài tập cho người đọc.
Alex Brown,

Lưu ý rằng mã của bạn làm rò rỉ bộ nhớ khi getline()cấp phát bộ đệm cho dữ liệu.
Martin R

1

(Lưu ý: Tôi đang sử dụng Swift 3.0.1 trên Xcode 8.2.1 với macOS Sierra 10.12.3)

Tất cả các câu trả lời tôi thấy ở đây đều bỏ sót rằng anh ta có thể đang tìm kiếm LF hoặc CRLF. Nếu mọi thứ diễn ra tốt đẹp, anh ấy / anh ấy có thể đối sánh trên LF và kiểm tra chuỗi trả về để tìm thêm CR ở cuối. Nhưng truy vấn chung liên quan đến nhiều chuỗi tìm kiếm. Nói cách khác, dấu phân tách cần phải là a Set<String>, trong đó tập hợp không trống hoặc không chứa chuỗi trống, thay vì một chuỗi đơn.

Trong lần thử đầu tiên vào năm ngoái, tôi đã cố gắng làm "điều đúng đắn" và tìm kiếm một bộ chuỗi chung. Nó quá khó; bạn cần một trình phân tích cú pháp và máy trạng thái hoàn chỉnh và như vậy. Tôi đã từ bỏ nó và dự án đó là một phần của nó.

Bây giờ tôi đang thực hiện lại dự án và đối mặt với thách thức tương tự. Bây giờ tôi sẽ tìm kiếm mã cứng trên CR và LF. Tôi không nghĩ rằng bất cứ ai sẽ cần phải tìm kiếm trên hai ký tự nửa độc lập và nửa phụ thuộc như thế này ngoài phân tích cú pháp CR / LF.

Tôi đang sử dụng các phương pháp tìm kiếm được cung cấp bởi Datavì vậy tôi không thực hiện mã hóa chuỗi và nội dung ở đây. Chỉ xử lý nhị phân thô. Chỉ cần giả sử tôi có bộ siêu ASCII, như ISO Latinh-1 hoặc UTF-8, tại đây. Bạn có thể xử lý mã hóa chuỗi ở lớp tiếp theo cao hơn và bạn quyết định xem CR / LF có đính kèm điểm mã phụ vẫn được tính là CR hay LF hay không.

Thuật toán: chỉ cần tiếp tục tìm kiếm CR tiếp theo LF tiếp theo từ phần bù byte hiện tại của bạn.

  • Nếu cả hai đều không được tìm thấy, thì hãy coi chuỗi dữ liệu tiếp theo là từ phần bù hiện tại đến phần cuối của dữ liệu. Lưu ý rằng độ dài của dấu chấm dứt là 0. Đánh dấu đây là phần cuối của vòng lặp đọc của bạn.
  • Nếu một LF được tìm thấy trước hoặc chỉ một LF được tìm thấy, hãy coi chuỗi dữ liệu tiếp theo là từ giá trị bù hiện tại đến LF. Lưu ý rằng độ dài của đầu cuối là 1. Di chuyển phần bù đến sau LF.
  • Nếu chỉ tìm thấy một CR, hãy làm như trường hợp LF (chỉ với một giá trị byte khác).
  • Nếu không, chúng ta có CR theo sau là LF.
    • Nếu hai phần tử liền nhau, thì xử lý giống như trường hợp LF, ngoại trừ độ dài của phần tử cuối sẽ là 2.
    • Nếu có một byte giữa chúng và byte đã nói cũng là CR, thì chúng tôi nhận được thông báo "Nhà phát triển Windows đã viết một tệp nhị phân \ r \ n khi ở chế độ văn bản, gây ra sự cố \ r \ r \ n". Cũng xử lý nó giống như trường hợp LF, ngoại trừ độ dài của đầu cuối sẽ là 3.
    • Nếu không, CR và LF không được kết nối và xử lý như trường hợp just-CR.

Đây là một số mã cho điều đó:

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
    }

}

Tất nhiên, nếu bạn có một Datakhối có độ dài ít nhất là một phần đáng kể của gigabyte, bạn sẽ nhận được một lần truy cập bất cứ khi nào không còn CR hoặc LF tồn tại từ phần bù byte hiện tại; luôn tìm kiếm không kết quả cho đến cuối trong mỗi lần lặp lại. Đọc dữ liệu theo từng phần sẽ giúp:

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

}

Bạn phải tự mình trộn những ý tưởng này với nhau, vì tôi chưa làm được. Xem xét:

  • Tất nhiên, bạn phải xem xét các dòng hoàn toàn được chứa trong một đoạn.
  • Nhưng bạn phải xử lý khi các đầu của một dòng nằm trong các đoạn liền kề.
  • Hoặc khi các điểm cuối có ít nhất một đoạn giữa chúng
  • Sự phức tạp lớn là khi dòng kết thúc bằng một chuỗi nhiều byte, nhưng chuỗi này lại xếp hai phần! (Một dòng kết thúc bằng CR cũng là byte cuối cùng trong đoạn mã là một trường hợp tương đương, vì bạn cần đọc đoạn tiếp theo để xem chỉ-CR của bạn thực sự là CRLF hay CR-CRLF. Có những điều tai quái tương tự khi đoạn kết thúc bằng CR-CR.)
  • Và bạn cần xử lý khi không còn đầu cuối nào nữa từ phần bù hiện tại của bạn, nhưng phần cuối của dữ liệu nằm ở phần sau.

Chúc may mắn!


1

Theo dõi câu trả lời của @ dankogai , tôi đã thực hiện một số sửa đổi cho 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()

Điều này đã làm việc cho tôi.

Cảm ơn


0

Tôi muốn một phiên bản không liên tục sửa đổi bộ đệm hoặc mã trùng lặp, vì cả hai đều không hiệu quả và sẽ cho phép bất kỳ bộ đệm kích thước nào (bao gồm 1 byte) và bất kỳ dấu phân cách nào. Nó có một phương pháp công cộng: readline(). Gọi phương thức này sẽ trả về giá trị Chuỗi của dòng tiếp theo hoặc nil tại 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)
  }
}

Nó được gọi như sau:

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

while let s = myStream.readLine() {
  print(s)
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.