Objective-C: Lire un fichier ligne par ligne

140

Quelle est la manière appropriée de traiter les fichiers texte volumineux dans Objective-C? Disons que j'ai besoin de lire chaque ligne séparément et que je veux traiter chaque ligne comme une NSString. Quelle est la manière la plus efficace de procéder?

Une solution consiste à utiliser 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'existe-t-il pas un moyen facile de traiter le fichier comme un flux, en énumérant chaque ligne, au lieu de simplement tout lire en même temps? Un peu comme java.io.BufferedReader de Java.

Quinn Taylor
la source
1
Un peu tard, mais vérifiez [NSScanner scanUpToString: @ "\ n" intoString: & read], en supposant que vous vouliez lire chaque ligne dans la chaîne 'read'.
hauntsaninja
Veuillez jeter un œil à cette question similaire . J'ai mis en place un projet qui traite de la lecture des fichiers ligne par ligne .
JJD

Réponses:

63

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 exactement ce que vous voulez faire.

NSInputStreamvous permet de lire des morceaux de N octets (très similaire à java.io.BufferedReader), mais vous devez le convertir en un NSStringseul, puis rechercher les nouvelles lignes (ou tout autre délimiteur) et enregistrer les caractères restants pour la prochaine lecture, ou lire plus de caractères si une nouvelle ligne n'a pas encore été lue. ( NSFileHandlevous permet de lire un NSDataque vous pouvez ensuite convertir en un NSString, mais c'est essentiellement le même processus.)

Apple a un guide de programmation de flux qui peut aider à remplir les détails, et cette question SO peut également vous aider si vous avez affaire à des uint8_t*tampons.

Si vous lisez fréquemment des chaînes comme celle-ci (en particulier dans différentes parties de votre programme), ce serait une bonne idée d'encapsuler ce comportement dans une classe qui peut gérer les détails pour vous, ou même de sous-classer NSInputStream(il est conçu pour être sous-classées ) 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 vais déposer une demande d'amélioration pour quelque chose qui rend cela possible. :-)


Modifier: il s'avère que cette demande existe déjà. Il existe un radar datant de 2006 pour cela (rdar: // 4742914 pour les personnes internes à Apple).

Quinn Taylor
la source
10
Voir l'approche complète de Dave DeLong à ce problème ici: stackoverflow.com/questions/3707427#3711079
Quinn Taylor le
Il est également possible d'utiliser des NSData simples et un mappage de mémoire. J'ai créé une réponse avec un exemple de code qui a la même API que l'implémentation NSFileHandle de Dave DeLong: stackoverflow.com/a/21267461/267043
Bjørn Olav Ruud
95

Cela fonctionnera pour la lecture générale Stringde Text. Si vous souhaitez lire du texte plus long ( texte de grande taille) , utilisez la méthode mentionnée par d'autres personnes ici, par exemple en mémoire tampon (réservez la taille du texte dans l'espace mémoire) .

Disons que vous lisez un fichier texte.

NSString* filePath = @""//file path...
NSString* fileRoot = [[NSBundle mainBundle] 
               pathForResource:filePath ofType:@"txt"];

Vous voulez vous débarrasser de la nouvelle ligne.

// 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à.

Yoon Lee
la source
17
J'ai un fichier de 70 Mo, l'utilisation de ce code pour lire le fichier ne me permet pas d'augmenter la mémoire de manière linéaire. Quelqu'un peut-il m'aider?
GameLoading
37
Ce n'est pas une réponse à la question. La question était de lire un fichier ligne par ligne pour réduire l'utilisation de la mémoire
doozMen
34

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 non-nouvelle ligne du fichier, jusqu'à 4095 à la fois. Si vous avez une ligne de plus de 4095 caractères, la lecture continue jusqu'à ce qu'elle atteigne une nouvelle ligne ou une fin de fichier.

Remarque : je n'ai pas testé ce code. Veuillez le tester avant de l'utiliser.

Adam Rosenfield
la source
1
juste changer [résultat appendFormat: "% s", tampon]; à [résultat appendFormat: @ "% s", tampon];
Codezy
1
comment modifieriez-vous le format pour accepter des lignes vides, ou plutôt des lignes constituées d'un seul caractère de nouvelle ligne?
jakev
Cela s'arrête tôt pour moi après 812 lignes. La 812ème ligne est "... 3 de plus", et cela fait que le lecteur affiche des chaînes vides.
sudo
1
J'ai ajouté une vérification pour dépasser les lignes vides: int fscanResult = fscanf (fichier, "% 4095 [^ \ n]% n% * c", buffer, & charsRead); if (fscanResult == 1) {[résultat appendFormat: @ "% s", tampon]; } else {if (feof (fichier)) {break; } else if (ferror (fichier)! = 0) {break; } fscanf (fichier, "\ n", nil, & charsRead); Pause; }
Go Rose-Hulman
1
Si je lis bien la documentation de fscanf, "%4095[^\n]%n%*c"consommera et jettera silencieusement un caractère à chaque lecture de tampon. Il semble que ce format suppose que les lignes seront plus courtes que la longueur du tampon.
Blago
12

