Obiettivo-C: lettura di un file riga per riga
-
20-08-2019 - |
Domanda
Qual è il modo appropriato di trattare file di testo di grandi dimensioni in Objective-C? Diciamo che ho bisogno di leggere ogni riga separatamente e voglio trattare ogni riga come una NSString. Qual è il modo più efficace per farlo?
Una soluzione sta usando il metodo NSString:
+ (id)stringWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(NSError **)error
e quindi dividere le linee con un separatore di nuova riga, quindi scorrere gli elementi nell'array. Tuttavia, questo sembra abbastanza inefficiente. Non esiste un modo semplice per trattare il file come un flusso, enumerandolo su ogni riga, invece di leggerlo tutto in una volta? Un po 'come Java java.io.BufferedReader.
Soluzione
Questa è un'ottima domanda. Penso che @Diederik abbia una buona risposta, anche se è un peccato che Cocoa non abbia un meccanismo esattamente per quello che vuoi fare.
NSInputStream
ti permette di leggere blocchi di N byte (molto simili a java.io.BufferedReader
), ma devi convertirlo in un NSString
da solo, quindi cercare nuovi caratteri (o qualsiasi altro delimitatore) e salvare eventuali caratteri rimanenti per la lettura successiva o leggere più caratteri se una nuova riga non è stata ancora letta. ( NSFileHandle
ti permette di leggere un NSData
che puoi quindi convertire in uint8_t*
, ma essenzialmente è lo stesso processo.)
Apple ha una Guida alla programmazione di stream che può aiutarti a riempire nei dettagli e questa domanda SO può essere d'aiuto se hai a che fare con <=> buffer.
Se stai leggendo stringhe come questa frequentemente (specialmente in diverse parti del tuo programma), sarebbe una buona idea incapsulare questo comportamento in una classe che può gestire i dettagli per te, o anche sottoclassare <= > (è progettato per essere suddiviso in sottoclassi ) e con l'aggiunta di metodi che ti consentono di leggere esattamente ciò che desideri.
Per la cronaca, penso che questa sarebbe una bella caratteristica da aggiungere, e presenterò una richiesta di miglioramento per qualcosa che lo rende possibile. : -)
Modifica: Risulta che questa richiesta esiste già. C'è un radar risalente al 2006 per questo (rdar: // 4742914 per le persone interne ad Apple).
Altri suggerimenti
Questo funzionerà per la lettura generale di String
da Text
.
Se desideri leggere un testo più lungo (grande dimensione del testo) , usa il metodo che è stato menzionato qui altre persone come buffer (riserva la dimensione del testo nello spazio di memoria) .
Supponi di leggere un file di testo.
NSString* filePath = @""//file path...
NSString* fileRoot = [[NSBundle mainBundle]
pathForResource:filePath ofType:@"txt"];
Vuoi sbarazzarti della nuova linea.
// read everything from text
NSString* fileContents =
[NSString stringWithContentsOfFile:fileRoot
encoding:NSUTF8StringEncoding error:nil];
// first, separate by new line
NSArray* allLinedStrings =
[fileContents componentsSeparatedByCharactersInSet:
[NSCharacterSet newlineCharacterSet]];
// then break down even further
NSString* strsInOneLine =
[allLinedStrings objectAtIndex:0];
// choose whatever input identity you have decided. in this case ;
NSArray* singleStrs =
[currentPointString componentsSeparatedByCharactersInSet:
[NSCharacterSet characterSetWithCharactersInString:@";"]];
Eccolo qui.
Questo dovrebbe fare il trucco:
#include <stdio.h>
NSString *readLineAsNSString(FILE *file)
{
char buffer[4096];
// tune this capacity to your liking -- larger buffer sizes will be faster, but
// use more memory
NSMutableString *result = [NSMutableString stringWithCapacity:256];
// Read up to 4095 non-newline characters, then read and discard the newline
int charsRead;
do
{
if(fscanf(file, "%4095[^\n]%n%*c", buffer, &charsRead) == 1)
[result appendFormat:@"%s", buffer];
else
break;
} while(charsRead == 4095);
return result;
}
Utilizzare come segue:
FILE *file = fopen("myfile", "r");
// check for NULL
while(!feof(file))
{
NSString *line = readLineAsNSString(file);
// do stuff with line; line is autoreleased, so you should NOT release it (unless you also retain it beforehand)
}
fclose(file);
Questo codice legge i caratteri non newline dal file, fino a 4095 alla volta. Se hai una linea più lunga di 4095 caratteri, continua a leggere fino a quando non raggiunge una nuova riga o fine del file.
Nota : non ho testato questo codice. Provalo prima di usarlo.
Mac OS X è Unix, Objective-C è superset C, quindi puoi semplicemente usare la vecchia scuola fopen
e fgets
da <stdio.h>
. È garantito per funzionare.
[NSString stringWithUTF8String:buf]
convertirà la stringa C in NSString
. Esistono anche metodi per creare stringhe in altre codifiche e creare senza copiare.
Puoi usare NSInputStream
che ha un'implementazione di base per i flussi di file. È possibile leggere i byte in un buffer (metodo read:maxLength:
). Devi scansionare tu stesso il buffer per le nuove righe.
Il modo appropriato di leggere i file di testo in Cocoa / Objective-C è documentato nella guida alla programmazione delle stringhe di Apple. La sezione per leggere e scrivere file dovrebbe essere proprio ciò che stai cercando. PS: Che cos'è un & Quot; linea & Quot ;? Due sezioni di una stringa separate da & Quot; \ n & Quot ;? Oppure & Quot; \ r & Quot ;? Oppure & Quot; \ r \ n & Quot ;? O forse stai effettivamente seguendo i paragrafi? La guida precedentemente menzionata include anche una sezione sulla divisione di una stringa in righe o paragrafi. (Questa sezione è chiamata & Quot; Paragrafi e interruzioni di riga & Quot ;, ed è collegata nel menu di sinistra della pagina che ho indicato sopra. Purtroppo questo sito non mi consente di pubblicare più di un URL in quanto non sono ancora un utente affidabile.)
Per parafrasare Knuth: l'ottimizzazione prematura è la radice di tutti i mali. Non dare per scontato che & Quot; leggere l'intero file in memoria & Quot; è lento. L'hai confrontato? Sai che in realtà legge l'intero file in memoria? Forse restituisce semplicemente un oggetto proxy e continua a leggere dietro le quinte mentre consumi la stringa? ( Dichiarazione di non responsabilità: non ho idea se NSString lo faccia davvero. Probabilmente potrebbe. ) Il punto è: prima di tutto segui il modo documentato di fare le cose. Quindi, se i benchmark mostrano che questo non ha le prestazioni che desideri, ottimizza.
Molte di queste risposte sono lunghe porzioni di codice o vengono lette nell'intero file. Mi piace usare i metodi c proprio per questo compito.
FILE* file = fopen("path to my file", "r");
size_t length;
char *cLine = fgetln(file,&length);
while (length>0) {
char str[length+1];
strncpy(str, cLine, length);
str[length] = '\0';
NSString *line = [NSString stringWithFormat:@"%s",str];
% Do what you want here.
cLine = fgetln(file,&length);
}
Nota che fgetln non manterrà il tuo personaggio newline. Inoltre, facciamo +1 sulla lunghezza della str perché vogliamo fare spazio per la terminazione NULL.
Per leggere un file riga per riga (anche per file di dimensioni estreme) è possibile eseguire le seguenti funzioni:
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
NSString * line = nil;
while ((line = [reader readLine])) {
NSLog(@"read line: %@", line);
}
[reader release];
o
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
[reader enumerateLinesUsingBlock:^(NSString * line, BOOL * stop) {
NSLog(@"read line: %@", line);
}];
[reader release];
La classe DDFileReader che abilita ciò è la seguente:
File di interfaccia (.h):
@interface DDFileReader : NSObject {
NSString * filePath;
NSFileHandle * fileHandle;
unsigned long long currentOffset;
unsigned long long totalFileLength;
NSString * lineDelimiter;
NSUInteger chunkSize;
}
@property (nonatomic, copy) NSString * lineDelimiter;
@property (nonatomic) NSUInteger chunkSize;
- (id) initWithFilePath:(NSString *)aPath;
- (NSString *) readLine;
- (NSString *) readTrimmedLine;
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL *))block;
#endif
@end
Implementazione (.m)
#import "DDFileReader.h"
@interface NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind;
@end
@implementation NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind {
const void * bytes = [self bytes];
NSUInteger length = [self length];
const void * searchBytes = [dataToFind bytes];
NSUInteger searchLength = [dataToFind length];
NSUInteger searchIndex = 0;
NSRange foundRange = {NSNotFound, searchLength};
for (NSUInteger index = 0; index < length; index++) {
if (((char *)bytes)[index] == ((char *)searchBytes)[searchIndex]) {
//the current character matches
if (foundRange.location == NSNotFound) {
foundRange.location = index;
}
searchIndex++;
if (searchIndex >= searchLength) { return foundRange; }
} else {
searchIndex = 0;
foundRange.location = NSNotFound;
}
}
return foundRange;
}
@end
@implementation DDFileReader
@synthesize lineDelimiter, chunkSize;
- (id) initWithFilePath:(NSString *)aPath {
if (self = [super init]) {
fileHandle = [NSFileHandle fileHandleForReadingAtPath:aPath];
if (fileHandle == nil) {
[self release]; return nil;
}
lineDelimiter = [[NSString alloc] initWithString:@"\n"];
[fileHandle retain];
filePath = [aPath retain];
currentOffset = 0ULL;
chunkSize = 10;
[fileHandle seekToEndOfFile];
totalFileLength = [fileHandle offsetInFile];
//we don't need to seek back, since readLine will do that.
}
return self;
}
- (void) dealloc {
[fileHandle closeFile];
[fileHandle release], fileHandle = nil;
[filePath release], filePath = nil;
[lineDelimiter release], lineDelimiter = nil;
currentOffset = 0ULL;
[super dealloc];
}
- (NSString *) readLine {
if (currentOffset >= totalFileLength) { return nil; }
NSData * newLineData = [lineDelimiter dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle seekToFileOffset:currentOffset];
NSMutableData * currentData = [[NSMutableData alloc] init];
BOOL shouldReadMore = YES;
NSAutoreleasePool * readPool = [[NSAutoreleasePool alloc] init];
while (shouldReadMore) {
if (currentOffset >= totalFileLength) { break; }
NSData * chunk = [fileHandle readDataOfLength:chunkSize];
NSRange newLineRange = [chunk rangeOfData_dd:newLineData];
if (newLineRange.location != NSNotFound) {
//include the length so we can include the delimiter in the string
chunk = [chunk subdataWithRange:NSMakeRange(0, newLineRange.location+[newLineData length])];
shouldReadMore = NO;
}
[currentData appendData:chunk];
currentOffset += [chunk length];
}
[readPool release];
NSString * line = [[NSString alloc] initWithData:currentData encoding:NSUTF8StringEncoding];
[currentData release];
return [line autorelease];
}
- (NSString *) readTrimmedLine {
return [[self readLine] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL*))block {
NSString * line = nil;
BOOL stop = NO;
while (stop == NO && (line = [self readLine])) {
block(line, &stop);
}
}
#endif
@end
La lezione è stata tenuta da Dave DeLong
Proprio come ha detto @porneL, l'API C è molto utile.
NSString* fileRoot = [[NSBundle mainBundle] pathForResource:@"record" ofType:@"txt"];
FILE *file = fopen([fileRoot UTF8String], "r");
char buffer[256];
while (fgets(buffer, 256, file) != NULL){
NSString* result = [NSString stringWithUTF8String:buffer];
NSLog(@"%@",result);
}
Come altri hanno risposto, sia NSInputStream che NSFileHandle sono ottime opzioni, ma può anche essere fatto in modo abbastanza compatto con NSData e mappatura della memoria:
BRLineReader.h
#import <Foundation/Foundation.h>
@interface BRLineReader : NSObject
@property (readonly, nonatomic) NSData *data;
@property (readonly, nonatomic) NSUInteger linesRead;
@property (strong, nonatomic) NSCharacterSet *lineTrimCharacters;
@property (readonly, nonatomic) NSStringEncoding stringEncoding;
- (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding;
- (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding;
- (NSString *)readLine;
- (NSString *)readTrimmedLine;
- (void)setLineSearchPosition:(NSUInteger)position;
@end
BRLineReader.m
#import "BRLineReader.h"
static unsigned char const BRLineReaderDelimiter = '\n';
@implementation BRLineReader
{
NSRange _lastRange;
}
- (instancetype)initWithFile:(NSString *)filePath encoding:(NSStringEncoding)encoding
{
self = [super init];
if (self) {
NSError *error = nil;
_data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedAlways error:&error];
if (!_data) {
NSLog(@"%@", [error localizedDescription]);
}
_stringEncoding = encoding;
_lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
}
return self;
}
- (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding
{
self = [super init];
if (self) {
_data = data;
_stringEncoding = encoding;
_lineTrimCharacters = [NSCharacterSet whitespaceAndNewlineCharacterSet];
}
return self;
}
- (NSString *)readLine
{
NSUInteger dataLength = [_data length];
NSUInteger beginPos = _lastRange.location + _lastRange.length;
NSUInteger endPos = 0;
if (beginPos == dataLength) {
// End of file
return nil;
}
unsigned char *buffer = (unsigned char *)[_data bytes];
for (NSUInteger i = beginPos; i < dataLength; i++) {
endPos = i;
if (buffer[i] == BRLineReaderDelimiter) break;
}
// End of line found
_lastRange = NSMakeRange(beginPos, endPos - beginPos + 1);
NSData *lineData = [_data subdataWithRange:_lastRange];
NSString *line = [[NSString alloc] initWithData:lineData encoding:_stringEncoding];
_linesRead++;
return line;
}
- (NSString *)readTrimmedLine
{
return [[self readLine] stringByTrimmingCharactersInSet:_lineTrimCharacters];
}
- (void)setLineSearchPosition:(NSUInteger)position
{
_lastRange = NSMakeRange(position, 0);
_linesRead = 0;
}
@end
Questa risposta NON è ObjC ma C.
Poiché ObjC è basato su "C", perché non utilizzare i budget?
E sì, sono sicuro che ObjC ha il suo metodo - non sono ancora abbastanza esperto per sapere di cosa si tratta :)
dalla risposta di @Adam Rosenfield, la stringa di formattazione di fscanf
verrebbe modificata come di seguito:
"%4095[^\r\n]%n%*[\n\r]"
funzionerà in osx, linux, terminazioni di linea di Windows.
Uso della categoria o dell'estensione per semplificarci un po 'la vita.
extension String {
func lines() -> [String] {
var lines = [String]()
self.enumerateLines { (line, stop) -> () in
lines.append(line)
}
return lines
}
}
// then
for line in string.lines() {
// do the right thing
}
Ho trovato molto utile la risposta di @lukaswelte e il codice di Dave DeLong . Stavo cercando una soluzione a questo problema, ma avevo bisogno di analizzare file di grandi dimensioni \r\n
non solo \n
.
Il codice come scritto contiene un bug se viene analizzato da più di un carattere. Ho modificato il codice come di seguito.
.h file:
#import <Foundation/Foundation.h>
@interface FileChunkReader : NSObject {
NSString * filePath;
NSFileHandle * fileHandle;
unsigned long long currentOffset;
unsigned long long totalFileLength;
NSString * lineDelimiter;
NSUInteger chunkSize;
}
@property (nonatomic, copy) NSString * lineDelimiter;
@property (nonatomic) NSUInteger chunkSize;
- (id) initWithFilePath:(NSString *)aPath;
- (NSString *) readLine;
- (NSString *) readTrimmedLine;
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL *))block;
#endif
@end
.m file:
#import "FileChunkReader.h"
@interface NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind;
@end
@implementation NSData (DDAdditions)
- (NSRange) rangeOfData_dd:(NSData *)dataToFind {
const void * bytes = [self bytes];
NSUInteger length = [self length];
const void * searchBytes = [dataToFind bytes];
NSUInteger searchLength = [dataToFind length];
NSUInteger searchIndex = 0;
NSRange foundRange = {NSNotFound, searchLength};
for (NSUInteger index = 0; index < length; index++) {
if (((char *)bytes)[index] == ((char *)searchBytes)[searchIndex]) {
//the current character matches
if (foundRange.location == NSNotFound) {
foundRange.location = index;
}
searchIndex++;
if (searchIndex >= searchLength)
{
return foundRange;
}
} else {
searchIndex = 0;
foundRange.location = NSNotFound;
}
}
if (foundRange.location != NSNotFound
&& length < foundRange.location + foundRange.length )
{
// if the dataToFind is partially found at the end of [self bytes],
// then the loop above would end, and indicate the dataToFind is found
// when it only partially was.
foundRange.location = NSNotFound;
}
return foundRange;
}
@end
@implementation FileChunkReader
@synthesize lineDelimiter, chunkSize;
- (id) initWithFilePath:(NSString *)aPath {
if (self = [super init]) {
fileHandle = [NSFileHandle fileHandleForReadingAtPath:aPath];
if (fileHandle == nil) {
return nil;
}
lineDelimiter = @"\n";
currentOffset = 0ULL; // ???
chunkSize = 128;
[fileHandle seekToEndOfFile];
totalFileLength = [fileHandle offsetInFile];
//we don't need to seek back, since readLine will do that.
}
return self;
}
- (void) dealloc {
[fileHandle closeFile];
currentOffset = 0ULL;
}
- (NSString *) readLine {
if (currentOffset >= totalFileLength)
{
return nil;
}
@autoreleasepool {
NSData * newLineData = [lineDelimiter dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle seekToFileOffset:currentOffset];
unsigned long long originalOffset = currentOffset;
NSMutableData *currentData = [[NSMutableData alloc] init];
NSData *currentLine = [[NSData alloc] init];
BOOL shouldReadMore = YES;
while (shouldReadMore) {
if (currentOffset >= totalFileLength)
{
break;
}
NSData * chunk = [fileHandle readDataOfLength:chunkSize];
[currentData appendData:chunk];
NSRange newLineRange = [currentData rangeOfData_dd:newLineData];
if (newLineRange.location != NSNotFound) {
currentOffset = originalOffset + newLineRange.location + newLineData.length;
currentLine = [currentData subdataWithRange:NSMakeRange(0, newLineRange.location)];
shouldReadMore = NO;
}else{
currentOffset += [chunk length];
}
}
if (currentLine.length == 0 && currentData.length > 0)
{
currentLine = currentData;
}
return [[NSString alloc] initWithData:currentLine encoding:NSUTF8StringEncoding];
}
}
- (NSString *) readTrimmedLine {
return [[self readLine] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
#if NS_BLOCKS_AVAILABLE
- (void) enumerateLinesUsingBlock:(void(^)(NSString*, BOOL*))block {
NSString * line = nil;
BOOL stop = NO;
while (stop == NO && (line = [self readLine])) {
block(line, &stop);
}
}
#endif
@end
Lo sto aggiungendo perché tutte le altre risposte che ho provato sono state insufficienti in un modo o nell'altro. Il seguente metodo può gestire file di grandi dimensioni, righe lunghe arbitrarie e righe vuote. È stato testato con contenuti reali e eliminerà il carattere di nuova riga dall'output.
- (NSString*)readLineFromFile:(FILE *)file
{
char buffer[4096];
NSMutableString *result = [NSMutableString stringWithCapacity:1000];
int charsRead;
do {
if(fscanf(file, "%4095[^\r\n]%n%*[\n\r]", buffer, &charsRead) == 1) {
[result appendFormat:@"%s", buffer];
}
else {
break;
}
} while(charsRead == 4095);
return result.length ? result : nil;
}
Il merito va a @Adam Rosenfield e @sooop
Ecco una bella soluzione semplice che uso per file più piccoli:
NSString *path = [[NSBundle mainBundle] pathForResource:@"Terrain1" ofType:@"txt"];
NSString *contents = [NSString stringWithContentsOfFile:path encoding:NSASCIIStringEncoding error:nil];
NSArray *lines = [contents componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\r\n"]];
for (NSString* line in lines) {
if (line.length) {
NSLog(@"line: %@", line);
}
}
Usa questo script, funziona benissimo:
NSString *path = @"/Users/xxx/Desktop/names.txt";
NSError *error;
NSString *stringFromFileAtPath = [NSString stringWithContentsOfFile: path
encoding: NSUTF8StringEncoding
error: &error];
if (stringFromFileAtPath == nil) {
NSLog(@"Error reading file at %@\n%@", path, [error localizedFailureReason]);
}
NSLog(@"Contents:%@", stringFromFileAtPath);