TEMA 4. MANEJO DE ARCHIVOS Y CARPETAS 4.1. INTRODUCCIÓN Las aplicaciones que hemos creado hasta ahora han obtenido los datos que han precisado a través de la información introducida por el usuario mediante los dispositivos de entrada estándar (teclado, ratón), mostrando resultados al usuario por pantalla. Toda esta información ha persistido únicamente durante la ejecución de la aplicación, si volvemos a utilizarla en otra ocasión habrá que subministrar de nuevo los datos de partida necesarios para obtener los resultados. En muchos casos esto nos supone una grave limitación. Por ejemplo, si pretendemos manejar algunos datos personales de nuestros usuarios, sería muy molesto tener que solicitarlos cada vez que usen la aplicación. Lo idóneo sería solicitarlos la primera vez y conservarlos de algún modo para sucesivas ocasiones. ¿Cómo conservar datos de una ejecución a otra? Mediante archivos que guardaremos en algún medio accesible por nuestras aplicaciones. Un archivo es una agrupación de datos, que, como ya hemos mencionado, es accesible por una computadora. Se identifica por su nombre, opcionalmente una extensión y su ubicación dentro del sistema de archivos que lo contiene. Normalmente los archivos se agrupan en carpetas o directorios según su contenido, propósito o cualquier otro criterio para una mejor organización de la información. En este tema vamos a aprender a manejar los archivos y carpetas desde nuestras aplicaciones para acceder así a su contenido. Para ello, .Net pone a nuestra disposición el espacio de nombres System.IO. System.IO contiene tipos que nos permitirán leer y escribir en archivos, así como tipos que proporcionan funcionalidad para manejar los archivos y carpetas. Programación .NET (II). Manejo de archivos y carpetas 4.1 4.2. ACCESO AL SISTEMA DE FICHEROS La comunicación de información desde un origen hasta una aplicación se produce a través de un flujo de información (stream). Este elemento será un objeto que actuará de intermediario entre la aplicación y el origen. De este modo la aplicación lee o escribe en el flujo de información sin importarle de donde viene la información o a donde va, y sin importar el tipo de dispositivo que alberga el archivo. El nivel de abstracción que nos ofrece el flujo de información va a permitir que la forma de leer o escribir información desde nuestras aplicaciones sea generalmente el mismo. Lectura Escritura Abrir un flujo desde un fichero Mientras haya información Leer información Cerrar el flujo Abrir un flujo hacia un fichero Mientras haya información Escribir información Cerrar el flujo Podremos distinguir dos tipos de acceso al archivo según la forma en que podemos recuperar su información: acceso secuencial y acceso aleatorio. Vamos a definir ambos tipos. 4.2 Programación .NET (II). Manejo de archivos y carpetas Acceso Secuencial Es la forma más simple de acceder a un archivo. La información se leerá o escribirá de forma secuencial, eso es desde el principio hasta el fin del archivo. Se utiliza generalmente con archivos de texto en los que se escribe toda la información desde el principio hasta el final y se lee de igual forma. Acceso Aleatorio El acceso aleatorio a un fichero nos permite leer o escribir a partir de determinada posición dentro del fichero. Es útil para acceder a ciertos datos dentro de un archivo sin tener que recorrer los datos previos para llegar hasta ellos, como sucede con el acceso secuencial. El espacio de nombres System.IO de la biblioteca .NET contiene las clases necesarias para leer y escribir en archivos. A continuación vamos a describir las más importantes. Clase FileStream Con un flujo de la clase FileStream podremos leer o escribir datos en un fichero byte a byte. Proporciona tanto acceso secuencial como aleatorio. Un flujo de este tipo permitirá tanto leer como escribir en el archivo vinculado al mismo. Deriva de la clase abstracta Stream Esta clase nos proporciona los siguientes constructores: Sub New(ByVal nombre As String, ByVal modo As FileMode) Con este constructor abrimos un flujo de entrada y salida vinculado al fichero especificado en nombre. El parámetro modo indicará la forma de apertura del archivo. Programación .NET (II). Manejo de archivos y carpetas 4.3 Sub New(ByVal nombre As String, ByVal modo As FileMode, _ ByVal acceso As FileAccess) Este otro constructor hace lo mismo que el anterior, salvo que se puede especificar el tipo de acceso al archivo (lectura, escritura o lectura y escritura) en el parámetro acceso. Los parámetros modo y acceso son de los tipos enumerados FileMode y FileAccess respectivamente. A continuación puedes ver los valores que puede tomar cada uno. Posibles valores para FileMode CreateNew Create Crear un nuevo archivo. Si ya existe se lanzará una excepción de tipo IOException. Crear un nuevo archivo o sobrescribirlo si ya existe. Open Abrir un archivo existente. Si no existe se lanzará una excepción de tipo FileNotFoundException. OpenOrCreate Si el archivo existe lo abre, si no lo crea. Truncate Abre un archivo existente y lo trunca a cero bytes de longitud. Append Abre un fichero para añadir datos al final del mismo. Si no existe lo crea. Posibles valores para FileAccess 4.4 Read El archivo será accesible para lectura. ReadWrite El archivo será accesible para lectura y escritura. Write El archivo será accesible para escritura. Programación .NET (II). Manejo de archivos y carpetas A continuación realizaremos un ejemplo que ilustrará el uso de la clase FileStream. Para ello, creamos un nueva aplicación de consola en Visual Basic Express. Este tipo de aplicación nos permitirá poder leer texto de forma simple, desde la entrada estándar. 'Importamos el espacio de nombres System.IO Imports System.IO Module EscribirFS Public Sub Main() Dim flujo As FileStream Dim texto(70) As Byte Dim car, contador As Integer 'leemos caracteres car = Console.Read While (car <> 13 And contador < 70) texto(contador) = Convert.ToByte(car) contador = contador + 1 car = Console.Read End While 'creamos el flujo y escribimos! flujo = New FileStream("c:\archivo.txt", FileMode.Create, _ FileAccess.Write) flujo.Write(texto, 0, contador) flujo.Close() End Sub End Module Observa el código anterior. Su función es leer caracteres de la entrada estándar y después guardarlos en un archivo. Programación .NET (II). Manejo de archivos y carpetas 4.5 Lo primero que hacemos es importar el espacio de nombres System.IO. Esto es vital para poder usar la clase FileStream. Después, en el proceso principal del módulo, declaramos un flujo de tipo FileStream llamado “flujo” y un array de bytes para guardar los caracteres introducidos por el usuario (“texto”). También declaramos una variable que servirá para almacenar el número de caracteres leídos (“contador”). Leemos caracteres hasta que le usuario pulse “Enter” (código 13) o bien se supere la capacidad de 70 caracteres de nuestro array. Una vez que tenemos el texto a guardar en el archivo, pasamos a crear el flujo FileStream. Usamos modo Create para crear el archivo y el modo de acceso Write para escritura. Una vez creado el flujo, usamos su método Write para escribir en el fichero. Pasamos a este método como parámetros el array de bytes que queremos guardar, la posición inicial del array desde la que queremos escribir y el número de bytes (caracteres) a escribir (variable “contador”). Al iniciar la aplicación se abrirá una ventana de consola. En ella escribimos el texto que queremos escribir en el archivo, por ejemplo: Escribimos lo que va en el archivo... Si abrimos el archivo generado por la aplicación (“archivo.txt”), veremos que contiene el texto que habíamos escrito. 4.6 Programación .NET (II). Manejo de archivos y carpetas Ten en cuenta que el archivo se creará de nuevo cada vez que ejecutemos el programa, al crear su flujo con modo Create. Si quisiéramos crear el archivo sólo si no existe, sólo tendríamos que cambiar al modo OpenOrCreate. Con el modo OpenOrCreate, si el archivo existe, escribiremos inicialmente al principio del archivo sobre su contenido previo. Si escribimos en la consola “HOLA” y luego terminamos pulsando Enter, el texto en el archivo sería este: HOLAibimos lo que va en el archivo… Como ves, si no indicamos otra cosa, el método Write va a empezar a escribir en la primera posición del archivo. Antes hemos comentado que FileStream permite acceso aleatorio y secuencial al archivo, por lo que podremos indicar la posición en la que queremos empezar a escribir o leer (acceso aleatorio). Programación .NET (II). Manejo de archivos y carpetas 4.7 Para este modo de acceso FileStream ofrece las siguientes propiedades y método: La propiedad Position. Position devuelve la posición actual en bytes del puntero de lectura/escritura. Este puntero apunta a la posición del archivo en la que se realizará la próxima lectura o escritura. También podemos establecer un valor para Position, incluso más allá del final del archivo. La propiedad Length. Esta propiedad indica la longitud del archivo en bytes. Por ejemplo, el siguiente código comprueba si se ha llegado al fin del archivo vinculado al flujo “flujo”: If flujo.Length = flujo.Position Then Console.WriteLine("Fin del archivo") End If El método Seek. Seek nos permite establecer la posición en la que se empezará a leer o escribir en el archivo en función de un desplazamiento respecto a cierta posición. Su sintaxis es la siguiente: Public Function Seek(ByVal desp As Long, ByVal pos As SeekOrigin) As Long Este método moverá el puntero de lectura/escritura a una posición desplazada desp bytes de la posición pos del archivo. 4.8 Programación .NET (II). Manejo de archivos y carpetas El parámetro pos toma un valor del tipo enumerado SeekOrigin. Estos valores pueden ser Begin (principio del archivo), End (final) y Current (posición en la que está actualmente el puntero de lectura/escritura). Por ejemplo: flujo.Seek(-10, SeekOrigin.End) Con esta instrucción situamos el puntero 10 bytes antes del final del archivo. flujo.Seek(10, SeekOrigin.Current) Con esta instrucción situamos el puntero 10 bytes después de la posición actual. Clases BinaryWriter y BinaryReader Estas clases permiten leer y escribir, respectivamente, datos de tipos primitivos (Boolean, Byte, Single, Long, Integer, etc) en formato binario y cadenas de caracteres en formato UTF-8. Hay que tener en cuenta que BinaryReader sólo podrá leer datos que hayan sido escritos con BinaryWriter. Utilizaremos estas clases cuando sea necesario escribir o leer en archivos información en formato binario, no como texto. Programación .NET (II). Manejo de archivos y carpetas 4.9 Los flujos de estas clases se definen a partir de un flujo de la clase Stream o sus derivadas (FileStream), de la siguiente forma: Sub New(ByVal flujo As Stream) De esta forma, los flujos BinaryReader y BinaryWriter actúan como un filtro del flujo al que están asociados. La forma genérica de trabajar con estos filtros será la siguiente Creamos un flujo asociado a un archivo (FileStream). Asociamos un filtro (BinaryReader/BinaryWriter) al flujo. Leemos o escribimos en el archivo a través del filtro. A continuación te mostramos un ejemplo sencillo con BinaryReader y BinaryWriter. El ejemplo consiste en escribir datos de diversos tipos en un archivo con BinaryWriter y después los leerlos con BinaryReader. 4.10 Programación .NET (II). Manejo de archivos y carpetas 'Importamos el espacio de nombres System.IO Imports System.IO Module Module1 Public Sub Main() Dim fs As FileStream Dim bw As BinaryWriter 'Abrimos un flujo FileStream fs = New FileStream("c:\binario.txt", FileMode.Create, _ FileAccess.ReadWrite) 'Asociamos un BinaryWriter al flujo fs bw = New BinaryWriter(fs) 'Escribimos con Write datos de diversos tipos bw.Write("Texto guardado en binario") bw.Write(True) Dim variable As Double = - 564.6 bw.Write(variable) bw.Write("Fin") Dim br As BinaryReader 'Asociamos un BinaryReader al flujo fs para leer datos br = New BinaryReader(fs) 'Volvemos al comienzo del archivo fs.Position = 0 'Leemos según el tipo de dato, y mostramos por la consola Console.WriteLine(br.ReadString) Console.WriteLine(br.ReadByte) Console.WriteLine(br.ReadDouble) Console.WriteLine(br.ReadString) 'Ya podemos cerrar flujo y filtros bw.Close() br.Close() fs.Close() 'Esperamos a que el usuario pulse una tecla para terminar Console.ReadKey() End Sub End Module Programación .NET (II). Manejo de archivos y carpetas 4.11 En el código anterior cabe destacar que usamos el mismo flujo tanto para escribir como para leer los datos. Por ello, cuando terminamos de escribir y nos disponemos a leer los datos guardados para presentarlo por pantalla, hemos tenido que mover el cursor de lectura/escritura desde el final de archivo hasta el principio. Observa además que se hará diferenciación del tipo del dato a la hora de leer, usando el método más apropiado al tipo de dato (ReadString, ReadByte...). Cuando probamos la aplicación se muestra una ventan de consola con el siguiente contenido: Cada lectura del archivo realizada se muestra en una línea. Cada uno se ha leído de acuerdo a su tipo, como ya hemos comentado. Comparamos con la vista directa del contenido del archivo: Como ves, BinaryWrite no guarda los datos como texto plano. Los datos de tipos primitivos se guardan en binario, mientras que el texto se guarda en formato UTF-8. 4.12 Programación .NET (II). Manejo de archivos y carpetas StreamReader y StreamWriter Estas clases son la opción más indicada para procesar información de tipo texto, pues nos van a permitir respectivamente leer o escribir datos carácter a carácter. Estas clases heredan los métodos de las clases TextReader y TextWriter. StreamWriter ofrece dos constructores: Sub New(ByVal nombre As String[, ByVal añadir As Boolean, _ [ByVal codif As System.Text.Encoding]]) Este constructor crea un flujo de escritura vinculado al archivo nombre. Opcionalmente, se puede indicar si se desea añadir texto al que ya contiene el archivo y especificar cómo se codificará el texto en él (por defecto se asignará codificación UTF-8). Sub New(ByVal flujo As Stream[, ByVal codif As System.Text.Encoding]) Este constructor hace lo mismo, pero sobre otro flujo existente, como hacíamos con BinaryReader y BinaryWriter. Opcionalmente podremos especificar una codificación (parámetro codif). A su vez, StreamReader cuenta con los siguientes constructores: Sub New(ByVal flujo As Stream [, ByVal codif As System.Text.Encoding] _ [, ByVal detecta_codif As Boolean] _ [, ByVal buffer As Integer]) Programación .NET (II). Manejo de archivos y carpetas 4.13 Crea un StreamReader a partir de un flujo existente. Opcionalmente podemos detectar la codificación del archivo (detecta_codif), establecer la codificación (codif)y el tamaño mínimo del buffer de lectura utilizado (buffer). Sub New(ByVal ruta As String _ [, ByVal codif As System.Text.Encoding] _ [, ByVal detecta_codif As Boolean] _ [, ByVal buffer As Integer]) Crea un flujo StreamReader vinculado al archivo indicado en ruta. Opcionalmente podemos detectar la codificación del archivo (detecta_codif), establecer la codificación (codif) y el tamaño mínimo del buffer de lectura utilizado (buffer). A continuación puedes ver un ejemplo de código contenido en una aplicación de consola. Su función consiste en escribir unos valores en un archivo con StreamWriter y leerlos con StreamReader. 4.14 Programación .NET (II). Manejo de archivos y carpetas Imports System.IO Module Module1 Sub Main() Dim sw As StreamWriter 'creamos un flujo de escritura vinculado a archivo.txt sw = New StreamWriter("e:\texto.txt") 'escribimos en el archivo, con WriteLine una línea por dato sw.WriteLine("Un texto") sw.WriteLine(True) Dim variable As Double = -564.6 sw.WriteLine(variable) 'cerramos el flujo de escritura sw.Close() Dim sr As StreamReader 'abrimos ahora un flujo de lectura del archivo sr = New StreamReader("e:\texto.txt") 'leemos del archivo una línea cada vez con ReadLine Console.WriteLine(sr.ReadLine()) Console.WriteLine(sr.ReadLine()) Console.WriteLine(sr.ReadLine()) 'cerramos el flujo de lectura sr.Close() Console.ReadKey() End Sub End Module Observa que, a diferencia de BinaryReader, StreamReader nos ha permitido leer todos los valores con el mismo método, con independencia del tipo de dato a leer, puesto que lo tratará como texto. Programación .NET (II). Manejo de archivos y carpetas 4.15 Si probamos la aplicación obtendremos en la ventana de la consola una vista muy parecida a la del ejemplo anterior: Hemos leído todos los valores con el mismo método y se muestran correctamente, pues en el archivo se han almacenado todos como cadenas de texto. Lo comprobamos viendo directamente el contenido del archivo: Como puedes observar, los valores se han guardado en el archivo como caracteres de texto, a diferencia de la información guardada con BinaryWriter. 4.16 Programación .NET (II). Manejo de archivos y carpetas 4.3. MANIPULANDO ARCHIVOS Y CARPETAS Trabajando con archivos a menudo necesitaremos acceder al sistema de archivos de la máquina, para buscar un archivo concreto, conocer sus propiedades, o incluso examinar el contenido de cierto directorio. System.IO cuenta con tres clases que nos ofrecen métodos para la manipulación de archivos y directorios: File, Directory y Path. Con ellas, podremos crear, copiar, borrar, mover, alterar, determinar la existencia y conocer diversa información de archivos y carpetas. File Proporciona métodos para crear, copiar, eliminar, mover y abrir archivos. También se puede usar File para consultar o definir los atributos de un archivo. Algunos métodos de File Copy Copia un archivo existente en un archivo nuevo. Delete Elimina un archivo. Exists Determina si existe el archivo especificado. Move Mueve un archivo a una nueva localización. GetAttributes Obtiene los atributos de un archivo (un objeto de tipo FileAttributes). SetAttributes Establece los atributos FileAttributes en el archivo especificado. Programación .NET (II). Manejo de archivos y carpetas 4.17 Directory Directory proporciona métodos para crear, copiar, eliminar, mover y cambiar el nombre a directorios y subdirectorios. Algunos métodos de Directory CreateDirectory Crea todos los directorios y subdirectorios especificados. Delete Elimina un directorio y su contenido. Exists Determina si existe el directorio dado por la ruta especificada. Move Mueve un directorio y su contenido a una nueva localización. GetFiles Devuelve los nombres de todos los archivos que contiene el directorio especificado. GetParent Devuelve un objeto de tipo DirectoryInfo del directorio “padre” del directorio especificado. Existe otra clase, la clase DirectoryInfo, que, en esencia, ofrece la misma funcionalidad que Directory. La diferencia entre ambas clases radica en que Directory se usa sin necesidad de instanciar la clase, lo que no ocurre con DirectoryInfo. DirectoryInfo será más apropiada si queremos realizar múltiples acciones sobre el mismo directorio, pues no habrá necesidad de especificar la ruta del directorio cada vez que usemos un método, como ocurre con Directory, ya que se especifica al hacer la instancia. 4.18 Programación .NET (II). Manejo de archivos y carpetas Path La clase Path permite realizar diversas operaciones sobre una ruta especificada mediante una cadena de texto. Estas operaciones están destinadas a obtener cierta información de archivos y carpetas. Esta clase permite trabajar con sistemas de archivos de diversas plataformas. Algunos métodos de Path ChangeExtension GetDirectoryName Cambia la extensión de un archivo. Obtiene la ruta de un archivo. GetExtension Devuelve la extensión de la ruta a un archivo. GetFileName Devuelve el nombre y extensión de la ruta especificada. GetPathRoot Devuelve la raíz de la ruta indicada. Como ejemplo, vamos a modificar una de las aplicaciones realizadas anteriormente para que, aprovechando la funcionalidad de File, solicite al usuario la ruta del archivo sobre la que se trabajará, asegurándonos de su existencia. Programación .NET (II). Manejo de archivos y carpetas 4.19 'Importamos el espacio de nombres System.IO Imports System.IO Module EscribirFS Public Sub Main() Dim flujo As FileStream Dim texto(70) As Byte Dim car, contador As Integer 'solicitamos el archivo Do Console.Write("Ruta del archivo: ") archivo = Console.ReadLine Loop While Not File.Exists(archivo) 'leemos caracteres car = Console.Read While (car <> 13 And contador < 70) texto(contador) = Convert.ToByte(car) contador = contador + 1 car = Console.Read End While 'creamos el flujo y escribimos! flujo = New FileStream(archivo, FileMode.Create, _ FileAccess.Write) flujo.Write(texto, 0, contador) flujo.Close() End Sub End Module Como ves, simplemente se ha añadido un bucle do – loop while para solicitar el archivo hasta que sea un archivo existente en nuestro equipo. Después abrimos el flujo asociado a la ruta del archivo indicado. 4.20 Programación .NET (II). Manejo de archivos y carpetas