Mac OS X est Unix, Objective-C est un sur-ensemble C, vous pouvez donc simplement utiliser old-school fopenet fgetsfrom <stdio.h>. Il est garanti de 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.

Kornel
la source
[Copier un commentaire anonyme] fgetsinclura le '\n'caractère, donc vous voudrez peut-être le supprimer avant de convertir la chaîne.
Kornel
9

Vous pouvez utiliser NSInputStreamqui a une implémentation de base pour les flux de fichiers. Vous pouvez lire des octets dans un tampon ( read:maxLength:méthode). Vous devez scanner vous-même la mémoire tampon pour les nouvelles lignes.

Dieerikh
la source
6

La manière appropriée de lire les fichiers texte dans Cocoa / Objective-C est documentée dans le guide de programmation String d'Apple. La section pour lire et écrire des fichiers devrait être exactement ce que vous recherchez. 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 êtes-vous en fait 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 s'appelle "Paragraphes et sauts de ligne" et est liée dans le 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 car je suis pas encore un utilisateur digne de confiance.)

Pour paraphraser Knuth: l'optimisation prématurée est la racine de tout mal. Ne supposez pas simplement que "lire le fichier entier en mémoire" est lent. L'avez-vous évalué? Savez-vous qu'il lit réellement le fichier entier en mémoire? Peut-être qu'il renvoie simplement un objet proxy et continue de lire dans les coulisses lorsque vous consommez la chaîne? ( Clause de non-responsabilité: je n'ai aucune idée si NSString fait réellement cela. Il pourrait le faire. ) Le point est: commencez par suivre la manière documentée de faire les choses. Ensuite, si les benchmarks montrent que cela n'a pas les performances que vous désirez, optimisez.

Stig Brautaset
la source
Puisque vous mentionnez les fins de ligne CRLF (Windows): C'est en fait un cas qui rompt la façon de faire Objective-C. Si vous utilisez l'une des -stringWithContentsOf*méthodes suivies par -componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet], il voit le \ret \nséparément et ajoute une ligne vide après chaque ligne.
Siobhán
Cela dit, la solution fgets échoue sur les fichiers CR uniquement. Mais ceux-ci sont (théoriquement) rares de nos jours, et fgets fonctionne à la fois pour LF et CRLF.
Siobhán
6

Un grand nombre de ces réponses sont de longs morceaux de code ou lisent 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. En outre, nous +1 la longueur de la chaîne parce que nous voulons faire de la place pour la terminaison NULL.

DCurro
la source
4

La lecture d'un fichier ligne par ligne (également pour les fichiers extrêmement volumineux) peut être effectuée par 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

Implémentation (.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

Lukaswelte
la source
4

Tout comme @porneL l'a dit, 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);
}
wdanxna
la source
4

Comme d'autres l'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
Bjørn Olav Ruud
la source
1

Cette réponse n'est PAS ObjC mais C.

Puisque ObjC est basé sur «C», pourquoi ne pas utiliser des fgets?

Et oui, je suis sûr qu'ObjC a sa propre méthode - je ne suis tout simplement pas encore assez compétent pour savoir ce que c'est :)

KevinDTimm
la source
5
Si vous ne savez pas comment le faire en Objective-C, alors pourquoi dire que ce n'est pas la réponse? Il y a de nombreuses raisons de ne pas passer directement au C si vous pouvez le faire autrement. Par exemple, les fonctions C gèrent char * mais il faut beaucoup plus de travail pour lire autre chose, comme des encodages différents. En outre, il veut des objets NSString. Tout compte fait, rouler vous-même n'est pas seulement plus de code, mais aussi sujet aux erreurs.
Quinn Taylor
3
Je suis d'accord avec vous à 100%, mais j'ai trouvé que (parfois) il est préférable d'obtenir une réponse qui fonctionne rapidement, de la mettre en œuvre et ensuite, lorsqu'une alternative plus correcte apparaît, utilisez-la. Ceci est particulièrement important lors du prototypage, en donnant la possibilité de faire fonctionner quelque chose et de progresser à partir de là.
KevinDTimm
3
Je viens de réaliser que ça commençait "Cette réponse" et non "La réponse". Doh! Je suis d'accord, il est certainement préférable d'avoir un hack qui fonctionne que du code élégant qui ne fonctionne pas. Je ne vous ai pas voté contre, mais jeter une supposition sans savoir ce que l'Objective-C pourrait avoir n'est probablement pas très utile non plus. Même ainsi, faire un effort est toujours mieux que quelqu'un qui sait et n'aide pas ... ;-)
Quinn Taylor
Cela ne répond pas à la question. Pour critiquer ou demander des éclaircissements à un auteur, laissez un commentaire sous sa publication.
Robotic Cat
1
@KevinDTimm: Je suis d'accord; Je suis seulement désolé de ne pas avoir repéré c'était une réponse de 5 ans. C'est peut-être une metaquestion; Les très anciennes questions d'utilisateurs réguliers devraient-elles pouvoir être signalées pour examen?
Robotic Cat
0

