Home > Artikel > Ausgabe 3/2015 > Kodierte Texte mit VBA umwandeln

Kodierte Texte mit VBA umwandeln

  PDF ansehen

  Download PDF und Beispieldatenbank

Auch wenn die Assistenten von Access zum Import und Export von Textdateien über ihre Einstellungsseiten genügend Möglichkeiten bieten, um mit allen erdenklichen Formaten zurechtzukommen, gibt es häufig Situationen, in denen die Verarbeitung über VBA-Programmierung benötigt wird. Hier geht es in erster Linie um den Umgang mit Texten in unterschiedlicher Sprachkodierung.

Beispieldatenbank

Die Beispiele dieses Artikels finden Sie in der Datenbank 1503_Textumwandlung.mdb.

Sprachen, Codepages, CharacterSets

Beleuchten wird zunächst, was es mit den Sprachcharakteristiken von Textdateien auf sich hat. Wenn Sie sich eingehender mit Textformaten beschäftigen – Stichwort Internationalisierung –, so begegnen Sie immer wieder Begriffen, wie Language Id, Locale Identifier (LCID), Codepage, Unicode und Character Set. Jeder hat seine eigene Bedeutung und doch hängen alle irgendwie miteinander zusammen. Grundsätzlich geht es dabei nicht nur um Textdateien, sondern generell um den Umgang mit Texten im System. Betroffen sind damit etwa die Tastatur, die Textausgabe auf dem Bildschirm, Schriftarten, oder auch die Interaktion mit dem Internet.

An oberster Stelle steht die Sprache eines Textes, die wiederum einer Gruppe von Sprachen angehören kann. Deutsch etwa gehört zur Gruppe Westeuropa und USA. Windows unterstützt von Haus aus mehrere Sprachgruppen und Sprachen. Dabei gibt es einen Unterschied zwischen nur unterstützten und den installierten Sprachen. Während es genau 17 Gruppen gibt, kann die Zahl der zugehörigen Sprachen selbst variieren. Für den Umgang mit Texten ist die Gruppierung weniger interessant, denn sie betrifft vor allem die Tastaturbelegung, die in der Regel pro Sprachgruppe eindeutig ist.

Welche Sprachen Windows unterstützt können Sie mit einem Doppelklick auf die Tabelle tblLCIDs der Beispieldatenbank herausfinden (siehe Bild 1). In der zweiten Spalte finden Sie die Sprache, in der dritten das zugehörige Land. Auf die Bedeutung der vierten (CP) kommen wir noch zu sprechen. Das Feld LCID legt die eindeutige Id der Sprache fest. Diese Zahl werden Sie häufiger unter Windows finden. Schauen Sie etwa in den Order von Microsoft Office im Explorer zu Ihrer Office-Version. Dort werden Sie als Unterverzeichnisse die von Ihrem Office unterstützten Sprachen als LCID finden. Die 1033 für Englisch (USA) ist immer mit dabei, die 1031 für Deutsch wahrscheinlich auch. In diesen Verzeichnissen befinden sprachspezifische Ressourcen für Office. Installieren Sie etwa zusätzlich die Rechtschreibprüfung für Französisch, so entsteht hier ein weiterer Ordner mit dem Namen 1036.

Inhalt der Tabelle tblLCIDs mit den von Windows unterstützten Sprachen

Bild 1: Inhalt der Tabelle tblLCIDs mit den von Windows unterstützten Sprachen

Ist die Sprache mit ihrer LCID noch eindeutig, so ist es das Land nicht. Filtern Sie die Tabelle nach dem Land Deutschland, so ergeben sich vier Datensätze, unter denen sich etwa auch das sehr spezielle Niedersorbisch befindet.

Der Inhalt dieser Tabelle ist keineswegs in Stein gemeißelt. Je nach Windows-Version, -Installationsoptionen und -Sprache kann sie andere Datensätze aufweisen. Sie können die Tabelle auf Ihrem System selbst erstellen. Dazu gibt es im VBA-Modul mdlTextconverter der Beispieldatenbank eine Prozedur EnumLanguageGroupsLocales. die Sie aus dem Direktfenster des VBA-Editors heraus aufrufen können. Sie löscht zunächst den Inhalt der Tabelle und erstellt ihn dann über verschiedene API-Funktionen neu. Es gibt allerdings keine API-Funktion, die die Sprachen direkt aufzählen könnte. Erst müssen die einzelnen Sprachgruppen ermittelt (EnumLanguageGroups), in einer Collection zwischengespeichert und anschließend der Sprach-Enumeration übergeben werden. In Listing 1 sehen Sie den Code der Prozedur, der hier nur exemplarisch angeführt wird.

Sub EnumLanguageGroupsLocales()

     Dim vID As Variant

     

     Set colInfo = New VBA.Collection

     CurrentDb.Execute "DELETE FROM tblLCIDs"

     CurrentDb.Execute "INSERT INTO tblLCIDs(LCID,Language,Country,CP) " & _

         "VALUES (0,'(Unbekannt)','(Unbekanntes Land)',0)"

     

     EnumLanguageGroups

     For Each vID In colInfo

         EnumLanguageGroupLocales AddrOf(AddressOf LanguageGroupLocalesProc), _

             ByVal CLng(Val("&H" & vID)), ByVal 0&, 0&

     Next vID

End Sub

Listing 1: Prozedur zum Füllen der Tabelle tblLCIDs mit Sprachdatensätzen

Denn im Rahmen dieses Beitrags würde es eindeutig zu weit führen, die nicht unkomplizierten und mit API-Aufrufen gespickten Funktionen näher zu erläutern. Und ohne API-Funktionen kommt man allein mit den Bordmitteln der VBA-, Access- und Office-Bibliotheken bei diesem Thema nicht weiter.

