Nele Kosog: Schreiben und lesen von Dateien mit Unicode Zeichen auf Windows

Hallo!

Ich versuche vergeblich eine von mir angelegte Datei, deren Name Unicode Zeichen enthält, wieder zu lesen.

Mein System: Windows XP, Perl Camelbox 5.10.0.

Angelegt wird die Datei so:

  
use utf8;  
  
use strict;  
use warnings;  
use diagnostics;  
  
use Encode qw( encode decode );  
use Symbol qw( gensym );  
use Win32API::File qw(  
  CreateFileW  
  OsFHandleOpen  
  CREATE_ALWAYS  
  OPEN_EXISTING  
  GENERIC_WRITE  
  GENERIC_READ  
);  
  
my @lines     = ();  
my $file_name = "йбный.txt";          # Beispielhafter Dateiname zum Testen  
my $content   = "пйрнобый";           # Beispielhafter Inhalt zum Testen  
my $encoding  = "UTF-8";              # Encoding UTF-8  
  
### WRITE  
  
my $win32_file_handle = CreateFileW(  
    encode( 'UTF-16LE', $file_name ), # Encode file name  
    GENERIC_WRITE,                    # For writing  
    0,                                # Not shared  
    [],                               # Security attributes  
    CREATE_ALWAYS,                    # Create and replace  
    0,                                # Special flags  
    [],                               # Permission template  
)  
or die("CreateFile: $^E\n");  
  
OsFHandleOpen(  
    my $fh = gensym(),                # Create global reference  
    $win32_file_handle,               # File handle  
    'w',                              # Open for writing  
)  
or die("OsFHandleOpen: $^E\n");  
  
print $fh encode(                     # Write to file  
    $encoding,                        # Encoding is UTF-8  
    $content,                         # File content in UTF-8  
);  
  
[...]  

Bis hierhin funktioniert es.

Recherche:

  • Der Dateiname wird im Explorer korrekt dargestellt.
  • Notepad zeigt den Inhalt korrekt an.
  • Andere Editoren zeigen den Inhalt korrekt an, wenn sie explizit auf UTF-8 umgestellt werden.

Ich versuche die Datei danach wie folgt zu lesen:

  
[...]  
  
### READ  
  
my $win32_file_handle = CreateFileW(  
    encode( 'UTF-16LE', $file_name ), # Encode file name  
    GENERIC_READ,                     # For reading  
    0,                                # Not shared  
    [],                               # Security attributes  
    OPEN_EXISTING,                    # Open existing  
    0,                                # Special flags  
    [],                               # Permission template  
)  
or die("CreateFileW: $^E\n");  
  
OsFHandleOpen(  
    my $fh = gensym(),                # Create global reference  
    $win32_file_handle,               # File handle  
    'r'                               # Open for reading  
)  
or die("OsFHandleOpen: $^E\n");  
  
while (<$fh>) {                       # Read lines  
    push @lines,  
      decode( $encoding, $_ );        # Decode UTF-8  
}  
  

Das Programm bricht beim Lese-Aufruf von CreateFileW mit der Fehlermeldung "The system cannot find the file specified [...]".
Das lese ich als "System kann die angegebene Datei nicht finden" - aufgrund des Dateinamens? Ich bin mittlerweile betriebsblind - ich sehe den Fehler einfach nicht. Doppeltes Encoding?