à partir de la réponse de @Adam Rosenfield, la chaîne de formatage de fscanfserait modifiée comme ci-dessous:

"%4095[^\r\n]%n%*[\n\r]"

il fonctionnera dans les fins de ligne osx, linux, windows.

sooop
la source
0

Utiliser une catégorie ou une extension pour nous rendre la 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
}
Kaz Yoshikawa
la source
0

J'ai trouvé la réponse de @lukaswelte et le code de Dave DeLong très utiles. Je cherchais une solution à ce problème, mais j'avais besoin d'analyser de gros fichiers \r\nnon seulement \n.

Le code tel qu'il est écrit contient un bogue s'il est analysé 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
hovey
la source
0

J'ajoute cela 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 du 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 crédit va à @Adam Rosenfield et @sooop

Blago
la source
0

Je vois que beaucoup de ces réponses reposent sur la lecture du fichier texte entier en mémoire au lieu de le prendre un morceau à la fois. Voici ma solution dans Swift moderne et agréable, en utilisant FileHandle pour réduire l'impact de la mémoire:

enum MyError {
    case invalidTextFormat
}

extension FileHandle {

    func readLine(maxLength: Int) throws -> String {

        // Read in a string of up to the maximum length
        let offset = offsetInFile
        let data = readData(ofLength: maxLength)
        guard let string = String(data: data, encoding: .utf8) else {
            throw MyError.invalidTextFormat
        }

        // Check for carriage returns; if none, this is the whole string
        let substring: String
        if let subindex = string.firstIndex(of: "\n") {
            substring = String(string[string.startIndex ... subindex])
        } else {
            substring = string
        }

        // Wind back to the correct offset so that we don't miss any lines
        guard let dataCount = substring.data(using: .utf8, allowLossyConversion: false)?.count else {
            throw MyError.invalidTextFormat
        }
        try seek(toOffset: offset + UInt64(dataCount))
        return substring
    }

}

Notez que cela préserve le retour chariot à la fin de la ligne, donc en fonction de vos besoins, vous souhaiterez peut-être ajuster le code pour le supprimer.

Utilisation: ouvrez simplement un descripteur de fichier vers votre fichier texte cible et appelez readLineavec une longueur maximale appropriée - 1024 est la norme pour le texte brut, mais je l'ai laissé ouvert au cas où vous sauriez qu'il sera plus court. Notez que la commande ne débordera pas la fin du fichier, vous devrez donc peut-être vérifier manuellement que vous ne l'avez pas atteint si vous avez l'intention d'analyser le tout. Voici un exemple de code qui montre comment ouvrir un fichier à myFileURLet le lire ligne par ligne jusqu'à la fin.

do {
    let handle = try FileHandle(forReadingFrom: myFileURL)
    try handle.seekToEndOfFile()
    let eof = handle.offsetInFile
    try handle.seek(toFileOffset: 0)

    while handle.offsetInFile < eof {
        let line = try handle.readLine(maxLength: 1024)
        // Do something with the string here
    }
    try handle.close()
catch let error {
    print("Error reading file: \(error.localizedDescription)"
}
Cendre
la source
-2

Voici une belle solution simple que j'utilise pour les petits fichiers:

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);
    }
}
Chris
la source
Il demandait comment lire une ligne à la fois afin qu'elle ne lise pas tout le contenu en mémoire. Votre solution crée une chaîne avec tout le contenu puis la divise en lignes.
David
-7

Utilisez ce script, cela fonctionne très bien:

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);
abhi
la source
1
Ce que @fisninear dit, c'est que cela ne répond pas au désir du PO de réduire l'utilisation de la mémoire. L'OP ne demandait pas comment utiliser la méthode (qui charge le fichier entier en mémoire), il demandait des alternatives respectueuses de la mémoire pour les gros fichiers texte. Il est tout à fait possible d'avoir des fichiers texte de plusieurs gigaoctets, ce qui crée évidemment un problème de mémoire.
Joshua Nozzi