[macos] 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()
        }
    }
}


답변

텍스트 파일을 한 줄씩 읽을 수있는 효율적이고 편리한 수업 (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의 저장소


답변

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


답변

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


답변

이 함수는 파일 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로 업데이트 하여이 답변을 개발했습니다.


답변

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


답변

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