Kommen wir zu der vierten Spalte der Tabelle mit dem Namen CP, was ein Kürzel für Codepage darstellt. Dort ist zu jeder Sprache die Vorgabe-Codepage zu finden. Was aber ist eine Codepage? Dazu ein Ausflug in die binäre Speicherung von Texten: Für jedes Zeichen wird bei Single-Byte-Formaten (ANSI, OEM) genau ein Byte festgelegt. Schreiben Sie ein A in einen Text, so finden Sie unter einem Hex-Editor an dieser Stelle die Zahl 65. Genauso gut können Sie auch VBA bemühen:

  Asc ("A") -> 65

Chr (65) -> "A"

Die 65 kennzeichnet, welches Zeichen innerhalb der deutschen Sprach-Codepage zu finden ist. Eine Codepage ist damit imgrunde eine Tabelle mit 255 Datensätzen, die jeder Position ein bestimmtes Zeichen zuordnet. Und eben diese Position ist, je nach Sprache, unterschiedlich. So ermitteln Sie etwa für ein Ü den Code 220. In Russland aber kennt man kein Ü. Die dort verwendete Codepage ordnet der 220 ein anderes, ein kyrillisches Zeichen zu. Ein Text in deutscher Sprache mit Umlauten wird dort falsch dargestellt, wenn er mit dem russischen Notepad geöffnet wird. Ein russisches Word hingegen wird beim Öffnen der Textdatei nachfragen, welche Codepage zu verwenden ist, und erst nach Einstellung von Westeuropa (Windows) das korrekte Ergebnis zeitigen.

Die Codepage ist also die zentrale Schaltstelle, wenn es um Textverarbeitung geht. Erst sie ermöglicht es, einem Byte-Code ein bestimmtes Zeichen zuzuordnen.

Nun braucht es nicht für jede einzelne Sprache eine gesondert Codepage. Die ANSI-Codepage für Westeuropa (1252) etwa kennt fast alle benötigten Zeichen für die Sprachen Europas, also die deutschen Umlaute und die französischen oder spanischen Akzente.

Es ist demnach ökonomischer, mehreren Sprachen eine Codepage zuzuweisen. So steht in der CP-Spalte der Sprachtabelle für Deutsch, Italienisch, Französisch, Spanisch, Englisch überall die 1252. Filtern Sie die Tabelle nach diesem Wert: Erstaunlicherweise gibt es über Hundert Sprachen, die sich diese Codepage teilen.

Nun kann man sich eine Zahl als Bezeichner für eine Codepage nur schwer merken. Deshalb haben Codepages auch Namen. Öffnen Sie die Tabelle tblCodepages der Beispieldatenbank (siehe Bild 2). Sie finden dort unter dem Feld Description die Bezeichnungen der Codepages. Die 1251 nennt sich exakt ANSI - Lateinisch I. Das ist gut zu wissen, denn eine Bezeichnung mit dem Teilinhalt Westeuropa werden Sie nämlich vergeblich suchen.

Inhalt der Tabelle tblCodepages

Bild 2: Inhalt der Tabelle tblCodepages

In der Tabelle sind allerlei kryptische Codepages aufgeführt, die zum großen Teil veraltet und nicht mehr in Gebrauch sind.

Sie werden sie unter Umständen dennoch benötigt, wenn Sie es mit Textdateien zu tun haben, die aus IT-Urzeiten stammen, wie die IBM-Codes. Eher noch treffen Sie auf Texte von MS-DOS-Systemen, die in der Tabelle durch das Präfix OEM gekennzeichnet sind.

Es gibt überdies ein Problem mit diesen Bezeichnungen, die leider nicht so eindeutig sind. Die Bezeichnungen in der Tabelle sind das Resultat einer Windows-API-Funktion, die in der Prozedur GetCPName der Beispieldatenbank zum Einsatz kommt:

  GetCPName (1252)

- > "(ANSI - Lateinisch I)"

Mithilfe dieser Prozedur und derjenigen zum Auflisten der Codepages (EnumCodepages), aufgerufen aus dem VBA-Direktfenster, können Sie den Inhalt der Tabelle neu erstellen. Wie wir noch sehen werden, gibt es aber auch noch die Möglichkeit, sich über DAO und die Access Database Engine alle Codepages ausgeben zu lassen. Dieses Ergebnis gibt die Tabelle tblCodepagesDAO wieder. Und dort steht für die Codepage 1252 die Bezeichnung Westeuropäisch (Windows). Damit Sie alle Sprachen, die zugehörigen Default-Codepages und deren Bezeichnungen im Blick haben, fasst die Abfrage qry_LCID_CP die drei besprochenen Tabellen zusammen und gibt sie wie in Bild 3 wieder. Sie sollte genügen, um aus einer Bezeichnung die zugehörige Codepage zu identifizieren. Denn diese Bezeichnungen listen etwa die Kombinationsfelder von Access oder Word auf, wenn es um die Auswahl einer Codepage geht.

Alle Sprachen und deren Codepages gibt die Abfrage qry_LCID_CP aus

Bild 3: Alle Sprachen und deren Codepages gibt die Abfrage qry_LCID_CP aus

Zuletzt fehlt noch die Erläuterung des Begriffs Character Set, zu Deutsch Zeichensatz. Die Zuordnung der Bytes eines Textes zu bestimmten Zeichen über die Codepage ist das Eine, die visuelle Darstellung dieses Zeichens über Schriften das Andere. Eine Schriftart kann mit der Codepage allein noch nichts anfangen. Der Satz von darstellbaren Zeichen ist durch den Zeichensatz definiert. Die wiederum ist ebenfalls über eine bestimmte Nummer gekennzeichnet. Die Funktion CharSetFromCP der Beispieldatenbank nimmt als Parameter eine Codepage entgegen und gibt die ID des zugehörigen Zeichensatzes zurück:

  CharSetFromCP(1252)

  -> 0 (Deutsch, System-Zeichensatz)

  CharSetFromCP(1251)

