Guida al Visual Basic .NET
Capitolo 56° - Input e Output su file
Gli StreamLe operazioni di input e output, in .NET come in molti altri linguaggi, hanno come target uno stream, ossia un flusso di dati. In .NET, tale flusso viene rappresentato da una classe astratta, System.IO.Stream, che espone alcuni metodi per accedere e manipolare i dati ivi contenuti. Dato che si tratta di una classe astratta, non possiamo utilizzarla direttamente, poiché, appunto, rappresenta un concetto astratto non istanziabile. Come già spiegato nel capitolo relativo, classi del genere rappresentano un archetipo per diverse altre classi derivate. Infatti, un flusso di dati può essere tante cose, e provenire da molti posti diversi:
Globalmente parlando, quindi, si può associare uno stream al flusso di dati proveniente da un qualsiasi
dispositivo virtuale o fisico o da qualunque entità astratta all'interno della macchina: ad esempio è possibile avere uno stream associato
a una stampante, a uno scanner, allo schermo, ad un file, alla memoria temporanea, a qualsiasi altra cosa. Per ognuno di questi casi,
esisterà un'opportuna classe derivata di Stream studiata per adempiere a quello specifico compito. FileStreamQuesta classe offre funzionalità generiche per l'accesso a un file. Il suo costruttore più semplice accetta due parametri: il primo è il percorso del file a cui accedere ed il secondo indica le modalità di apertura. Quest'ultimo parametro è di tipo IO.FileMode, un enumeratore che contiene questi campi:
Un terzo parametro opzionale può specificare i permessi (solo lettura, solo scrittura o entrambe), ma per ora non lo useremo.
Seguendo questa logica, avremo la funzione Read: Read(buffer, index, length) che legge length bytes dallo stream aperto e li pone in buffer (a partire da index); e, parimenti, la funzione Write: Write(buffer, index, length) che scrive sullo stream length bytes prelevati dall'array buffer (a partire da index). Ecco un esempio: Module Module1 Sub Main() Dim File As IO.FileStream Dim FileName As String Console.WriteLine("Inserire il percorso di un file:") FileName = Console.ReadLine 'IO.File.Exists(path) restituisce True se il percorso 'path indica un file esistente e False in caso contrario If Not IO.File.Exists(FileName) Then Console.WriteLine("Questo file non esiste!") Console.ReadKey() Exit Sub End If Console.Clear() 'Apre il file specificato, posizionandosi all'inizio File = New IO.FileStream(FileName, IO.FileMode.Open) Dim Buffer() As Byte Dim Number, ReadBytes As Int32 'Chiede all'utente quanti bytes vuole leggere, e 'memorizza tale numero in Number Console.WriteLine("Quanti bytes leggere?") Number = CType(Console.ReadLine, Int32) 'Se Number è un numero positivo e non siamo ancora 'arrivati alla fine del file, allora legge quei bytes. 'La proprietà Position restituisce la posizione 'corrente all'interno del file (a iniziare da 0), mentre 'File.Length restituisce la lunghezza del file, in bytes. Do While (Number > 0) And (File.Position < File.Length - 1) 'Ridimensiona il buffer ReDim Buffer(Number - 1) 'Legge Number bytes e li mette in Buffer, a partire 'dall'inizio dell'array. Read è una funzione, e 'restituisce come risultato il numero di bytes 'effettivamente letti dallo stream. ReadBytes = File.Read(Buffer, 0, Number) Console.WriteLine("Bytes letti:") For I As Int32 = 0 To ReadBytes - 1 Console.Write("{0:000} ", Buffer(I)) Next Console.WriteLine() 'Se abbiamo letto tanti bytes quanti ne erano stati 'chiesti, allora non siamo ancora arrivati alla 'fine del file. Richiede all'utente un numero If ReadBytes = Number Then Console.WriteLine("Quanti bytes leggere?") Number = CType(Console.ReadLine, Int32) End If Loop 'Controlla se si è raggiunta la fine del file. 'Infatti, il ciclo potrebbe terminare anche se l'utente 'immettesse 0. If File.Position >= File.Length - 1 Then Console.WriteLine("Raggiunta fine del file!") End If 'Chiude il file File.Close() Console.ReadKey() End Sub End Module
Bisogna sempre ricordarsi di chiudere il flusso di dati quando si è finito di utilizzarlo. FileStream, e in generale anche Stream,
implementa l'interfaccia IDisposable e il metodo Close non è altro che un modo indiretto per richiamare Dispose (a cui, comunque,
possiamo fare ricorso). Allo stesso modo, possiamo usare la funzione Write per scrivere dati, oppure WriteByte per scrivere un byte alla
volta. Seek(offset, origin) offset è un intero che specifica la posizione a cui recarsi, mentre origin è un valore enumerato di tipo IO.SeekOrigin che può assumere tre valori: Begin (si riferisce all'inizio del file), Current (si riferisce alla posizione corrente) ed End (si riferisce alla fine del file). Ad esempio: 'Si sposta alla posizione 100 File.Seek(100, IO.SeekOrigin.Begin) 'Si sposta di 250 bytes indietro rispetto alla posizione corrente File.Seek(-250, IO.SeekOrigin.Current) 'Si sposta a 100 bytes dalla fine del file File.Seek(-100, IO.SeekOrigin.End) Certo che leggere e scrivere dati un byte alla volta non è molto comodo. Vediamo, allora, la prima categoria di file: i file testuali. Lettura/scrittura di file testualiI file testuali sono così denominati perchè contengono solo testo, ossia bytes codifcabili in una delle codifiche standard dei caratteri (ASCII, UTF-8, eccetera...). Alcuni particolari bytes vengono intepretati in modi diversi, come ad esempio la tabulazione, che viene rappresentata con uno spazio più lungo; altri vengono tralasciati nella visualizzazione e sembrano non esistere, ad esempio il NULL terminator, che rappresenta la fine di una stringa, oppure l'EOF (End Of File); altri ancora vengono presi a gruppi, come il carattere a capo, che in realtà è formato da una sequenza di due bytes (Carriage Return e Line Feed, rispettivamente 13 e 10). La differenza insita in questi tipi di file rispetto a quelli binari è il fatto di non poter leggere i singoli bytes perchè non ce n'è necessità: quello che importa è l'informazione che il testo porta al suo interno. La classe usata per la lettura è StreamReader, mentre quella per la scrittura StreamWriter: il costruttore di entrambi accetta un unico parametro, ossia il percorso del file in questione; esistono anche altri overloads dei costruttori, ma il più usato e quindi il più importante di tutti è quello appena citato. Ecco un piccolo esempio di come utilizzare tali classi in una semplice applicazione console: Module Module1 Sub Main() Dim File As String Dim Mode As Char Console.WriteLine("Premere R per leggere un file, W per scriverne uno.") 'Console.ReadKey restituisce un oggetto ConsoleKeyInfo, 'al cui interno ci sono tre proprietà: Key, 'enumeratore che definisce il codice del pulsante premuto; 'KeyChar, il carattere corrispondente a quel pulsante; 'Modifier, enumeratore che definisce i modificatori attivi, 'ossia Ctrl, Shift e Alt. 'Quello che serve ora è solo KeyChar Mode = Console.ReadKey.KeyChar 'Dato che potrebbe essere attivo il Bloc Num, ci si 'assicura che Mode contenga un carattere maiuscolo 'con la funzione statica ToUpper del tipo base Char Mode = Char.ToUpper(Mode) 'Pulisce lo schermo Console.Clear() Select Case Mode Case "R" Console.WriteLine("Inserire il percorso del file da leggere:") File = Console.ReadLine 'Cosntrolla che il file esista If Not IO.File.Exists(File) Then 'Se non esiste, visualizza un messggio ed esce Console.WriteLine("Il file specificato non esiste!") Console.ReadKey() Exit Sub End If Dim Reader As New IO.StreamReader(File) 'Legge ogni singola riga del file, fintanto che non 'si è raggiunta la fine Do While Not Reader.EndOfStream 'Come Console.Readline, la funzione d'istanza 'ReadLine restituisce una linea di testo 'dal file Console.WriteLine(Reader.ReadLine) Loop 'Quindi chiude il file Reader.Close() Case "W" Console.WriteLine("Inserire il percorso del file da creare:") File = Console.ReadLine Dim Writer As New IO.StreamWriter(File) Dim Line As String Console.WriteLine("Immettere il testo del file, " & _ "premere due volte invio per terminare") 'Fa immettere righe di testo fino a quando 'si termina Do Line = Console.ReadLine 'Come Console.WriteLine, la funzione d'istanza 'WriteLine scrive una linea di testo sul file Writer.WriteLine(Line) Loop While Line <> "" 'Chiude il file Writer.Close() Case Else Console.WriteLine("Comando non valido!") End Select Console.ReadKey() End Sub End Module
Ovviamente esistono anche i metodi Read e Write, che scrivono del testo senza mandare a capo: inoltre, Write e WriteLine hanno degli overloads
che accettano anche stringhe di formato come quelle viste nei capitoli precedenti. Lettura/scrittura di file binariCome già accennato nel paragrafo precedente, la distinzione tra file binari e testuali avviene tramite l'interpretazione dei singoli bytes. Con questo tipo di file, c'è una corrispondenza biunivoca tra i bytes del file e i dati letti dal programma: infatti, non a caso, l'I/O viene gestito attraverso un array di byte. BinaryWriter e BinaryReader espongono, oltre alle canoniche Read e Write già analizzate per FileStream, altre procedure di lettura e scrittura, che, di fatto, scendono a più basso livello. Ad esempio, all'inizio della guida ho illustrato alcuni tipi di dato basilari, riportando anche la loro grandezza (in bytes). Integer occupa 4 bytes, Int16 ne occupa 2, Single he occupa 4 e così via. Valori di tipo base vengono quindi salvati in memoria in notazione binaria, rispettando quella specifica dimensione. Ora, esistono modi ben definiti per convertire un numero in base 10 in una sequenza di bit facilmente manipolabile dall'elaboratore: mi riferisco, ad esempio, alla notazione in complemento a 2 per gli interi e al formato in virgola mobile per i reali. Potete documentarvi su queste modalità di rappresentazione dell'informazione altrove: in questo momento ci interessa sapere che i dati sono "pensati" dal calcolatore in maniera diversa da come li concepiamo noi. BinaryWriter e BinaryReader sono classi appositamente create per far da tramite tra ciò che capiamo noi e ciò che capisce il computer. Proprio perchè sono dei "mezzi", il loro costruttore deve specificare lo stream (già aperto) su cui lavorare. Ecco un esempio: Module Module1 Sub Main() 'Apre il file "prova.dat", creandolo o sovrascrivendolo Dim File As New IO.FileStream("prova.dat", IO.FileMode.Create) 'Writer è lo strumento che ci permette di scrivere 'sullo stream File con codifica binaria Dim Writer As New IO.BinaryWriter(File) Dim Number As Int32 Console.WriteLine("Inserisci 10 numeri da scrivere sul file:") For I As Int32 = 1 To 10 Console.Write("{0}: ", I) Number = CType(Console.ReadLine, Int32) Writer.Write(Number) Next Writer.Close() Console.ReadKey() End Sub End Module Io ho inserito questi numeri: -10 -5 0 1 20 8000 19001 -345 90 22. Provando ad aprire il file con un editor di testo vedrete solo caratteri strani, in quanto questo non è un file testuale. Aprendolo, invece, con un editor esadecimale, otterrete questo: f6 ff ff ff fb ff ff ff 00 00 00 00 01 00 00 00 14 00 00 00 40 lf 00 00 39 4a 00 00 a7 fe ff ff 5a 00 00 00 16 00 00 00
Ogni gruppetto di quattro bytes rappresenta un numero intero codificato in binario. Potremmo fare la stessa cosa con Single, Double, Date,
Boolean, String e altri tipi base per vedere cosa succede. Esempio: steganografia su immagini
La steganografia è l'arte di nascondere del testo all'interno di un'immagine. Per i più curiosi, mi avventurerò nella scrittura
di un semplicissimo programma di steganografia su immagini, nascondendo del testo al loro interno.
Public Class Form1 Private Sub btnHide_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnHide.Click If Not IO.File.Exists(txtPath.Text) Then MessageBox.Show("File inesistente!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) Exit Sub End If If IO.Path.GetExtension(txtPath.Text) <> ".jpg" Then MessageBox.Show("Il file deve essere in formato JPEG!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation) Exit Sub End If Dim File As New IO.FileStream(txtPath.Text, IO.FileMode.Open) 'Converte il testo digitato in una sequenza di bytes, 'secondo gli standard della codifica UTF8 Dim TextBytes() As Byte = _ System.Text.Encoding.UTF8.GetBytes(txtText.Text) 'Va alla fine del file File.Seek(0, IO.SeekOrigin.End) 'Scrive i bytes File.Write(TextBytes, 0, TextBytes.Length) File.Close() MessageBox.Show("Testo nascosto con successo!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Information) End Sub Private Sub btnRead_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRead.Click If Not IO.File.Exists(txtPath.Text) Then MessageBox.Show("File inesistente!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error) Exit Sub End If If IO.Path.GetExtension(txtPath.Text) <> ".jpg" Then MessageBox.Show("Il file deve essere in formato JPEG!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation) Exit Sub End If Dim File As New IO.FileStream(txtPath.Text, IO.FileMode.Open) Dim TextBytes() As Byte Dim B1, B2 As Byte 'Legge un byte B1 = File.ReadByte() Do 'Legge un altro byte B2 = File.ReadByte() 'Se i bytes formano la sequenza FF D9, si ferma. 'In Visual Basic, in numeri esadecimali si scrivono 'facendoli precedere da "&H" If B1 = &HFF And B2 = &HD9 Then Exit Do End If 'Passa il valore di B2 in B1 B1 = B2 Loop While (File.Position < File.Length - 1) ReDim TextBytes(File.Length - File.Position - 1) 'Legge ciò che rimane dopo FF D9 File.Read(TextBytes, 0, TextBytes.Length) File.Close() txtText.Text = System.Text.Encoding.UTF8.GetString(TextBytes) End Sub End Class Il testo accodato può essere rilevato facilmente con un Hex Editor, per questo lo si dovrebbe criptare con una password: per ulteriori informazioni sulla criptazione in .NET, vedere capitolo rekativo.
C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...
|