objective-c - txt - leer una linea especifica de un archivo en c
Objetivo-C: Leer un archivo línea por línea (17)
¿Cuál es la forma adecuada de tratar con archivos de texto grandes en Objective-C? Digamos que necesito leer cada línea por separado y quiero tratar cada línea como un NSString. ¿Cuál es la forma más eficiente de hacer esto?
Una solución es usar el método NSString:
+ (id)stringWithContentsOfFile:(NSString *)path
encoding:(NSStringEncoding)enc
error:(NSError **)error
y luego divide las líneas con un separador de nueva línea, y luego itera sobre los elementos en la matriz. Sin embargo, esto parece bastante ineficiente. ¿No hay una manera fácil de tratar el archivo como una secuencia, enumerando sobre cada línea, en lugar de simplemente leer todo de una vez? Algo así como Java''s java.io.BufferedReader.
Al igual que @porneL dijo, la C api es muy útil.
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);
}
Aquí hay una buena solución simple que uso para archivos más pequeños:
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);
}
}
Como otros han respondido, tanto NSInputStream como NSFileHandle son buenas opciones, pero también se pueden hacer de una manera bastante compacta con NSData y la asignación de 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
Encontré la respuesta de @lukaswelte y el código de Dave DeLong muy útil. Estaba buscando una solución a este problema, pero necesitaba analizar archivos grandes por /r/n
no solo /n
.
El código tal como está escrito contiene un error si lo analiza más de un personaje. Cambié el código de la siguiente manera.
.h archivo:
#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
archivo .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
Esa es una gran pregunta. Creo que @Diederik tiene una buena respuesta, aunque es desafortunado que Cocoa no tenga un mecanismo para exactamente lo que quieres hacer.
NSInputStream
permite leer fragmentos de N bytes (muy similares a java.io.BufferedReader
), pero tiene que convertirlos a NSString
por su cuenta, luego buscar nuevas líneas (o cualquier otro delimitador) y guardar los caracteres restantes para el siguiente lectura, o leer más caracteres si aún no se ha leído una nueva línea. ( NSFileHandle
permite leer un NSData
que luego puede convertir a un NSString
, pero básicamente es el mismo proceso).
Apple tiene una Guía de programación de Stream que puede ayudar a completar los detalles, y esta pregunta de SO también puede ser útil si va a tratar con buffers uint8_t*
.
Si va a leer cadenas de este tipo con frecuencia (especialmente en diferentes partes de su programa), sería una buena idea encapsular este comportamiento en una clase que pueda manejar los detalles por usted, o incluso subclases de NSInputStream
(está diseñado para estar subclasificado ) y agregar métodos que le permitan leer exactamente lo que desea.
Para el registro, creo que esta sería una buena característica para agregar, y voy a presentar una solicitud de mejora de algo que hace esto posible. :-)
Editar: Resulta que esta solicitud ya existe. Hay un radar que data de 2006 para esto (rdar: // 4742914 para personas internas de Apple).
Esta respuesta NO es ObjC sino C.
Dado que ObjC está basado en ''C'', ¿por qué no utilizar fgets?
Y sí, estoy seguro de que ObjC tiene su propio método. Todavía no soy lo suficientemente competente como para saber qué es :)
Esto debería funcionar:
#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;
}
Use de la siguiente manera:
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);
Este código lee caracteres no nuevos del archivo, hasta 4095 a la vez. Si tiene una línea que tiene más de 4095 caracteres, sigue leyendo hasta que llegue a una línea nueva o al final del archivo.
Nota : No he probado este código. Por favor, pruébelo antes de usarlo.
Esto funcionará para la lectura general de una String
de Text
. Si desea leer texto más extenso ( texto de gran tamaño) , utilice el método que otras personas mencionaron aquí, como el almacenado en búfer (reserve el tamaño del texto en el espacio de la memoria) .
Digamos que leíste un archivo de texto.
NSString* filePath = @""//file path...
NSString* fileRoot = [[NSBundle mainBundle]
pathForResource:filePath ofType:@"txt"];
Desea deshacerse de la nueva línea.
// 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:@";"]];
Ahí tienes.
Estoy agregando esto porque todas las otras respuestas que probé quedaron cortas de una forma u otra. El siguiente método puede manejar archivos grandes, líneas largas arbitrarias, así como líneas vacías. Se ha probado con contenido real y eliminará el carácter de nueva línea de la salida.
- (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;
}
El crédito es para @Adam Rosenfield y @sooop
La forma apropiada de leer archivos de texto en Cocoa / Objective-C está documentada en la guía de programación de cadenas de Apple. La sección para leer y escribir archivos debe ser lo que buscas. PD: ¿Qué es una "línea"? ¿Dos secciones de una cadena separadas por "/ n"? O "/ r"? O "/ r / n"? ¿O tal vez estás realmente después de los párrafos? La guía mencionada anteriormente también incluye una sección sobre la división de una cadena en líneas o párrafos. (Esta sección se llama "Párrafos y saltos de línea", y está enlazada en el menú del lado izquierdo de la página que mencioné anteriormente. Desafortunadamente, este sitio no me permite publicar más de una URL como soy no es un usuario confiable todavía).
Parafraseando a Knuth: la optimización prematura es la raíz de todo mal. No asuma simplemente que "leer todo el archivo en la memoria" es lento. ¿Lo has comparado? ¿Sabes que realmente lee todo el archivo en la memoria? Tal vez simplemente devuelva un objeto proxy y siga leyendo detrás de escena mientras consume la cadena. ( Descargo de responsabilidad: no tengo idea si NSString realmente hace esto. Es concebible que pueda ) . El punto es: primero ve con la forma documentada de hacer las cosas. Luego, si los puntos de referencia muestran que esto no tiene el rendimiento que desea, optimícelo.
Mac OS X es Unix, Objective-C es C superconjunto, por lo que puedes usar fopen
y fgets
de la vieja escuela desde <stdio.h>
. Está garantizado para funcionar.
[NSString stringWithUTF8String:buf]
convertirá la cadena C a NSString
. También hay métodos para crear cadenas en otras codificaciones y crear sin copiar.
Muchas de estas respuestas son trozos largos de código o leen en el archivo completo. Me gusta usar los métodos c para esta misma tarea.
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);
}
Tenga en cuenta que fgetln no mantendrá su carácter de nueva línea. Además, tenemos +1 la longitud de str porque queremos dejar espacio para la terminación NULL.
Para leer un archivo línea por línea (también para archivos extremadamente grandes) se puede hacer con las siguientes funciones:
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 clase DDFileReader que habilita esto es la siguiente:
Archivo de interfaz (.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
Implementación (.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 clase fue hecha por Dave DeLong
Puede usar NSInputStream
que tiene una implementación básica para las secuencias de archivos. Puede leer bytes en un buffer ( read:maxLength:
method). Tienes que escanear el buffer para nuevas líneas tú mismo.
Usa este script, funciona genial:
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);
Usando categoría o extensión para hacer nuestra vida un poco más fácil.
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
}
de la respuesta de @Adam Rosenfield, la cadena de formateo de fscanf
se cambiaría como se muestra a continuación:
"%4095[^/r/n]%n%*[/n/r]"
funcionará en osx, linux, terminaciones de líneas de ventanas.