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