Alle Tipps sind willkommen!
Vielen Dank für eure Hilfe,
Nele

  1. Es funktioniert dann doch etwas anders.

    Zum Beispiel so:

      
    use utf8;  
    binmode STDOUT, ":encoding(UTF-8)";  
    binmode STDIN,  ":encoding(UTF-8)";  
      
    use strict;  
    use warnings;  
    use diagnostics;  
      
    use Encode qw( encode decode );  
    use Symbol qw( gensym );  
    use Win32API::File qw(  
      CreateFileW  
      OsFHandleOpen  
      CREATE_ALWAYS  
      OPEN_EXISTING  
      GENERIC_WRITE  
      GENERIC_READ  
      );  
      
    ### Test Case  
    my $encoding  = 'UTF-8';               # Encoding UTF-8  
    my $file_name = 'пробный/пробный.txt'; # File name for testing purposes  
    my $content   = 'пйрнобый';            # File content for testing purposes  
      
    ### Write file  
    write_file( file_name => $file_name, encoding => $encoding, content => $content, );  
      
    ### Read file  
    my @lines = read_file( file_name => $file_name, encoding => $encoding, );  
      
    ### Output  
    foreach (@lines) { print $_; }  
      
      
    ###----------------------------------------------------------------------------  
    sub read_file {  
    ###----------------------------------------------------------------------------  
    ### USAGE: read_file(  
    ###                     file_name => <file name>,  
    ###                     encoding => <your encoding>,  
    ###                 );  
    ###----------------------------------------------------------------------------  
    	#shift;  
    	my %parameter = @_;  
    	my @lines     = ();  
    	if ( defined $parameter{file_name} && $parameter{file_name} ne '' ) {  
    		if ( defined $parameter{encoding} && $parameter{encoding} ne '' ) {  
      
    			my $win32_file_handle = CreateFileW(  
    				encode( 'UTF-16LE', $parameter{file_name} ),  
    				GENERIC_READ,        # For reading  
    				0,                   # Not shared  
    				[],                  # Security attributes  
    				OPEN_EXISTING,       # Open existing file  
    				0,                   # Special flags  
    				[],                  # Permission template  
    			  )  
    			  or die("CreateFileW: $^E\n");  
      
    			OsFHandleOpen( my $rfh = gensym(), $win32_file_handle, 'r' )  
    			  or die("OsFHandleOpen: $^E\n");  
      
    			while (<$rfh>) {  
    				push @lines, decode( $encoding, $_ );  
    			  }  
    		  }  
    	  }  
    	else {  
    		die("Cannot write file. No file name defined.");  
    	  }  
    	return @lines;  
    }  
      
    ###----------------------------------------------------------------------------  
    sub write_file {  
    ###----------------------------------------------------------------------------  
    ### USAGE: write_file(  
    ###                       file_name => <file name>,  
    ###                       encoding => <your encoding>,  
    ###                       content => <your content>,  
    ###                   );  
    ###----------------------------------------------------------------------------	  
    	#shift;  
    	my %parameter = @_;  
    	if ( defined $parameter{file_name} && $parameter{file_name} ne '' ) {  
    		if ( defined $parameter{encoding} && $parameter{encoding} ne '' ) {  
      
    			my $win32_file_handle = CreateFileW(  
    				encode( 'UTF-16LE', $parameter{file_name} ),  
    				GENERIC_WRITE,    # For writing  
    				0,                # Not shared  
    				[],               # Security attributes  
    				CREATE_ALWAYS,    # Create and replace  
    				0,                # Special flags  
    				[],               # Permission template  
    			  )  
    			  or die("CreateFileW: $^E\n");  
      
    			OsFHandleOpen( my $wfh = gensym(), $win32_file_handle, 'w' )  
    			  or die("OsFHandleOpen: $^E\n");  
      
    			print $wfh encode( $parameter{encoding}, $parameter{content} );  
    		  }  
    	  }  
    	else {  
    		die("Cannot write file. No file name defined.");  
    	  }  
    }  
      
    
    

    Einschränkungen:

    • Erstmal Angabe des Devices (z.B. C:) nicht möglich.
    • Löschen der Datei nicht getestet.

    Verbesserungsvorschläge sind willkommen!
    Viele Grüße,
    Nele

    1. sub read_file {

      ...

      my %parameter = @_;

      ...

        	while (<$rfh>) {  
        		push @lines, decode( $encoding, $\_ );  
        	  }  
      

      Da ist ein Problem mit deiner Routine.
      Du hast Sie so verfasst, als ob sie auf jedes Encoding anwendbar sein sollte, also auch auf Files, die eine BOM beinhalten.
      Aus diesem Grund solltest du hier Files im Slurpmode einlesen
           { local $/=undef;
             my $file = decode($encoding, <$rfh>);
           }

      Verbesserungsvorschläge sind willkommen!

      Wüsste nicht wirklich viel zu sagen...

      mfg Beat

      --
      ><o(((°>           ><o(((°>
         <°)))o><                     ><o(((°>o
      Der Valigator leibt diese Fische
      1. Hallo Beat!

        Da ist ein Problem mit deiner Routine.
        Du hast Sie so verfasst, als ob sie auf jedes Encoding anwendbar sein sollte, also auch auf Files, die eine BOM beinhalten.
        Aus diesem Grund solltest du hier Files im Slurpmode einlesen
             { local $/=undef;
               my $file = decode($encoding, <$rfh>);
             }

        Mal davon abgesehen, dass deine Variante um ein Vielfaches schneller ist als meine zeilenorientierte - warum soll ich die Datei slurpen? Ach, weil ich sonst das BOM verliere und eine einzelne Zeile gar nicht interpretieren kann, richtig? Kannst du es kurz erklären?

        Und bevor ich es vergesse: Die Funktion CreateFileW kann natürlich mit Devices umgehen. Ich habe den Pfad nicht korrekt angegeben: Mit 'C:\dir\file_name.txt' klappt es!

        Danke & viele Grüße,
        Nele

        1. Mal davon abgesehen, dass deine Variante um ein Vielfaches schneller ist als meine zeilenorientierte - warum soll ich die Datei slurpen? Ach, weil ich sonst das BOM verliere und eine einzelne Zeile gar nicht interpretieren kann, richtig? Kannst du es kurz erklären?

          Du hast die Erklärung ja schon fast.
          Wenn du von UTF-16 decodierst, dann wird eine BOM erwartet.
          Jedoch die BOM existiert nur in der ersten Zeile. Als was werden die ersten Bytes jeder nächsten Zeile interpretiert? Ich habe es nicht getestet.

          mfg Beat

          --
          ><o(((°>           ><o(((°>
             <°)))o><                     ><o(((°>o
          Der Valigator leibt diese Fische
          1. Wenn du von UTF-16 decodierst, dann wird eine BOM erwartet.
            Jedoch die BOM existiert nur in der ersten Zeile.

            Stimmt - du hast recht. :-)

            Danke & viele Grüße,
            Nele

    2. Und hier noch ein Beispiel für das Löschen einer Datei:

        
      ###----------------------------------------------------------------------------  
      sub delete_file {  
      ###----------------------------------------------------------------------------  
      ### USAGE: delete_file (  
      ###                        file_name => <file name>,  
      ###                    );  
      ###----------------------------------------------------------------------------  
      	#shift;  
      	my %parameter = @_;  
      	if ( defined $parameter{file_name} && $parameter{file_name} ne '' ) {  
      		DeleteFileW( encode( 'UTF-16LE', $parameter{file_name} ) )  
      		  or die("CreateFileW: $^E\n");  
      	  }  
      	else {  
      		die("Cannot delete file. No file name defined.");  
      	  }  
      }
      

      Viele Grüße,
      Nele