-> 204 (Russisch, Kyrillisch)

Das Ergebnis 0 bedeutet, dass es sich um den Vorgabezeichensatz des installierten Systems handelt. Um kyrillische Zeichen darzustellen, benötigen Sie das CharacterSet 204.

Die Standardschriftarten von Windows, wie Arial, Times New Roman, Courier New, et cetera, enthalten alle diese Zeichen. Die Nummer des Zeichensatzes identifiziert dann, welcher Block von Zeichen zu verwenden ist.

Unicode

Bisher ging es nur im Texte im Single-Byte-Format, wobei ANSI der Standard ist. Die Beschränkung auf 256 mögliche Zeichen durch ein Byte führt hier zum beschriebenen System mit den LCIDs und Codepages, welches einige Kopfschmerzen bereiten kann. Um einiges angenehmer ist da das Unicode-Format, das jedem Zeichen mehrere Bytes spendieren kann.

Bei UTF-16 sind das pro Zeichen in der Regel zwei Bytes. Dadurch steigt die Zahl zuordenbarer Zeichen auf 2^16, was für alle Zeichen dieser Welt ausreicht. Um Codepages müsste man sich im Prinzip keine Gedanken mehr machen, wenn alle Texte in diesem Format gespeichert wären. Allerdings bedeutet diese Erweiterung noch nicht, dass damit auch die Sprache des Textes bekannt ist und welcher Zeichensatz zur Darstellung kommen soll. Insofern behält das System um LCIDs, Codepages und Charactersets weiterhin seine Bedeutung.

Bei UTF-8 wird es komplizierter, weil ein Zeichen meist ein Byte belegt, aber bei spezielleren Zeichen bis zu vier. Der Beginn eines Mehr-Byte-Zeichens wird durch bestimmte Flag-Bytes eingeleitet.

Dasselbe gilt für das wenig verbreitete UTF-7, welches eine noch komplexere Kodierung vorsieht, denn ein Zeichen wird hier durch nur 7 Bits eines Bytes identifiziert. Infolgedessen sind speziellere Zeichen durch eine ganze Kette von Byte-Anweisungen repräsentiert.

Eigentlich ist das UTF-8-Format das optimalste, weil es alle Zeichen darstellen kann, aber wenig Platz benötigt. Weil es jedoch durch variable Byte-Anzahl pro Zeichen etwas schwieriger zu handhaben ist, verwendet VBA ausschließlich das UTF-16-Format. Tatsächlich sind alle Strings unter VBA solche Unicode-Strings. Das lässt sich leicht nachweisen. Während die Funktion Len die Zahl der Zeichen eines Strings zurückgibt, ermittelt LenB die Anzahl der Bytes, die dafür benötigt werden. Sie gibt grundsätzlich das Doppelte aus:

  Len ("ACCESS") -> 6

LenB ("ACCESS") ->12

Intern steht dieser Text mit diesen Bytes im Speicher:

65 - 0 - 67 - 67 - 0 - 69 - 0 - 83 -0 - 83 - 0

Das macht auch schon deutlich, dass UTF-16 zur Speichervergeudung neigt. Der häufigste Wert in diesen Strings ist 0. Da Strings in Textfeldern seit Access 2000 ebenfalls grundsätzlich in UTF-16 gespeichert sind, sieht Access die Möglichkeit vor, für diese Felder als Eigenschaft die Unicode-Kompression einzuschalten, um Speicherplatz zu sparen. Die Kompression elimiert die ganzen Nullen und führt intern zu einem Format, das UTF-8 ähnelt.

Es gibt also einige Varianten von Unicode, und bei Unicode-Textdateien kommt noch ein weiterer Stolperstein hinzu. Es gibt zum einen solche Dateien, die die Bytes vertauschen. Der String oben sieht dann so aus:

0 - 65 - 0 - 67 - 67 - 0 - 69 - 0 - 83 -0 - 83

Hier handelt es sich um die LE-Variante (Low Endian), während VBA die BE-Variante (Big Endian) nutzt. Zum anderen kann eine Unicode-Datei noch sogenannte Lead-Bytes ganz zu Anfang beherbergen, die sie eindeutig als im Unicode-Format kodiert kennzeichnen soll. Das ist im Allgemeinen die Byte-Folge 255, 254 oder 254, 255, je nachdem, ob es sich um Big Endian oder Low Endian handelt. Bei UTF-8 stehen hier sogar 3 Lead-Bytes.

All dies macht klar, dass auch Textdateien im Unicode-Format Probleme bescheren können. Damit VBA mit Strings korrekt umgehen kann, müssen diese immer in UTF-16 vorliegen. Auch alle Steuerelemente, etwa die Textbox, erwarten dieses Format, sonst kommt es zu seltsamen Zeichenfolgen.

Texte konvertieren

Die wichtigste VBA-Funktion zur Umwandlung von ANSI in Unicode und zurück ist StrConv:

  StrConv ("ACCESS", vbUnicode)

-> "A C C E S S"

  StrConv ("ACCESS", vbFromUnicode)

-> "???"

Da der übergebene String bereits im Unicode-Format vorliegt, verdoppelt VBA nicht nur das Zeichen-Byte, sondern auch die interne Null, was zu drei Nullen führt, die als Leerzeichen ausgegeben werden. Umgekehrt bildet sich bei der Konvertierung aus Unicode nach ANSI zwar eine gültige Byte-Folge, die aber von VBA nicht angezeigt werden kann, was durch die Fragezeichen deutlich wird. Kombinieren Sie beide Funktionen, so erhalten Sie ein sinnvolles Ergebnis und den Nachweis, dass bei der Konvertierung nach ANSI nichts verloren ging:

  StrConv(StrConv("ACCESS", _ vbFromUnicode),vbUnicode)

- > "ACCESS"

