In den letzten Tagen ist ein eigentlich simpler Codepage-Fehler aufgefallen, dessen Analyse für mich sehr aufschlussreich war. Ich möchte die leider etwas lange "Geschichte" der Analyse und Lösungsfindung daher gern teilen, falls jemand bei vergleichbaren Problemen einen Ansatz finden kann.
Was war passiert?
Wir wollten ursprünglich nur ein neues Adobe-PDF-Formular testen. Beim Kontrollieren des Druckresultats ist uns durch Zufall aufgefallen, dass auf einem Formular im Nachnamen ein "✡" stand. Im Debugger zeigte sich an der entsprechenden Stelle ein "#", also gab es ein ungültiges Zeichen im Nachnamen. Von "✡" keine Spur, als Unicode-Zeichen (U+2721) hätte man es in unserem Unicode-System allerdings sehen müssen.
Bei "seltsamen Zeichen" ist mein Ansatz immer, mir im Debugger den Hexadezimalwert anzusehen und diesen mit einem Converter mit verschiedenen Zeichensätzen zu übersetzen.
Code: Alles auswählen.
Hex 4D 75 C2 9A 74 ...
---------------------------------
Latin-1 MuÂt...
UTF-8 Mut...
Windows-1252 Mušt...
Windows-1252 sah vielversprechend aus, "š" könnte zum Rest des Namens passen. Hier aber auch kein Hinweis auf den Stern (nach UTF-8 hätte der den Hex-Wert "E2 9C A1").
Die eigentliche Ursache war dann schnell gefunden: Die Daten wurden über eine Schnittstelle aus einer CSV-Datei eingelesen. Beim Einlesen wurde die Codepage nicht explizit mit angegeben; da die Datei mit ENCODING NON-UNICODE geöffnet wurde, hat das System (
siehe ABAP-Hilfe zum Zusatz NON-UNICODE) die Codepage 1100 (ISO-8859-1, Latin-1) angenommen. Tatsächlich hatte die Datei allerdings die Codepage Windows-1252, also 1160. Dank IGNORING CONVERSION ERRORS gab es keinen Abbruch.
Für die Zukunft ist die Lösung also klar: einfach die richtige Codepage "1160" angeben. Bleibt noch die Frage, wie man die bisher fehlerhaft erzeugten Daten auffinden und reparieren kann und wo genau der Stern herkommt.
Von š zu #
Eine Codepage ist ja vereinfacht gesagt eine Liste von Zeichen, denen eine bestimmte Zahl (
Codepoint) zugeordnet wird. Die Zahl wird letztlich binär gespeichert. Beim Interpretieren wird dann in der entsprechenden Liste nachgeschlagen, welches Zeichen hinter der Zahl steckt.
Es ist oft so, dass sich hinter einer Zahl in zwei Codepages völlig verschiedene Zeichen verbergen. Es gibt gewisse Übereinstimmungen, so sind alle in Latin-1 enthaltenen druckbaren Zeichen mit dem gleichen Codepoint auch in Windows-1252 und UTF-8 vorhanden. Windows-1252 hat aber Zeichen, die in Latin-1 und in UTF-8 nicht druckbar sind. Schlägt man in der falschen Liste nach, bekommt man also ggf. ein falsches Zeichen, also wie hier "Mu
termann" statt "Mu
štermann".
Auch haben einige Codepages nur ein Byte pro Zeichen und beispielsweise UTF-8 bis zu vier. Übersetzt man beispielsweise UTF-8-Daten mit der Codepage Windows-1252, erhält man unter Umständen zwei, drei oder vier statt nur einem Zeichen. Schlägt man in der falschen Liste nach, bekommt man also unter Umständen nicht nur falsche, sondern mehr Zeichen als erwartet - hier "Mu
štermann" statt "Mu
štermann". .
Das fragliche Zeichen aus der Ursprungsdatei war ein "š". Dieses ist bei Windows-1252 das 154. Zeichen. Im Zeichensatz Latin-1 ist das 154. Zeichen ein nicht druckbares Zeichen; auch in UTF-8 ist dies der Fall. Das System hat also aufgrund der falsch angegebenen Codepage die Binärdaten nicht als "š", sondern als Steuerzeichen interpretiert.
In den 1-Byte-Zeichensätzen wäre der Hexadezimalwert übrigens "9A", unter UTF-8 hat das Zeichen 2 Byte und den Hexadezimalwert "C2 9A". Bei der Übersetzung des Hex-Codes aus dem Debugger mit Windows-1252 wurde "C2" als eigenes Zeichen interpretiert und ergab ein zusätzliches "Â".
Damit ist klar, warum in der Datenbanktabelle eine Raute auftaucht bzw. ein nicht druckbares Zeichen vorhanden ist. Es gibt aber immer noch keine Spur vom Stern und es wäre gut, wenn man die fehlerhaften Daten korrigieren oder zumindest identifizieren könnte.
Von # zu ✡
Warum aus dem nicht druckbaren Zeichen ein Stern wird, liegt letztlich am Adobe Document Server. Wenn Zeichen übermittelt werden, die in der jeweiligen Schrift nicht druckbar sind, versucht Adobe, ersatzweise auf eine Symbolschriftart zurückzugreifen. Im fertigen PDF war die Schriftart "Adobe Pi Std" zu finden, welche als 154. Zeichen tatsächlich den Stern hat.
Das Verhalten könnte man ggf. mit Ersetzungen (vgl. Hinweis 1489570) beeinflussen oder durch eine Bereinigung der Daten um nicht druckbare Zeichen vor Aufruf des Formulars, aber das würde nicht das eigentlich richtige Zeichen zurückbringen.
Lösung zur Bereinigung
Der Anspruch war, alle durch den Codepagefehler erzeugten fehlerhaften Daten zu identifizieren und im besten Fall automatisch zu reparieren. Dazu mussten wir zunächst eingrenzen, wonach wir eigentlich suchen.
Die Zeichensätze
Latin-1 und
Windows-1252 sind weitestgehend deckungsgleich. Beide haben 1 Byte pro Zeichen und umfassen damit 256 mögliche Zeichen. Die Zeichen 00 bis 1F (0 - 31) sind in beiden Zeichensätzen Steuerzeichen, danach folgen Sonderzeichen, Ziffern sowie Groß- und Kleinbuchstaben.
Einen Unterschied gibt es bei den Zeichen 7F bis 9F (127 - 159): Während diese Zeichen bei Latin-1 nicht belegt sind, finden sich hier bei Windows-1252 einige Zeichen wie das Eurozeichen, Š oder Ž. Im nachfolgenden Abschnitt A0 bis FF (160 - 255) sind die Zeichensätze wieder identisch.
Wurde Latin-1 statt Windows-1252 angegeben, ist dies also nur dann problematisch, wenn in den jeweiligen Daten die Zeichen 7F bis 9F - bzw. 127 bis 159 - vorkommen; alle anderen Zeichen werden dennoch korrekt interpretiert. Da wir uns in einem Unicode-System bewegen, ist dabei zu beachten, dass diese Zeichen dort mit zwei Byte - hexadezimal C2 7F bis C2 9F - gespeichert werden.
Die Lösung ist also, den Hexadezimalcode der Daten zu analysieren und zu prüfen, ob die genannte Bytefolge dort vorkommt. Hierbei bin ich so vorgegangen:
Zunächst werden alle in Frage kommenden Datensätze, beschränkt auf die relevanten Spalten, selektiert. In einer Schleife werden dann die einzelnen Datensätze Feld für Feld analysiert.
Hier wird geprüft, ob das Feld überhaupt Sonderzeichen enthält, um nicht jedes Zeichen analysieren zu müssen (könnte man natürlich auch tun; was performanter wäre, habe ich nicht ausprobiert). Dazu wird der Inhalt zunächst in Großbuchstaben umgewandelt; irrelevante gängige Sonderzeichen und Leerzeichen werden entfernt. Danach wird über einen regulären Ausdruck nach verbliebenen, nicht alphanumerischen Zeichen gesucht.
Code: Alles auswählen.
TRANSLATE lv_string TO UPPER CASE.
TRANSLATE lv_string USING '. - & , + : ß ẞ ( ) " '.
CONDENSE lv_string NO-GAPS.
SHIFT lv_string LEFT DELETING LEADING space.
FIND ALL OCCURRENCES OF REGEX '[^A-ZÄÖÜ0-9]' IN lv_string
RESULTS lt_found_chars.
Die Treffer werden aus dem String extrahiert und in einer internen Tabelle mit Angabe von Offset und Länge gespeichert. Ebenfalls wird bereits der Hexadezimalwert mit der Klasse CL_ABAP_CODEPAGE erzeugt.
Code: Alles auswählen.
ls_character-hex_value= cl_abap_codepage=>convert_to( source = |{ ls_character-character }| codepage = 'utf-8' ).
Im nächsten Schritt werden die gefundenen Sonderzeichen näher untersucht. Ist der Hexadezimalwert kleiner als 2 (Byte) oder ist dessen erstes Byte nicht "C2", kann das Sonderzeichen bereits ausgeschlossen werden.
Code: Alles auswählen.
IF xstrlen( ls_character-hex_value ) NE 2.
RETURN.
ENDIF.
IF ls_character-hex_value(1) NE 'C2'.
RETURN.
ENDIF.
Andernfalls wird durch eine einfache Konvertierung in einen Integer-Wert der Codepoint ermittelt. Dieser muss zwischen 127 und 159 liegen, dann liegt eines der gesuchten problematischen Zeichen vor.
Code: Alles auswählen.
DATA: lv_codepoint TYPE i.
[...]
lv_codepoint = ls_character-hex_value+1.
lv_critical = xsdbool( lv_codepoint GE 127 AND lv_codepoint LE 159 ).
Nun der vielleicht interessantere Teil: Das eigentliche Zeichen wird "wiederhergestellt" und der Feldinhalt damit repariert. Zunächst wird hierzu der 1-Byte-Hexadezimalwert abgeleitet (in UTF-8 ist ja nur im ersten Byte "C2" vorangestellt, das zweite Byte ist gleich). Dazu genügt, das zweite Byte des Zeichens zu übernehmen. Aus "C2 9A" wird hier somit "9A".
Code: Alles auswählen.
DATA: lv_replacement_char TYPE c LENGTH 1,
lv_ansi_hex_value TYPE xstring.
lv_ansi_hex_value = ls_character-hex_value+1.
Dieses wird nun mithilfe der Klasse CL_ABAP_CODEPAGE unter Anwendung des Zeichensatzes Windows-1252 in Text konvertiert. Aus "9A" wird dann wieder das in der Ursprungsdatei enthaltene "š". Man muss letztlich also nur das "überflüssige" zweite Byte abschneiden und die Daten noch einmal mit der richtigen Codepage neu interpretieren.
Code: Alles auswählen.
lv_replacement_char = cl_abap_codepage=>convert_from( source = lv_ansi_hex_value codepage = `windows-1252` ).
Über Offset und Länge der Fundstelle lässt sich das nicht druckbare Zeichen dann im Feldwert ersetzen. Die entsprechenden Werte können dann (natürlich ausschließlich mittels offiziell freigegebener Schnittstellen und nach Kontrolle durch die Fachabteilung 😉) zurückgeschrieben werden.
Code: Alles auswählen.
lv_string+ls_character-offset(ls_character-length) = lv_replacement_char.
Fazit
Wurden durch die Angabe einer falschen Codepage fehlerhafte Daten erzeugt, lassen sich die betroffenen Datensätze ggf. durch eine Analyse des Hexadezimalcodes auffinden und rekonstruieren. Jedenfalls wenn wie hier ein problematischer Bereich und eine Ableitungsregel so eindeutig bestimmt werden können.
In jedem Fall lohnt sich ein Blick in die Zeichentabellen der beteiligten Zeichensätze.