Program Tip

Swift에서 한 줄씩 파일 / URL 읽기

programtip 2020. 10. 31. 10:04
반응형

Swift에서 한 줄씩 파일 / URL 읽기


에 주어진 파일을 읽고 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
}

나는 몇 가지 이유로 이것에별로 만족스럽지 않다. 첫째, 저는 몇 킬로바이트에서 수백 MB 크기의 파일로 작업하고 있습니다. 상상할 수 있듯이, 이렇게 큰 현을 사용하는 것은 느리고 다루기 어렵습니다. 둘째, 이것은 실행 중일 때 UI를 멈 춥니 다.

이 코드를 별도의 스레드에서 실행하는 방법을 살펴 보았지만 문제가 발생했습니다. 게다가 여전히 거대한 문자열을 처리하는 문제를 해결하지 못했습니다.

내가하고 싶은 것은 다음 의사 코드의 줄을 따르는 것입니다.

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

Swift에서 어떻게이 작업을 수행 할 수 있습니까?

내가 읽고있는 파일에 대한 몇 가지 참고 사항 : 모든 파일은 \n또는로 구분 된 짧은 (<255 자) 문자열로 구성됩니다 \r\n. 파일 길이는 ~ 100 줄에서 5 천만 줄 이상입니다. 유럽 ​​문자 및 / 또는 악센트가있는 문자가 포함될 수 있습니다.


(코드는 현재 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.thoughtbot.com/swift-sequences 비교 ) :

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

Swift 3 / Xcode 8 베타 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()
        }
    }
}

algal의 답변 코드를 편리한 클래스로 래핑했습니다 (Swift 4, Swift 5)

UPD :이 코드는 플랫폼 독립적입니다 (macOS, iOS, 우분투).

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의 저장소


나는 게임에 늦었지만 여기에 그 목적으로 작성한 소규모 수업이 있습니다. 몇 가지 다른 시도 (subclass 시도) 후에 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)
    }
}

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

이 함수는 파일 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(fileURL.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: "")
}

나는 Martin R의 의견에서 언급 한 메모리 누수를 제거하기 위해 Alex Brown의 답변을 수정하고 Swift 5로 업데이트 하여이 답변을 개발했습니다.


UnsafePointer를 사용하면 좋은 구식 C API가 Swift에서 매우 편안합니다. 다음은 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()

답변을 시도 하거나 Mac OS Stream Programming Guide를 읽으십시오 .

stringWithContentsOfURL그러나 디스크 기반 데이터보다 메모리 기반 (또는 메모리 매핑 된) 데이터로 작업하는 것이 더 빠르기 때문에을 사용하면 성능이 실제로 더 나아질 수 있습니다.

다른 스레드에서 실행하는 방법도 잘 설명되어 있습니다 (예 : 여기) .

최신 정보

한 번에 모든 것을 읽고 싶지 않고 NSStreams를 사용하고 싶지 않다면 아마도 C 레벨 파일 I / O를 사용해야 할 것입니다. 이를 수행하지 않는 이유 여러 가지 가 있습니다. 차단, 문자 인코딩, I / O 오류 처리, 이름 지정 속도 등 몇 가지가 있습니다. 이것이 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)
}

(참고 : macOS Sierra 10.12.3과 함께 Xcode 8.2.1에서 Swift 3.0.1을 사용하고 있습니다.)

내가 여기서 본 모든 답변은 그가 LF 또는 CRLF를 찾을 수 있다는 것을 놓쳤습니다. 모든 것이 잘되면 LF에서 일치하고 반환 된 문자열에서 끝에 추가 CR을 확인할 수 있습니다. 그러나 일반적인 쿼리에는 여러 검색 문자열이 포함됩니다. 즉, 구분 기호는 a 여야합니다 Set<String>. 여기서 집합은 비어 있지도 않고 단일 문자열 대신 빈 문자열도 포함하지 않습니다.

작년에 첫 번째 시도에서 "올바른 일"을하고 일반적인 문자열 집합을 검색하려고했습니다. 너무 힘들 었어요. 완전한 파서와 상태 머신 등이 필요합니다. 나는 그것과 그것이 일부였던 프로젝트를 포기했다.

이제 저는 다시 프로젝트를 진행하고 있으며 같은 도전에 다시 직면합니다. 이제 CR과 LF에서 하드 코드 검색을 할 것입니다. 나는 CR / LF 구문 분석 외부에서 이와 같은 두 개의 반독립 및 반 종속 문자를 검색 할 필요가 없다고 생각합니다.

에서 제공하는 검색 방법을 사용하고 Data있으므로 여기에서 문자열 인코딩 등을 수행하지 않습니다. 원시 바이너리 처리입니다. 여기에 ISO Latin-1 또는 UTF-8과 같은 ASCII 수퍼 세트가 있다고 가정하십시오. 다음 상위 계층에서 문자열 인코딩을 처리 할 수 ​​있으며 보조 코드 포인트가 첨부 된 CR / LF가 여전히 CR 또는 LF로 계산되는지 여부를 확인합니다.

알고리즘 : 현재 바이트 오프셋에서 다음 CR 다음 LF를 계속 검색합니다 .

  • 둘 다 발견되지 않으면 다음 데이터 문자열이 현재 오프셋에서 데이터 끝까지 인 것으로 간주합니다. 종결 자 길이는 0입니다. 이것을 읽기 루프의 끝으로 표시하십시오.
  • LF가 먼저 발견되거나 LF 만 발견되면 다음 데이터 문자열이 현재 오프셋에서 LF까지 인 것으로 간주하십시오. 종결 자 길이는 1입니다. 오프셋을 LF 뒤로 이동합니다.
  • CR 만 발견되면 LF 케이스를 좋아합니다 (다른 바이트 값만 사용).
  • 그렇지 않으면 CR 다음에 LF가 있습니다.
    • 두 개가 인접하면 종결 자 길이가 2 인 것을 제외하고 LF 케이스와 같이 처리합니다.
    • 그 사이에 1 바이트가 있고 그 바이트도 CR이라고 말하면 "Windows 개발자가 텍스트 모드에서 \ r \ n 바이너리를 작성하여 \ r \ r \ n"문제가 발생합니다. 또한 터미네이터 길이가 3이라는 점을 제외하면 LF 케이스처럼 처리하십시오.
    • 그렇지 않으면 CR과 LF가 연결되지 않고 just-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로 끝납니다.)
  • 그리고 현재 오프셋에서 더 이상 종결자가 없지만 데이터 끝이 나중 청크에있을 때 처리해야합니다.

행운을 빕니다!


또는 간단히 다음을 사용할 수 있습니다 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) { ... }

둘 다 비효율적이고 모든 크기의 버퍼 (1 바이트 포함) 및 구분 기호를 허용하므로 버퍼 또는 중복 코드를 지속적으로 수정하지 않는 버전을 원했습니다. 하나의 공개 방법 : readline(). 이 메서드를 호출하면 다음 줄의 String 값을 반환하거나 EOF에서 nil을 반환합니다.

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

참고 URL : https://stackoverflow.com/questions/24581517/read-a-file-url-line-by-line-in-swift

반응형