Eine Besonderheit von StrConv ist, dass es nicht nur Strings als Parameter entgegennimmt, sondern auch Byte-Arrays. Oder aus Byte-Arrays wieder Strings erzeugen kann:

Dim bin() As Byte

bin = StrConv("ACCESS", vbFromUnicode)

  UBound(bin) '-> 5 = 6 Bytes

  StrConv(bin, vbUnicode)

-> "ACCESS"

Über Byte-Arrays können Strings so am einfachsten manipuliert werden.

Ein weiterer Parameter von StrConv, der optional angegeben werden kann, ist LCID. Damit kann eine Sprache festgelegt werden, in die der String konvertiert werden soll und umgekehrt. Das macht es möglich einen String von einer Sprach-Id in die andere umzuwandeln. Die Funktion ConvertUnicodeToUnicode (Listing 2) der Beispieldatenbank macht sich das zunutze. Hier wird aus dem übergebenen Text zunächst ein Byte-Array erzeugt. Dabei wird der Parameter LCID weggelassen, was bedeutet, dass StrConv die Sprach-Id des Systems verwendet und damit einen westeuropäischen Text. Beim Zurückverwandeln in einen String als Rückgabe der Funktion jedoch wird die LCID eingesetzt, die als optionaler Parameter übergeben wurde. Rufen Sie die Funktion etwa so auf:

Public Function ConvertUnicodeToUnicode(ByVal sText As String, _

     Optional lLCID As Long) As String

     Dim bin() As Byte

     

     On Error GoTo Fehler

     If lLCID = 0 Then lLCID = GetSystemDefaultLangID

     bin = StrConv(sText, vbFromUnicode)

     ConvertUnicodeToUnicode = StrConv(bin, vbUnicode, lLCID)

     Exit Function

     

Fehler:

     MsgBox "Ungültige Sprach-ID (LCID)", vbCritical

End Function

Listing 2: Funktion zum Konvertieren eines Textes mithilfe einer Sprach-ID (LCID)

  ConvertUnicodeToUnicode( _

"André",1031)

Das übersetzt André nach Englisch. Heraus kommt genau dasselbe, da Englisch die gleiche Codepage verwendet, wie Deutsch (1252). Übergeben Sie stattdessen 1042, was für Koreanisch steht, so gibt die Funktion dies aus:

  ConvertUnicodeToUnicode( _

"André",1031)

- > Andr?

Offenbar hat sich der String also geändert. Verwechseln Sie den LCID-Parameter von StrConv nicht mit dem Wert für die Codepage! Diese wird aus der LCID intern von der Funktion selbst berechnet. Das in Bild 3 gezeigte Abfrageergebnis gibt darüber Aufschluss.

Die Anwendung Textconverter

Damit Sie sich nicht mit zu vielen VBA-Prozeduren und "API-Geraffel" beschäftigen müssen, ist in die Beispieldatenbank das Formular frmTextconverter eingebaut, das Ihnen manuelle Programmierarbeit abnehmen kann. Beim Start des Formulars werden Sie gefragt, ob die Beispieltexte zum Beitrag importiert werden sollen. Sie müssten sich im Unterverzeichnis Textdateibeispiele befinden. Es handelt sich um einige Dateien, die in den unterschiedlichsten Formaten abgespeichert wurden, um die Funktionsfähigkeit des Formulars zu testen. Bejahen Sie die Nachfrage, so liest eine Prozedur alle Dateipfade in die dem Formular zugrundeliegende Tabelle ein. Mehr geschieht noch nicht. Ein Klick auf den Button Einlesen nimmt sich der angezeigten Datei an und liest ihren Inhalt ein, um ihn im Textfeld unten auf den Bildschirm zu bringen (Bild 4). Über den Pünktchen-Button können Sie aber ebenso eine andere Textdatei Ihrer Wahl laden.

Das Formular frmTextconverter der Beispieldatenbank nach dem Einlesen einer Textdatei

Bild 4: Das Formular frmTextconverter der Beispieldatenbank nach dem Einlesen einer Textdatei

Bei Textfeld handelt es sich übrigens nicht um eine Access-Textbox, sondern um ein MS-Forms-Textfeld, also ein ActiveX-Steuerelement. MS-Forms ist Bestandteil jeder Office-Installation und damit garantiert verfügbar. Die MS-Forms-Textbox hat gegenüber dem Pendant von Access einige Vorteile.

Sie lässt sich ebenso an ein Datenfeld des Formulars binden, aber der Text lässt sich besser scrollen, und vor allem können für die angegebene Schrift nicht nur die üblichen Verdächtigen, wie Font.Name und Font.Size angegeben werden, sondern auch Font.Characterset, was zu unserem Thema besonders passt.

In der Abbildung wird der Text nach dem Einlesen korrekt dargestellt. Kein Wunder, da es sich um einen ANSI-Text mit der verwendeten Codepage 1252 handelt. Um das zu verifizieren, können Sie nun auf die Schaltfläche Analysieren klicken. Dabei wird die Routine DetectCPFileDAO2 des Moduls mdlTextconverter angesprochen, die über einen später zu erläuternden Code die Codepage des Textes zu ermitteln versucht – die Access Database Engine hält dazu eine verborgene Anweisung bereit. Nach dieser Analyse wird die ermittelte Codepage namentlich im Kombinationsfeld rechts angezeigt. Die Datenherkunft dieser Combobox ist über einen SQL-String auf die Tabelle tblCodepages eingestellt. Zusätzlich zeigt ein Meldungsfenster an, wie lange die Routine für ihre Analyse benötigt hat. Grund: Es gibt im Modul noch zwei weitere gleichartige Routinen, die für diese Analyse herangezogen werden können. Die eine verwendet eine temporäre Tabelle über DAO (DetectCPFileDAO), die andere ADO (DetectCPFileADO) für den gleichen Zweck. Die voreingestellte Routine hingegen arbeitet mit einer temporären Abfrage. Sie können nun in der Click-Ereignisprozedur des Analysieren-Buttons abwechselnd einer der drei Routinen setzen und schauen, wie es sich mit der Performance verhält. Es zeigt sich, dass die Abfragen-Routine ...DAO2 eindeutig die schnellste ist. Zwar geht es hier nur um Sekundenbruchteile, doch immerhin dauert die Berechnung etwa eine Sekunde, was heutzutage nicht gar so wenig ist. Die Analyse-Methoden der Access Database Engine müssen hier ja aufwändigere statistische Algorithmen ausführen.

