File Handling In Pascal/es

From Lazarus wiki
Revision as of 00:16, 15 February 2020 by Trev (talk | contribs) (Fixed syntax highlighting; deleted category included in page template)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

العربية (ar) English (en) español (es) suomi (fi) français (fr) 日本語 (ja) русский (ru) 中文(中国大陆)‎ (zh_CN) 中文(台灣)‎ (zh_TW)

Descripción

Algo que necesitan conocer todos los programadores es como trabajar con ficheros. Los ficheros se utilizan para almacenar datos de forma persistente, i.e. almacenar datos de manera que puedan ser retornados en un momento posterior sin tener que volver a crearlos. Los ficheros pueden utilizarse para almacenar configuraciones de usuario, reportes de error, medidas o resultados de cálculos, etc. Esta página explica lo básico sobre manejo de ficheros.

Estilo del procedimiento antiguo

Cuando se utilizan ficheros en el modo clásico de Pascal (no orientado a objetos) se puede utilizar el tipo TextFile (o simplemente Text) para almacenar texto, que está estructurado típicamente en líneas. Cada línea finaliza con una marca de fin de línea (EOL=End Of Line). En este tipo de fichero se pueden almacenar tanto cadenas (strings) como números (integer, real...) formateados además de la forma que más nos convenga. Estos ficheros pueden posteriormente abrirse para visualizarlos o editarlos mismamente con el IDE de Lazarus o cualquier otro editor de texto.

Para propósitos específicos se puede crear un tipo de fichero personalizado que puede almacenar únicamente un tipo de dato. Por ejemplo:

...
type
  TIntegerFile  = file of integer;  // Permite escribir únicamete números de tipo entero (integer) al fichero.
  TExtendedFile = file of extended; // Permite escribir úncamente números de tipo real al fichero.
  TCharFile     = file of char;     // Permite escribir únicamente caracteres simples al fichero.
  TPCharFile    = file of PChar;    // Permite escribir únicamente PChar al fichero.
  TByteFile     = file of byte;     // Permite escribir únicamente bytes al fichero.
...

Manejo de errores de Entrada/Salida

El I/O error handling flag o flag (bandera) de Entrada/Salida (I/O -> Input/Output) indica al compilador como manejarse con las situaciones de error: lanzar una excepción o almacenar el resultado de la entrada/salida en la variable IOResult.

El flag de manejo del error de entrada/salida es una directiva del compilador. Para habilitarlo/deshabilitarlo escribimos:

{$I+} // Los errores generarán una excepción EInOutError (por defecto)
{$I-} // Suprime los errores de entrada/salida: chequea la veriable IOResult para saber su código de error.

Mediante la supresión de los errores de entrada/salida utilizando ({$I-}) los resultados de la operación con ficheros se meterán en la variable IOSResult. Este es un cardinal (number) type. Cada uno de los distintos numeros indicarán los diferentes errores obtenidos. Estos códigos de error se pueden consultar en la documentación [1].

Procedimientos de fichero

Estas funciones y procedimientos de manejo de ficheros se encuentran en la unit 'system'. Ver la documentación de FPC para más detalles: Referenccia sobre la unidad 'System'.

  • AssignFile (previene del uso del antiguo procedimiento Assign ) - Asigna un nombre a un fichero.
  • Append - Abre un fichero ya existente en el modo añadir al final del mismo.
  • BlockRead - Lee un bloque de datos de un fichero sin tipo poniéndolo en memoria.
  • BlockWrite - Escribe un bloque de datos desde la memoria hacia un fichero sin tipo.
  • CloseFile (previene el uso del procedimiento antiguo Close ) - Cierra un fichero que está abierto.
  • EOF - Chequea si se ha llegado al final del fichero (EOF=End Of File), devuelve true si ha llegado y false si todavía no.
  • Erase - Borra un fiechro del disco.
  • FilePos - Retorna da la posición en que nos encontramos dentro del fichero.
  • FileSize - Retorna el tamaño del fichero.
  • Flush - Escribe los buffers de fichero a disco.
  • IOResult - Retorna el resultado de la última operación de entrada/salida (I/O) de ficheros.
  • Read - Lee desde un fichero tipo texto.
  • ReadLn - Lee desde un fichero tipo texto (una línea entera) y salta a la siguiente línea.
  • Reset - Abre un fichero para lectura.
  • Rewrite - Crea un fichero para escritura.
  • Seek - Cambia a una posición dentro del fichero.
  • SeekEOF - Sitúa la posición dentro del fichero en su final.
  • SeekEOLn - sitúa la posición del fichero al final de la línea.
  • Truncate - Trunca el fichero en la posición indicada.
  • Write - Escribe una variable en el fichero.
  • WriteLn - Escribe una variable a un fichero de texto y salta a una nueva línea.

