Quelle est la manière appropriée de traiter les gros fichiers texte dans Objective-C? Disons que je dois lire chaque ligne séparément et que je veux traiter chaque ligne comme une chaîne NSString. Quel est le moyen le plus efficace de le faire?
Une solution utilise la méthode NSString:
+ (id)stringWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(NSError **)error
puis divisez les lignes avec un séparateur de nouvelle ligne, puis parcourez les éléments du tableau. Cependant, cela semble assez inefficace. N'y a-t-il pas un moyen simple de traiter le fichier en tant que flux, en énumérant chaque ligne au lieu de tout lire en une fois? Un peu comme Java.io.BufferedReader de Java.
C'est une excellente question. Je pense que @Diederik a une bonne réponse, même s’il est regrettable que Cocoa n’ait pas de mécanisme pour ce que vous voulez faire.
NSInputStream
vous permet de lire des fragments de N octets (très similaires à Java.io.BufferedReader
), mais vous devez le convertir vous-même en un NSString
, puis rechercher des nouvelles lignes (ou tout autre délimiteur) et enregistrer tous les caractères restants pour les prochaine lecture, ou lit plus de caractères si une nouvelle ligne n'a pas encore été lue. ( NSFileHandle
vous permet de lire une NSData
que vous pourrez ensuite convertir en une NSString
, mais il s’agit essentiellement du même processus.)
Apple a un Guide de programmation de flux qui peut aider à compléter les détails, et cette question SO / peut également vous aider si vous envisagez de manipuler des tampons uint8_t*
.
Si vous lisez fréquemment des chaînes comme celle-ci (en particulier dans différentes parties de votre programme), il serait judicieux d'encapsuler ce problème dans une classe pouvant gérer les détails pour vous, ou même de sous-classer NSInputStream
(c'est conçu pour être sous-classé ) et en ajoutant des méthodes qui vous permettent de lire exactement ce que vous voulez.
Pour mémoire, je pense que ce serait une fonctionnalité intéressante à ajouter, et je déposerai une demande d'amélioration pour quelque chose qui rend cela possible. :-)
Edit: Il s'avère que cette requête existe déjà. Il existe un radar datant de 2006 pour cela (rdar: // 4742914 pour les personnes internes à Apple).
Cela fonctionnera pour la lecture générale de String
de Text
. Si vous souhaitez lire un texte plus long (taille importante du texte) , utilisez la méthode que d’autres personnes ont été mentionnées, telle que tamponnée (réserve). la taille du texte dans la mémoire) .
NSString* filePath = @""//file path...
NSString* fileRoot = [[NSBundle mainBundle]
pathForResource:filePath ofType:@"txt"];
// 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:@";"]];
Voilà.
Cela devrait faire l'affaire:
#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;
}
Utilisez comme suit:
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);
Ce code lit les caractères hors ligne du fichier, jusqu'à 4095 à la fois. Si vous avez une ligne de plus de 4095 caractères, la lecture se poursuit jusqu'à atteindre une nouvelle ligne ou une fin de fichier.
Note : Je n'ai pas testé ce code. S'il vous plaît, testez-le avant de l'utiliser.
Mac OS X est sous Unix, Objective-C est un sur-ensemble C, vous pouvez donc utiliser les anciennes variables fopen
et fgets
de <stdio.h>
. C'est garanti pour fonctionner.
[NSString stringWithUTF8String:buf]
convertira la chaîne C en NSString
. Il existe également des méthodes pour créer des chaînes dans d'autres encodages et créer sans copier.
Vous pouvez utiliser NSInputStream
qui a une implémentation de base pour les flux de fichiers. Vous pouvez lire des octets dans un tampon (méthode read:maxLength:
). Vous devez analyser le tampon pour les nouvelles lignes vous-même.
La manière appropriée de lire les fichiers texte dans Cocoa/Objective-C est documentée dans le guide de programmation Apple String. La section pour lire et écrire des fichiers devrait être exactement ce que vous cherchez. PS: Qu'est-ce qu'une "ligne"? Deux sections d'une chaîne séparées par "\ n"? Ou "\ r"? Ou "\ r\n"? Ou peut-être que vous êtes après les paragraphes? Le guide mentionné précédemment comprend également une section sur la division d'une chaîne en lignes ou en paragraphes. (Cette section, intitulée "Paragraphes and Line Breaks", est liée au menu de gauche de la page que j'ai indiquée ci-dessus. Malheureusement, ce site ne me permet pas de publier plus d'une URL, pas encore un utilisateur de confiance.)
Pour paraphraser Knuth: l’optimisation prématurée est la racine de tous les maux. Ne supposez pas simplement que "la lecture du fichier entier en mémoire" est lente. L'avez-vous évalué? Savez-vous qu'il effectivement lit l'intégralité du fichier en mémoire? Peut-être qu'il renvoie simplement un objet proxy et continue à lire dans les coulisses lorsque vous utilisez la chaîne? (Disclaimer: Je ne sais pas du tout si NSString le fait réellement. Il pourrait le faire.) Le problème est le suivant: tout d'abord, utilisez la méthode documentée. Ensuite, si les repères montrent que cela n’a pas les performances souhaitées, optimisez-les.
Beaucoup de ces réponses sont de longs morceaux de code ou sont lues dans le fichier entier. J'aime utiliser les méthodes c pour cette tâche même.
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);
}
Notez que fgetln ne conservera pas votre caractère de nouvelle ligne. De plus, nous +1 la longueur de la chaîne car nous voulons faire de la place pour la terminaison NULL.
Comme d’autres ont répondu, NSInputStream et NSFileHandle sont de bonnes options, mais cela peut également être fait de manière assez compacte avec NSData et le mappage de la mémoire:
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
Comme @porneL, l’API C est très pratique.
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);
}
Pour lire un fichier ligne par ligne (même dans le cas de fichiers extrêmement volumineux), vous pouvez utiliser les fonctions suivantes:
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
NSString * line = nil;
while ((line = [reader readLine])) {
NSLog(@"read line: %@", line);
}
[reader release];
Ou:
DDFileReader * reader = [[DDFileReader alloc] initWithFilePath:pathToMyFile];
[reader enumerateLinesUsingBlock:^(NSString * line, BOOL * stop) {
NSLog(@"read line: %@", line);
}];
[reader release];
La classe DDFileReader qui permet cela est la suivante:
Fichier d'interface (.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
Mise en oeuvre (.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
Le cours a été fait par Dave DeLong
Cette réponse n'est PAS objC mais C.
Puisque ObjC est basé sur 'C', pourquoi ne pas utiliser fgets?
Et oui, je suis sûr que ObjC a sa propre méthode - je ne suis pas encore assez compétent pour savoir ce que c'est :)
J'ajoute ceci parce que toutes les autres réponses que j'ai essayées ont échoué d'une manière ou d'une autre. La méthode suivante peut gérer des fichiers volumineux, de longues lignes arbitraires ainsi que des lignes vides. Il a été testé avec le contenu réel et supprimera le caractère de nouvelle ligne de la sortie.
- (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;
}
Le mérite revient à @Adam Rosenfield et à @ sooop
J'ai trouvé la réponse de @lukaswelte et le code de Dave DeLong très utile. Je cherchais une solution à ce problème, mais j’avais besoin d’analyser les gros fichiers par \r\n
et pas seulement \n
.
Le code tel qu’écrit contient un bogue si l’analyse est effectuée par plus d’un caractère. J'ai changé le code comme ci-dessous.
fichier .h:
#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
fichier .m:
#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
de @Adam Rosenfield, la chaîne de formatage de fscanf
serait modifiée comme ci-dessous:
"%4095[^\r\n]%n%*[\n\r]"
cela fonctionnera sous osx, linux et les fins de ligne Windows.
Utiliser la catégorie ou l'extension pour rendre notre vie un peu plus facile.
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
}