Nach dem Analysieren der Codepage, die in unserem dargestellten Fall korrekt die 1252 ermittelte, wird der Button in Unicode aktiv. Ein Klick auf ihn konvertiert den Originaltext mithilfe der Codepage in das Unicode-Format und bringt diesen im Textfeld zur Ansicht. Wenn die Darstellung nun immer noch korrekt ist, können Sie auf den Speichern-Button gehen, wodurch der Unicode-Text in der Tabelle tblText als konvertierter Text gespeichert wird (Bild 5). Wenn Sie später wieder auf diesen Datensatz gehen, wird automatisch dieser konvertierte Text im Formular angezeigt. Sie können ihn aber auch wieder über den Button mit dem Löschen-Symbol entfernen. Der Originaltext bleibt auf jeden Fall erhalten, wie auch die ermittelte Codepage. Gehen wir an dieser Stelle auf den Aufbau der Tabelle tblText ein, deren Entwurf in Bild 6 zu sehen ist.

Das Formular frmTextconverter der Beispieldatenbank nach dem Konvertieren in Unicode per ermittelter Codepage

Bild 5: Das Formular frmTextconverter der Beispieldatenbank nach dem Konvertieren in Unicode per ermittelter Codepage

Entwurfsansicht der Formulartabelle tblText

Bild 6: Entwurfsansicht der Formulartabelle tblText

File ist das Feld, welches den Pfad zur Textdatei aufnimmt. TextOriginal wird beim Einlesen der Datei gefüllt und erhält damit deren Inhalt. Es macht Sie stutzig, dass hier der Datentyp OLE-Objekt gewählt wurde? Wie bereits ausgeführt, speichert Access Texte immer im UTF-16-Format. Wir möchten jedoch die Textdatei binär Byte für Byte wie im Original abspeichern, und dazu eignet sich als Binär-Container ein OLE-Feld am besten. In der Datenblattansicht erscheinen die Texte dann lediglich mit dem Vermerk Long Binary-Daten. (Der Begriff OLE-Objekt ist veraltet und verweist auf Zeiten, in denen dieser Feldtyp für die tatsächliche Speicherung von OLE-Objekten, also Automationsobjekten, vorgesehen war. Tatsächlich handelt es sich dabei nur um einen Container für Byte-Arrays, weshalb der Begriff LongBinary eigentlich angemessener wäre, obwohl Access durchaus zwischen Binärdaten und OLE-Daten in diesem Feld unterscheidet.)

TextConverted wird beim Konvertieren des Textes nach Unicode im Formular gefüllt. Hier wird ein Memo-Feld verwendet, das dann auch in der Datenblattansicht der Tabelle den lesbaren Text anzeigt. Und Codepage enthält nach Auslösen des Analysieren-Buttons oder der Auswahl über das Kombinationsfeld die ermittelte oder gesetzte Codepage als Wert.

Über dieses Formular der Anwendung können Sie bequem alle Textdateien testen und in für Access akzeptables Unicode bringen. Die Analysefunktion ist dabei nur eine Hilfestellung. Nicht selten schlägt sie fehl. Lesen Sie etwa eine der UTF-8-Textdateien des Beispielfundus ein. Access findet dabei seltsamerweise in der Regel, dass es sich um normales ANSI handelt. In diesem Fall sind Sie auf Experimente angewiesen und wählen aus dem Kombinationsfeld vermutete Codepages aus, wandeln in Unicode, und begutachten die Darstellung im Textfeld solange, bis sie stimmt.

Sollte der Import einer Textdatei in eine Tabelle über den Import-Assistenten von Access fehlschlagen, so leistet Ihnen dieses Formular ebenfalls gute Dienste. Lesen Sie etwa die Beispieldatei Adressen_DOS.txt ein und analysieren Sie sie. Access kommt abermals zum Ergebnis, dass es sich um 1252er-ANSI handelt, was jedoch nicht stimmen kann, da das Textfeld in Unicode falsche Umlaute anzeigt. Nach einigem Probieren steht fest, dass die Codepage 850 für MS-DOS-Lateinisch-I die korrekte ist. Nun können Sie den Import-Assistenten erneut starten und auf der Erweitert-Seite diese Codepage angeben. Ihren Namen Westeuropäisch (DOS) erhalten Sie allerdings erst nach einem Blick in die Tabelle tblCodepagesDAO. An der nun importierten Tabelle ist alles richtig.

VBA-Background der Anwendung

Schauen wir hinter die Kulissen und erläutern, wie das Formular programmiert ist.

Das Finden des Dateipfades übernimmt der Office-FileDialog. Der Code in Listing 3 sollte keiner weiteren Erklärung bedürfen. Der Filter für die Dateiendung wird auf "*.txt" eingestellt und der ausgewählte Dateipfad mit SelectedItem(1) übernommen. Da AllowMultiSelect auf False steht, kann nur eine Datei ausgewählt worden sein, weshalb die Beschränkung auf den Index 1 ausreicht.

Private Sub cmdOpenFile_Click()

     Dim oDlg As Office.FileDialog

     

     Set oDlg = FileDialog(msoFileDialogFilePicker)

     With oDlg

         .Filters.Clear

         .AllowMultiSelect = False

         .Title = "Textdatei auswählen"

         .InitialFileName = "*.txt"

         .InitialView = msoFileDialogViewDetails

         .Show

         If .SelectedItems.Count > 0 Then

             Me!txtFile = .SelectedItems(1)

         End If

     End With

     

     Set oDlg = Nothing

     EnableButtons