Ejemplo

Un ejemplo completo de manejo de un fichero de texto del tipo TextFile: (es aconsejable utilizar estos ejemplos con el IDE de FreePascal o bien creando una aplicación de consola, nos facilitará así ver los mensajes de WriteLn. En este último caso nos crea automáticamente una estructura de programa en la cual tendremos que insertar el código en las secciones correspondientes por ejemplo donde pone { add your program here }.

program CreateFile;

uses
 Sysutils;

const
  C_FNAME = 'textfile.txt';

var
  tfOut: TextFile; // tipo fichero de salida.

begin
  // Establece el nombre del fichero que vamos a crear
  AssignFile(tfOut, C_FNAME);

  // Habilitamos el uso de excepciones para interceptar los errores (esto es como está por defecto por lo tanto no es absolutamente requerido)

  {$I+}

  // Embebe la creación del fichero en un bloque try/except para manejar los errores elegántemente.
  
  try
  
    //crea el fichero, escribe algo de texto y lo cierra.
    ReWrite(tfOut);
    WriteLn ('A continuación escribimos algo de texto al fichero ya que ponemos en WriteLn tfOut como salida');
    WriteLn(tfOut, '¡Hola textfile!');
    WriteLn(tfOut, 'Esto que se escribe directamente al fichero, no aparece por pantalla: ', 42);

    CloseFile(tfOut); // Hemos terminado de escribir el texto en el fichero, por tanto le cerramos.

  except
    // Si ocurre algún error podemos encontrar la razón en E: EInOutError
      WriteLn('A continuación imprimimos en pantalla mediante E.ClassName / E.Message:'); 
      WriteLn('Ha ocurrido un error en el manejo del fichero. Detalles: ', E.ClassName, '/', E.Message);
      WriteLn ('Por otro lado podemos imprimir en pantalla el valor de la variable IOResult que es: ',IOResult);

    //  Tambien podemos ponerlo de la forma: 
    //  on E: EInOutError do
    //    begin
    //      Writeln('Ha ocurrido un error en el manejo del fichero. Detalle: '+E.ClassName,'/',E.Message);
    //    end;

  // Da información y espera por la pulsación de una tecla.
  writeln('Fichero ', C_FNAME, ' creado si todo fue bien. Presiona Enter para finalizar.');
  ReadLn;
end.

Ahora abre el fichero generado utilizando para ello cualquier editor y verás justamente el texto que hemos escrito mediante código. Puedes probar el manejo de errores ejecutando el programa de nuevo, en esta ocasión la prueba que puedes realizar es establecer el atributo del fichero en modo solo lectura de forma que el programa no pueda hacer el rewrite y ver el error que nos lanza. Los atributos del fichero los podemos modificar mediante la función FileSetAttr (unidad sysutils) y consultarlos mediante FileGetAttr.

Las constantes para los atributos de FileGetAttr y FileSetAttr son:

  • faReadOnly: solo lectura.
  • faHidden: oculto (en Linux los encontramos en los ficheros que comienzan con un punto).
  • faSysFile: sistema.
  • faVolumeId: etiqueta de volumen.
  • faDirectory: directorio.
  • faArchive: archivo.

Adicionalmente es interesante echar un vistazo a la función FileOpen donde lista los modos de fichero que acepta, así como otras funciones relacionadas, las cuales tienen su equivalencia a las listadas más arriba.

Nota que el manejo de excepciones se ha utilizado como una manera fácil de realizar múltiples operaciones con ficheros y manejar los errores que obtengamos como resultado. También puedes utilizar {$I-}, pero en este caso hay que consultar el valor almacenado en la variable IOResult después de cada operación y modificar tu próxima operación para tratar el posible error.

Lo siguiente muestra como se puede añadir texto a un fichero del tipo textfile (al final del contenido ya existente):

program AppendToFile;

uses
 Sysutils;

const
  C_FNAME = 'textfile.txt';

var
  tfOut: TextFile;

begin
  // Establece el nombre del fichero que va a recibir más texto.
  AssignFile(tfOut, C_FNAME);

  // Embebe el manejo del fichero en un bloque try/except para gestionar los errores de manera elegante.
  try
    // Abre el fichero en la modalidad añadir (appending), escribe algo de texto y lo cierra.
    append(tfOut);

    WriteLn(tfOut, ' Hola de nuevo fichero de texto. ');
    WriteLn(tfOut, 'El resultado de 6 * 7 = ', 6 * 7);

    CloseFile(tfOut);

  except
    on E: EInOutError do
     writeln('Ha ocurrido un error en el manejo del fichero. Detalles: ', E.Message);
  end;

  // Da información y espera a que se pulse una tecla.
  WriteLn('Fichero ', C_FNAME, ' puede contener más texto. Presiona Enter para finalizar.');
  ReadLn;
end.

Leyendo un fichero de texto (textfile):

program ReadFile;

uses
 Sysutils;

const
  C_FNAME = 'textfile.txt';

var
  tfIn: TextFile;
  s: string;

begin
  // Da algo de información
  WriteLn('Leyendo el contenido del fichero: ', C_FNAME);
  WriteLn('=========================================');

  // Establece el nombre del fichero a leer.

  AssignFile(tfIn, C_FNAME);

  // Embebe el manejo del fichero en un bloque try/except para manejar errores de manera elegante.
 try
    // Abre el fichero en la modalidad de lectura.
    ReSet(tfIn);

    // Se mantiene leyendo líneas hasta alcanzar el final del fichero.
    while not eof(tfIn) do  // Mientras no fin de fichero haz...
    begin
      ReadLn(tfIn, s); // Lee una línea de texto desde el fichero.
      WriteLn(s);  // Escribe la línea de texto leida anteriormente mostrándola en pantalla.
    end;

    // Realizado, por tanto procedemos a cerrar el fichero.
    CloseFile(tfIn);

  except
    on E: EInOutError do
    WriteLn('Ha ocurrido un error en el manejo del fichero. Detalles: ', E.Message);
  end;

  // Espera la intervención del usuario para finalizar el programa.
  WriteLn('=========================================');
  WriteLn('fichero ', C_FNAME, ' fue probablemente leido. Presiona Enter para finalizar.');
  ReadLn;
end.

Estilo de Objetos

Adicionalmente al antiguo estilo de rutinas de manejo de ficheros mencionada anteriormente, existe un nuevo sistema que utiliza el concepto de streams (una corriente de datos o flujo de datos) a un nivel de abstracción superior. Esto significa que los datos pueden ser leidos o escritos a cualquier ubicación (disco, memoria, puertos de hardware, etc.) por un interface uniforme.

Asociado a esto tenemos que la mayor parte de las clases manejadoras de cadenas tienen la habilidad de cargar y salvar contenidos desde/hacia un fichero. Estos métodos usualmente tienen los nombres SaveToFile y LoadFromFile. Otro montón de objetos (tales como las grids o rejillas de Lazarus) tienen funcionalidades similares, incluyendo los datasets de Lazarus (DBExport). Vale la pena echar un vistazo a la documentación o al código fuente antes de tratar de crear rutinas propias de salvado/carga.

Ficheros Binarios

Para abrir ficheros en modo de acceso directo se puede utilizar TFileStream. Esta clase encapsula los procedimientos de sistema FileOpen, FileCreate, FileRead, FileWrite, FileSeek y FileClose que residen en la unidad SysUtils.

rutinas de entrada/salida (I/O)

Fijate en el ejemplo de abajo como se encapsula el manejo de ficheros con un bloque try..except de forma que los errores se manejan correctamente justamente como sucedia con las rutinas clásicas equivalentes (para no enrevesar el ejemplo el fsOut.write no se pone dentro del bloque try...finally).

program WriteBinaryData;
{$mode objfpc}

uses
  Classes, Sysutils;

const
  C_FNAME = 'binarydata.bin';

var
  fsOut    : TFileStream;
  ChrBuffer: array[0..2] of char;

begin
  // Establecemos algunos datos aleatorios para su almacenamiento.
  ChrBuffer[0] := 'A';
  ChrBuffer[1] := 'B';
  ChrBuffer[2] := 'C';

  // Interceptamos los errores en caso de que el fichero no pueda crearse.
  try
    // Creamos la instancia del flujo de datos,escribimos en el mismo y lo liberamos para evitar pérdidas de memoria.
    fsOut := TFileStream.Create( C_FNAME, fmCreate);
    fsOut.Write(ChrBuffer, sizeof(ChrBuffer));
    fsOut.Free;

  // Manejamos los errores
  except
    on E:Exception do
      writeln('El fichero ', C_FNAME, ' no se ha podido crear debido a: ', E.Message);
  end;

  // Damos algo algo de información y esperamos la pulsación de una tecla.
  writeln('El fichero ', C_FNAME, ' se ha creado si todo ha ido bien. Presiona Enter para finalizar.');
  readln;
end.

Se puede por tanto realizar la carga de un fichero completo a memoria siempre que su tamaño lógicamente sea menor que la cantidad de memoria disponible en el sistema. Para tamaños mayores de la memoria física disponible puede que el sistema operativo comience a utilizar la memoria páginada en el fichero de intercambio de memoria, haciendo esta función menos útil desde el punto de vista del rendimiento.

program ReadBinaryDataInMemoryForAppend;
{$mode objfpc}

uses
  Classes, Sysutils;

const
  C_FNAME = 'binarydata.bin';

var
  msApp: TMemoryStream;

begin
  // Creamos el Stream de memoria para tener la corriente de datos.
  msApp := TMemoryStream.Create;

  // Intercepción de errores en caso de que el fichero no pueda leerse o escribirse.
  try
    // Read the data into memory
    msApp.LoadFromFile(C_FNAME); // Cargamos el Stream en memoria con el contenido leido desde un fichero.

    // Posicionamos con Seek al final del Stream de manera que podamos añadirle datos.
    msApp.Seek(0, soEnd); // donde 0 nos indica el offset o desplazamiento como primer parámetro, mientras que el segundo
    // parámetro nos indica a donde movernos, pudiendo ser soBeginning, soCurrent o bien soEnd, como en este caso.

    // Escribe datos arbitrarios al Stream de memoria.
    msApp.WriteByte(68);
    msApp.WriteAnsiString('Algo de texto extra');
    msApp.WriteDWord(671202);

    // Almacena de nuevo los datos a disco, sobreescribiendo el contenido previo.
    msApp.SaveToFile(C_FNAME);

  // Maneja los errores.
  except
    on E:Exception do
      writeln('El fichero ', C_FNAME, ' no puede leerse o escribirse debido a: ', E.Message);
  end;

  // Limpiamos, liberando la memoria que estaba utilizando el TMemoryStream.
  msApp.Free;

  // Damos algo de información y esperamos por la pulsación de una tecla
  writeln('El fichero ', C_FNAME, ' fue extendido en contenido si todo fue bien. Presiona Enter para finalizar.');
  readln;
end.

Con ficheros muy largos de varios GB, puede que sea necesario leer en buffers, digamos de 4096 bytes por ejemplo (se aconseja aquí que se utilicen múltiplos del tamaño de cluster del sistema de fichero) y realizamos algo con cada buffer de datos leidos.

var
  TotalBytesRead, BytesRead : Int64;
  Buffer : array [0..4095] of byte;  // o, array [0..4095] of char
  FileStream : TFileStream;

try
  FileStream := TFileStream.Create;
  FileStream.Position := 0;  // Nos aseguramos con esto que nos encontramos al comienzo del fichero.
  while TotalBytesRead <= FileStream.Size do  // Mientras la cantidad de datos leidos es menor o igual que
  //el tamaño del Stream realizar:
  begin
    BytesRead := FileStream.Read(Buffer,sizeof(Buffer));  // Leer en bloques de datos de 4096 bytes
    inc(TotalBytesRead, BytesRead);                       // Incrementa TotalByteRead al tamaño del buffer, i.e. 4096 bytes
    // Realizar algo con el buffer de datos.
  end;

Copia de ficheros

Con lo aprendido anteriormente podemos implementar una función simple de copia de ficheros (FreePascal no tiene ninguna en su RTL aunque Lazarus tiene copyfile) - se puede ajustar para ficheros de mayor tamaño, etc:

program FileCopyDemo;
// Demonstración de la función FileCopy.

{$mode objfpc}

uses
  classes;

const
  fSource = 'test.txt';
  fTarget = 'test.bak';

function FileCopy(Source, Target: string): boolean;
// Copia fuente a destino, sobreescribe destino.
// Cachea el contenido completo en memoria.
// Retorna true si tiene exito; falso en caso contrario.
var
  MemBuffer: TMemoryStream;
begin
  result := false;
  MemBuffer := TMemoryStream.Create;
  try
    MemBuffer.LoadFromFile(Source);
    MemBuffer.SaveToFile(Target); 
    result := true  // Si no se genera un error la función nos devuelve true, caso contrario false.
  except
  end;
  // Clean up
  MemBuffer.Free  // Libera el buffer en memoria para tener disponibles esos recursos.
end;

begin
  If FileCopy(fSource, fTarget)
    then writeln('Fichero ', fSource, ' copiado a ', ftarget)
    else writeln('Fichero ', fSource, ' no copiado a ', ftarget);
  readln()
end.

Manejando ficheros de texto (TStringList)

En general, se puede utilizar la clase TStringList para ficheros de texto, de forma que se cargue el fichero completo en memoria y tengamos un acceso sencillo a las líneas del mismo. Por supuesto, también se puede realizar la operación inversa para escribir el StringList al fichero:

program StringListDemo;
{$mode objfpc}

uses
  Classes, SysUtils;

const
  C_FNAME = 'textfile.txt';

var
  slInfo: TStringList;

begin
  // Crea una instancia de StringList para manejar el fichero de texto.
  slInfo := TStringList.Create;

  // Embebe el manejo del fichero en un bloque try/except para menejar los errores de manera elegante.
  try
    // Load the contents of the textfile completely in memory
    slInfo.LoadFromFile(C_FNAME);

    // Añade más contenido.
    slInfo.Add('Una línea extra añadida al texto');
    slInfo.Add('Y otra más.');
    slInfo.Add('Paremos aquí.');
    slInfo.Add('La hora actual es ' + DateTimeToStr(now));

    // Y escribimos el contenido de vuelta al disco, reeeplazando su contenido original.
    slInfo.SaveToFile(C_FNAME);

  except
    // si se genera un error la razón la podemos encontrar aquí.
    on E: EInOutError do
      writeln('Error en el manejo de ficheros. Razón: ', E.Message);
  end;

  // Limpiamos, liberando así memoria.
  slInfo.Free;

  // Damos algo de información y esperamos a que se pulse una tecla.
  writeln('El fichero ', C_FNAME, ' se ha actualizado si todo salió correctamente. Presiona Enter para finalizar.');
  readln;
end.

Demostración: salvando una cadena simple a un fichero

Para escribir una simple cadena a memoria puede que necesites el procedimiento definido a continuación. Fíjate que las cadenas (Strings) en FreePascal pueden ser extremadamente largas, esto es además útil para escribir un bloque grande o datos tipo texto a un fichero.

program SaveStringToPathDemo;
{$mode objfpc}

uses
  Classes, sysutils;

const
  C_FNAME = 'textstringtofile.txt';

// SaveStringToFile: función para almacenar una cadena de texto en un fichero en disco.
// Si el resultado de la función es igual a true, entonces significa que la cadena se escribió correctamente,
// en caso contrario significa que se ha encontrado un error.

function SaveStringToFile(theString, filePath: AnsiString): boolean;
var
  fsOut: TFileStream;
begin
  // Por defecto asume que la escritura generará fallo.
  result := false;

  // Escribe la cadena a un fichero, interceptando errores potenciales en el proceso.
  try
    fsOut := TFileStream.Create(filePath, fmCreate);
    fsOut.Write(theString[1], length(theString));
    fsOut.Free;

    // En este punto sabemos que la escritura se realizó correctamente.
    result := true

  except
    on E:Exception do
      writeln('La cadena no pudo escribirse. Detalles: ', E.ClassName, ': ', E.Message);
  end
end;

//
// Programa principal.
//
begin
  // Trata de salvar un simple textstring a un fichero y da información si se realiza correctamente.
  if SaveStringToFile('>> este texto va a ser almacenado <<', C_FNAME) then
    writeln('Texto escrito correctametne a fichero')
  else
    writeln('Ha fallado la escritura del texto a fichero.');

  // Espera a que el usuario presione la tecla Enter.
  readln
end.

Ver también

  • CopyFile Lazarus function (also available for command line programs) that copies files for you
  • File