Я пытаюсь прочитать файл, указанный в, 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
}
Я не очень доволен этим по нескольким причинам. Во-первых, я работаю с файлами размером от нескольких килобайт до сотен МБ. Как вы понимаете, работа со строками такого размера выполняется медленно и громоздко. Во-вторых, это приводит к зависанию пользовательского интерфейса при его выполнении - опять же, не очень хорошо.
Я изучал возможность запуска этого кода в отдельном потоке, но у меня возникли проблемы с этим, и, кроме того, он все еще не решает проблему работы с огромными строками.
Я бы хотел сделать что-то вроде следующего псевдокода:
var aStreamReader = new StreamReader(from_file_or_url)
while aStreamReader.hasNextLine == true {
currentline = aStreamReader.nextLine()
list.addItem(currentline)
}
Как мне это сделать в Swift?
Несколько примечаний о файлах, из которых я читаю: Все файлы состоят из коротких (<255 символов) строк, разделенных символом \n
или \r\n
. Длина файлов составляет от ~ 100 строк до более 50 миллионов строк. Они могут содержать европейские символы и / или символы с диакритическими знаками.
Ответы:
(Сейчас код предназначен для 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.oughttbot.com/swift-sequences ):extension StreamReader : SequenceType { func generate() -> AnyGenerator<String> { return AnyGenerator { return self.nextLine() } } }
Обновление для Swift 3 / Xcode 8 beta 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() } } }
источник
generate()
функцию в класс StreamReader и объявить ее какclass StreamReader : Sequence { ... }
. Но кажется хорошим стилем Swift использовать расширения для отдельных частей функциональности.while !aStreamReader.atEof { try autoreleasepool { guard let line = aStreamReader.nextLine() else { return } ...code... } }
Эффективный и удобный класс для построчного чтения текстовых файлов (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)) }
источник
Я опаздываю в игру, но вот небольшой класс, который я написал для этой цели. После нескольких разных попыток (попробуйте
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>
Бросок не нужен. Наконец, ваш пример использования не компилируется.Эта функция принимает 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: "") }
Я разработал этот ответ, изменив ответ Алекса Брауна, чтобы удалить утечку памяти, упомянутую в комментарии Мартина Р., и обновив его до Swift 5.
источник
Попробуйте этот ответ или прочитайте Руководство по программированию Mac OS Stream. .
Вы можете обнаружить, что производительность на самом деле будет лучше, если
stringWithContentsOfURL
, так как с данными на основе памяти (или отображенными в память) будет быстрее работать, чем с данными на диске.Выполнение его в другом потоке также хорошо документировано, например, здесь .
Обновить
Если вы не хотите читать все сразу и не хотите использовать NSStreams, вам, вероятно, придется использовать файловый ввод-вывод C-уровня. Есть много причин не делать этого - блокировка, кодировка символов, обработка ошибок ввода-вывода, скорость называния, но некоторые из них - это то, для чего нужны библиотеки 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) }
источник
case 0...127
символы на символы, отличные от ASCII?Оказывается, старый добрый C API довольно удобен в Swift, если вы попробуете UnsafePointer. Вот простой кот, который читает из стандартного ввода и выводит его построчно. Вам даже не нужен Foundation. Дарвина достаточно:
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()
источник
fgets()
читает символы до (включительно) символа новой строки (или EOF). Или я неправильно понимаю ваш комментарий?Или вы можете просто использовать
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()
приводит к утечке памяти, поскольку выделяет буфер для данных.(Примечание: я использую Swift 3.0.1 в Xcode 8.2.1 с macOS Sierra 10.12.3)
Все ответы, которые я здесь видел, упускали из виду, что он мог искать LF или CRLF. Если все пойдет хорошо, он / она может просто сопоставить LF и проверить возвращенную строку на наличие лишнего CR в конце. Но общий запрос включает несколько строк поиска. Другими словами, разделитель должен быть a
Set<String>
, где набор не пуст и не содержит пустую строку, а не одну строку.С моей первой попытки в прошлом году я попытался поступить «правильно» и найти общий набор строк. Это было слишком сложно; вам нужен полноценный парсер, конечные автоматы и все такое. Я отказался от этого и от проекта, частью которого он был.
Теперь я снова занимаюсь этим проектом и снова сталкиваюсь с той же проблемой. Теперь я собираюсь выполнить жесткий поиск по CR и LF. Я не думаю, что кому-то понадобится искать два полунезависимых и полузависимых символа, подобных этому, вне анализа CR / LF.
Я использую методы поиска, предоставленные
Data
, поэтому я не использую здесь строковые кодировки и прочее. Просто необработанная двоичная обработка. Предположим, у меня есть надмножество ASCII, например ISO Latin-1 или UTF-8. Вы можете обрабатывать строковое кодирование на следующем, более высоком уровне, и вы можете понять, считается ли CR / LF с присоединенными вторичными кодовыми точками как CR или LF.Алгоритм: просто продолжайте поиск следующего CR и следующего LF от вашего текущего байтового смещения.
Вот код для этого:
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))) } }
Вы должны сами смешать эти идеи, так как я еще этого не сделал. Рассматривать:
Удачи!
источник
После ответа @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()
Это сработало для меня.
благодаря
источник
Мне нужна была версия, которая не изменяла бы буфер или дублированный код постоянно, поскольку оба они неэффективны и позволяли бы использовать буфер любого размера (включая 1 байт) и любой разделитель. Он имеет один публичный метод:
readline()
. Вызов этого метода вернет строковое значение следующей строки или ноль в 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) } }
Он называется следующим образом:
guard let myStream = LineStream(path: "/path/to/file.txt") else { exit(EXIT_FAILURE) } while let s = myStream.readLine() { print(s) }
источник