End Sub

Listing 3: Prozedur zum Öffnen des Dateiauswahldialogs

Die Prozedur EnableButtons des Formulars wird abschließend aufgerufen, um den Status der Schaltflächen einzustellen, die je nach vorhergegangener Aktion aktiviert oder deaktiviert werden.

Das Einlesen der Datei geschieht in der Ereignisprozedur cmdRead_Click (Listing 4). Hier wird über die VBA-Open-Anweisung die Datei in ein Byte-Array binText eingelesen. Dieses Array ist global im Kopf des Formularmoduls deklariert, weil auch in weiteren Prozeduren auf es zugegriffen wird:

Private Sub cmdRead_Click()

     Dim F As Integer

     Dim n As Long

     F = FreeFile

     Open Me!txtFile.Value For Binary Access Read As F

     ReDim binText(LOF(F) - 1)

     Get F, , binText

     Close F

     Me!TextOriginal.Value = binText

     Me.Dirty = False

     Form_Current

End Sub

Listing 4: Ereignisprozedur zum Öffnen des Dateiauswahldialogs

Private binText() As Byte

Dem Textfeld TextOriginal der dem Formular zugrundeliegenden Tabelle kann das Array nun schlicht über Value zugewiesen werden. Der Text befindet sich dann also binär im OLE-Feld und wird durch Me.Dirty = False auch physisch in der Tabelle gespeichert. Schließlich wird noch die Routine Form_Current angesprungen, die sonst eigentlich vom Ereignis Beim Anzeigen ausgelöst wird.

In verkürzter Form steht diese Routine in Listing 5. Ist ein Text schon eingelesen, so wird das Byte-Array binText umgekehrt aus dem Feld TextOriginal gefüllt. Andernfalls wird es auf 0 gesetzt und die Textanzeige der Textbox txtRead mit einem Leer-String bestückt. Im Weiteren wird geschaut, ob auch das Feld TextConverted mit Inhalt versehen ist. In diesem Fall wird dieser Text direkt dem Textsteuerelement zugewiesen. Andernfalls, und hier wird es spannender, muss die Textanzeige aus den Binärdaten des Arrays erzeugt werden. Da die MS-Forms-Textbox mit ANSI genauso wenig anfangen kann, wie die normale Textbox, sondern nach UTF-16 verlangt, müssen die Binärdaten erst nach Unicode konvertiert werden.

Private Sub Form_Current()

     If Not IsNull(Me!TextOriginal) Then

         binText = Me!TextOriginal.Value

     Else

         Me!txtRead.Text = ""

         Me!cbCodepages = Null

         ReDim binText(0)

     End If

     If Not IsNull(Me!TextConverted) Then

         Me!txtRead.Value = Me!TextConverted.Value

     Else

         Me!txtRead.Text = StrConv(binText, vbUnicode)

     End If

End Sub

Listing 5: Ereignisprozedur Beim Anzeigen des Formulars

Und das geschieht über die VBA-Funktion StrConv. Weil dieser hier kein Parameter LCID übergeben wird, nimmt sie die System-Sprache an. Folglich werden für einen DOS-formatierten Text nun auch falsche Umlaute in der Textbox erscheinen.

Dies alles läuft ab, wenn Sie das Einlesen auslösen oder das Formular schlicht angezeigt wird. Alle weiteren Prozeduren des Formulars werden durch die entsprechenden Schaltflächen ausgelöst.

Bevor wir die Analyse-Prozedur besprechen, nehmen wir uns des Buttons in Unicode an, dessen Ereignisprozedur verkürzt in Listing 6 abgebildet ist. Ist das Kombinationsfeld cbCodepages zur Auswahl der Codepage leer, so tut die Routine nichts anderes, als schon die Form_Current-Prozedur: sie verwandelt den Binärtext über StrConv in Unicode. Andernfalls folgen mehrere Schritte. Erst wird der passende Zeichensatz für die Codepage der Schrift der Textbox (Font.Characterset) zugewiesen. Dafür wird die Funktion CharSetFromCP des Moduls mdlTextconverter herangezogen. Sie kann über die Codepage und eine API-Funktion das CharacterSet bestimmen. Damit ist sichergestellt, dass die Textbox den richtigen Zeichensatz des Fonts benutzt und es nicht zu falscher Zeichenausgabe kommt. Übrigens ist als Schriftart für die Textbox Arial angegeben, das für fast alle Zwecke geeignet ist. Im weiteren Verlauf wird die Unicode-Umwandlung ebenfalls von einer gesonderten Routine DataFromFileCP vorgenommen. Ihr wird der Inhalt des Byte-Arrays binText übergeben und außerdem die gewünschte Codepage. Schauen wir uns diese Routine in Listing 7 genauer an.

Public Function DataFromFileCP(arrText() As Byte, ByVal lCodepage As Long) As String

     Select Case lCodepage

         Case 1200 'Unicode LE

             DataFromFileCP = StripLB(arrText())

         Case 1201 'Unicode BE

             DataFromFileCP = StripLB(ReverseBytes(arrText))

         Case 65001 'UTF8

             DataFromFileCP = StripLB(ANSIToWideChar(arrText, lCodepage))

         Case Else

             DataFromFileCP = ANSIToWideChar(arrText, lCodepage)

     End Select

End Function

Function ANSIToWideChar(ByVal sText As String, Optional ByVal CodePage As Long) As String

     Dim sBuffer As String

     Dim n As Long

     Dim pText As Long

     Dim pBuffer As Long

         

     sBuffer = String$((Len(sText) + 1) * 2, 0)

     pText = StrPtr(sText)

     pBuffer = StrPtr(sBuffer)

     n = MultiByteToWideChar(CodePage, 0&, pText, -1, pBuffer, Len(sBuffer))

     If n = 0 Then n = MultiByteToWideChar(GetACP(), 0&, pText, -1, pBuffer, Len(sBuffer))

     If n > 0 Then ANSIToWideChar = Left$(sBuffer, n - 1)

