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


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
}

나는 몇 가지 이유로 이것에별로 만족하지 않습니다. 첫째, 몇 킬로바이트에서 수백 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 천만 줄 이상입니다. 유럽 ​​문자 및 / 또는 악센트가있는 문자가 포함될 수 있습니다.


이동하면서 어레이를 디스크에 쓰거나 OS가 메모리로 처리하도록 하시겠습니까? 그것을 실행하는 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-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()
        }
    }
}

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

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

나는 이것을 좋아하지만 여전히 개선 될 수있다. 를 사용하여 포인터를 만드는 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: "")
}

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


2

답변을 시도 하거나 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)
}

제안에 감사하지만 Swift에서 코드를 특별히 찾고 있습니다. 또한 한 번에 모든 줄을 사용하지 않고 한 번에 한 줄씩 작업하고 싶습니다.
Matt

한 줄로 작업 한 다음 릴리스하고 다음 줄을 읽으려고하십니까? 메모리에서 작업하는 것이 더 빠를 것이라고 생각해야합니다. 순서대로 처리해야합니까? 그렇지 않은 경우 열거 형 블록을 사용하여 배열 처리 속도를 크게 높일 수 있습니다.
macshome

한 번에 여러 줄을 가져오고 싶지만 반드시 모든 줄을로드 할 필요는 없습니다. 질서에 관해서는 중요하지는 않지만 도움이 될 것입니다.
Matt

case 0...127비 ASCII 문자로 확장하면 어떻게됩니까 ?
Matt

1
파일에 어떤 문자 인코딩이 있는지에 따라 다릅니다. 유니 코드 형식이 많은 유니 코드 형식 중 하나 인 경우이를 위해 코드를 작성해야합니다. 유니 코드 이전 PC "코드 페이지"시스템 중 하나 인 경우이를 디코딩해야합니다. 재단 도서관은 여러분을 위해이 모든 것을 수행합니다. 그것은 여러분 스스로 많은 작업을합니다.
Grimxn

2

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

1
"라인 단위"를 전혀 처리하지 못합니다. 출력 할 입력 데이터를 블리츠하고 일반 문자와 줄 끝 문자의 차이를 인식하지 못합니다. 분명히 출력은 입력과 동일한 줄로 구성되지만 개행 문자도 표시되기 때문입니다.
Alex Brown

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 코드를 준 지금 떠난 답변에 감사드립니다!
Alex Brown

1
분명히 나는 ​​인코딩을 완전히 무시하고 있습니다. 독자를위한 연습 문제로 남았습니다.
Alex Brown

getline()데이터에 대한 버퍼를 할당하므로 코드에서 메모리 누수가 발생 합니다.
Martin R

1

(참고 : 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로 끝납니다.)
  • 그리고 현재 오프셋에서 더 이상 종결자가 없지만 데이터 끝이 나중 청크에있을 때 처리해야합니다.

행운을 빕니다!


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(). 이 메서드를 호출하면 다음 줄의 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)
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.