End Function

Listing 6: Prozedur zum Einlesen und zum Umwandeln

Wenn die Codepage die Werte 1200 oder 1201 aufweist, dann handelt es sich bereits um Unicode-16. Also könnte man der Textbox diesen direkt verabreichen? Leider nein, denn wenn die Textdatei die oben erwähnten Lead-Bytes zur Identifikation als UTF-16 enthält, werden diese beiden Bytes ebenfalls als Zeichen interpretiert. Sie müssen deshalb erst abgeschnitten werden, was die Funktion StripLB übernimmt, welche im Listing nicht dargestellt ist. Außerdem kann es sich um die Unicode-Variante Low Endian (Codepage 1201) handeln, die die Zeichen-Bytes vertauscht. Auch dieser Unicode wird von der Textbox nicht durchschaut. Die Bytes müssen erst in des Big-Endian-Format gebracht werden, was die Routine ReverseBytes erledigt – ebenfalls nicht im Listing abgebildet. Sie vertauscht einfach die geradzahligen mit den ungeradzahlig positionierten Bytes im Array.

Für alle andern Codepages muss eine Konvertierung über API erfolgen. Und zwar deshalb, weil die einfache VBA-Funktion StrConv nicht eine Codepage als Parameter erwartet, sondern eine LCID. Zwar kann die Codepage über API ebenfalls aus der LCID ermittelt werden, doch das Ergebnis ist dann immer nur die Default-Codepage der Sprache, die von der gewünschten Codepage abweichen kann. Wie auch immer: Die Prozedur ANSIToWideChar macht die ganze Arbeit über die API-Funktion MultiByteToWideChar. WideChar ist ein Synonym für Unicode-16.

Codepage analysieren

Und nun kommen wir zu genialen Funktion zur Analyse der Codepage eines Textes. Sie werden beim Googeln darüber nichts finden. Sie stammt im Kern von Michael Kaplan, einem ehemaligen Mitarbeiter von Microsoft, der wegen seiner freizügigen Berichterstattung in Foren und Blogs zu Zeiten von Access 2000 erst geschasst, dann als Leiter der Abteilung Internationalisierung wegen seiner Fähigkeiten aber wieder eingestellt wurde. Er hat über versteckte Kanäle veröffentlicht, dass die Jet-Engine, die sich nun Access Database Engine nennt, durch ihre Text-ISAM-Treiber über die Möglichkeit verfügt, für Importzwecke die Codepage einer Textdatei zu ermitteln, indem als Verbindungsoption die Anweisung Charset=Detect abgesetzt wird. Um den Vorgang zu verstehen, muss ein Ausflug in die SQL-Syntax für die Ansprache von Textdateien in Abfragen gemacht werden.

Statt einer Tabelle können Sie in einer Abfrage als Datenquelle auch eine Textdatei angeben. Eine mögliche Variante sähe in SQL so aus:

SELECT * FROM beispiel#txt

IN '' [TEXT;C:\XYZ\Textverzeichnis;]

Anstelle eines Tabellennamens wird nach der Select-Anweisung hier der Name einer Datei eingesetzt. Allerdings mag der ISAM-Treiber keinen Punkt als Trennzeichen für die Dateiendung. Er muss durch das Kreuz ersetzt werden. Der IN-Operator leitet ein, dass sich diese virtuelle Tabelle in einer Datenbank befindet, deren Zugriffsverbindung in eckigen Klammern anzugeben ist.

Interessanterweise kann als Quelle einfach der Verzeichnispfad angegeben werden. Damit die Database Engine weiß, um welche Art von Datenbank es sich handelt, ist vor den Pfad noch die Kennzeichnung zu setzen – hier TEXT. Die doppelten einfachen Anführungszeichen, ein String ohne Inhalt, sagen dem Treiber, dass alle Daten der Verbindung abzuholen sind. Diese Abfrage können Sie so zunächst nur in der SQL-Ansicht des Abfrageentwurfs eingeben. Nach Ausführung werden die einzelnen Zeilen der Textdatei als Datensätze ausgegeben.

Nun können Sie der Verbindung zusätzliche Optionsargumente übergeben. Sie können die Engine etwa anweisen, den Text als ANSI oder Unicode zu behandeln:

SELECT * FROM beispiel#txt

IN '' [TEXT;CharacterSet=ANSI;

C:\XYZ\Textverzeichnis;]

Diese Abfrage wird nur dann ein sinnvolles Ergebnis haben, wenn der eingelesene Text tatsächlich im Single-Byte-Format vorliegt. Haben Sie es mit einer Unicode-Datei zu tun, so ersetzten Sie ANSI einfach durch UNICODE. Das ist aber noch nicht alles. Auch die zu verwendende Sprache kann als Argument übergeben werden:

SELECT * FROM beispiel#txt

IN '' [TEXT;CharacterSet=ANSI;

Locale=1031;C:\XYZ\Textverzeichnis;]

Mit Locale=1031 erwirken Sie, dass nur die für Deutschland gültige Codepage zum Einsatz kommen soll. Setzten Sie stattdessen den Ausdruck ALL ein, so verwendet die Engine eine beliebige naheliegende Codepage, und das ist die des Systems.

Welche weiteren Parameter für CharacterSet angegeben werden können, ist nicht dokumentiert. Versuche ergaben etwa, dass es auch mit OEM funktioniert, was die Textdatei als MS-DOS-Text anspricht. Der Hammer ist jedoch der Begriff Detect, den Sie ebenfalls einsetzen können:

SELECT * FROM beispiel#txt

IN '' [TEXT;CharacterSet=DETECT;

Locale=ALL;C:\XYZ\Textverzeichnis;]

Damit ändert sich das Verhalten der Abfrage radikal! Statt den Text einzulesen, gibt sie jetzt sämtliche vom Text-Treiber unterstützte Codepages als Datensätze aus, wie in Bild 7, ein Ausschnitt aus der Abfrage qry_Detect. Die Spalte CPDescription enthält dabei die Bezeichnung, CPId die ID der Codepage. Der Clou ist jedoch die Spalte BestChoice. Dort steht für genau einen Datensatz ein True, also eine -1. Und das kennzeichnet die Codepage, die der Text-Treiber als günstigste Annahme für das Format der Textdatei ansah. Sie können die untersuchten Codepages auch einschränken, indem Sie für Locale eine andere LCID benutzen. Die Abfrage wird dann weniger Datensätze zurückgeben. Mit 1032 für Griechisch etwa bleiben noch 12 Datensätze. Geben Sie eine ungültige LCID an, so stellt sich nicht etwa ein Fehler ein, sondern in der Ausgabe tauchen nur noch vier Unicode-Codepages auf.

Von der Database Engine ermittelte Codepages

Bild 7: Von der Database Engine ermittelte Codepages

Nun können wir auch das Rätsel lösen, wie die Database Engine für das Füllen der Tabelle tblCodepagesDAO herangezogen werden kann. Dazu wird einfach die Abfrage qry_Detect als Quelle für eine Anfügeabfrage qry_MakeDAOCodepages verwendet und die Spalten CPDescription, wie CPId, werden zum Anfügen an die Tabelle genutzt.

[30707]

Damit kehren wir zum Formular frmTextconverter zurück. Die Ereignisprozedur ruft die Funktion DetectCPFileDAO2 auf, um die Codepage zu ermitteln, wobei als Parameter das Text-Array binText übergeben wird. Der Rückgabewert schließlich wird zum Setzen des Werts für das Kombinationsfeld cbCodepages verwendet. Den Code der Routine finden Sie im Wesentlichen in Listing 8. Sie können der Funktion entweder ein Byte-Array mit dem Text übergeben, wie dies auch im Formular geschieht, oder den Dateipfad einer Textdatei.

Function DetectCPFileDAO2(ByVal sText As Variant, sDescription As String) As Long

     Dim dbs As Database

     Dim qdf As QueryDef

     Dim rs As DAO.Recordset

     Dim sDir As String

     Dim sFile As String

     Dim n As Long

     Dim F As Integer

     Dim bin() As Byte

     If TypeName(sText) = "Byte()" Then

         sFile = CurrentProject.Path & "\testxy.txt"

         bin = sText

         F = FreeFile

         Open sFile For Binary As F

         Put F, , bin

         Close F

     ElseIf TypeName(sText) = "String" Then

         sFile = sText

     Else

         Exit Function

     End If

     

     n = InStrRev(sFile, "\")

     sDir = Left$(sFile, n)

     sFile = Mid(sFile, n + 1)

     

     Set dbs = CurrentDb

     Set qdf = dbs.CreateQueryDef("")

     qdf.SQL = "SELECT * FROM [" & _

         Replace(sFile, ".", "#") & "] IN '' " & _

         "[TEXT;CharacterSet=Detect;Locale=All;DATABASE=" & sDir & "]"

     Set rs = qdf.OpenRecordset(dbOpenSnapshot)

     Do While Not rs.EOF

         If rs!BestChoice Then

             DetectCPFileDAO2 = rs!CPId.Value

             sDescription = rs!CPDescription

             Exit Do

         End If

         rs.MoveNext

     Loop

     Set rs = Nothing

     If Len(Dir(CurrentProject.Path & "\testxy.txt")) > 0 Then

          Kill CurrentProject.Path & "\testxy.txt"

     End If

End Function

Listing 8: Funktion zum Ermitteln der wahrscheinlichsten Codepage eines Textes

Über TypeName untersucht die Funktion zunächst, ob es sich beim Parameter sText um ein Array oder einen Pfad-String handelt. Da die Database Engine nur auf Textdateien zugreifen kann, nicht jedoch auf Strings, muss im Falle eines Arrays dieses erst in eine temporäre Datei testxy.txt exportiert werden, die am Ende der Routine per Kill-Anweisung wieder gelöscht wird. In der Variablen sFile steht nun jedenfalls der Pfad zur Textdatei.

[31543]

Sie wird in zwei Teile gesplittet: sDir nimmt per Left() den Verzeichnisteil auf, sFile wird per Mid() neu auf den Dateinamen gesetzt. Beide braucht man zum Zusammensetzten des SQL-Strings für die im Folgenden erzeugte Abfrage. CreateQueryDef verlangt als Parameter einen Namen für die Abfrage. Übergibt man hier einen Leer-String, so wird eine temporäre Abfrage angelegt. Das bedeutet, dass sie nicht im Navigationsfenster erscheint und beim Verlassen der Routine automatisch gelöscht wird. Der SQL-Text der Abfrage wird nun mit den Teilen des Dateipfads zu einem ähnlichen String zusammengesetzt, wie weiter oben beschrieben.

[31979]

Dann wird ein Recordset rs auf die Abfrage über OpenRecordset geöffnet und deren Datensätze solange in einer Schleife durchlaufen, bis BestChoice den Wert True enthält. Das ist der Datensatz mit der gefundenen Codepage. Der Wert von CPId wird nun als Rückgabewert der Funktion verwendet und die Schleife wird verlassen. Eine alternative Lösung wäre, im SQL -Ausdruck die Datensätze gleich aufsteigend nach dem Feld BestChoice zu sortieren – dann wäre gleich der erste Datensatz der gesuchte und auf die Schleife könnte verzichtet werden:

[32607]

... ORDER BY BestChoice ASC;

[33145]