Hallo,
Ist es auf ähnlich einfache Weise möglich, jedes beliebige Zeichen (das einen Akzent besitzt) in das entsprechende akzentlose Zeichen umzuwandeln (ohne einen regulären Ausdruck zu bemühen)?
Auf einfache Weise ist in Javascript nicht möglich; man muss sich da einiges dazu programmieren. Das Grundproblem ist es, dass Buchstaben mit diakritischen Anhängseln in den Unicode-Strings des Javascript in verschiedenen Varianten vorliegen können. Nehmen wir mal die beliebte „Äffin“ und ordnen ihren Buchstaben die Unicode Codepoints zu:
Ä f f i n U+00c4 U+0066 U+0066 U+0069 U+006e
Es gibt aber nun mal ziemlich viele diakritische Zeichen – und im Prinzip sollte man jedes Basiszeichen mit einem oder mehreren diakritischen Zeichen kombinieren können, wie das Sven-S. Porst hier s gut wie fast allen macht. In dieser kombinierenden Form sind die diakritischen Zeichen eigene Zeichen die sich mit dem vorher gehenden Basiszeichen kombinieren. Das sieht für die Äffin dann so aus:
A (Pünktchen) f f i n U+0041 U+0308 U+0066 U+0066 U+0069 U+006e
In einem beliebigen Unicode-String können dann beide Formen vorkommen, die kombinierende Form oder aber die Form des „Ä“, die vom Unicode Standard zur Kompabilität zu anderen Zeichensystemen vorgehalten werden. Für Programmierzwecke ist das natürlich doof; deswegen gibt es Varianten, Unicode-Strings zu eine Normalform zu bringen.
• Die erste Äffin wäre die Normalform NFC wie „Normal Form, Composed“. In dieser sind Zeichen und deren diakritische Zeichen zu einem Zeichen zusammengefasst, wenn es dieses Zeichen gibt und es geht.
• Die zweite Äffin wäre die Normalform NFD wie „Normal Form, Decomposed“. In dieser sind kombinierende Zeichen wie diakritische Zeichen prinzipiell vom Basiszeichen getrennt.
Die letztere Form wäre für den Zweck des Rausschmeißens diakritischer Zeichen gut geeignet; man kommt an einem Zeichen wie die Ä-Pünktchen vorbei, guckt nach, dass es ein diakritisches Zeichen ist und schmeißt es einfach raus. Nun definiert aber die Javascript-Spezifikation keine verpflichtende Normalform. Sie empfiehlt für Quelltext die Normalform NFC – empfiehlt es nur – zudem kann man Unicode-Strings aus den möglichsten und unmöglichsten Quellen bekommen, mittels XMLHttpRequest, mittels <script> und natürlich von irgendwelchen dahingebrowstern Besuchern. Vor einer Verarbeitung steht also immer eine Normalisierung auf NFD. Und das muß man sich selber anhand des Unicode-Standards programmieren. Hier mal eine „kleine“, naive Implementierung:
Die Unicode Normalisierungen (es gibt vier) sind im Unicode Standard Annex #15 definiert, am wesentlichen ist dort die Specification mit ihren vier Definitionen und zwei Regeln zuständig. Liest man das quer (und überliest mutig manche Ausnahmen wie Hangul und sonstige Spezialfälle), dann fällt einem schnell auf, dass sich dort auf die Unicode Character Database bezogen wird. Das ist eine Sammlung von Textdateien, in denen haufenweise Zusatzinformationen zu den verschiedenen Zeichen gesammelt wird. Am wichtigsten ist UnicodeData.txt (1.1 MiB), die Sammlung sämtlicher Zeichen in einer Datei. Eine Zeile dort hat dieses Format:
00C4;LATIN CAPITAL LETTER A WITH DIAERESIS;Lu;0;L;0041 0308;;;;N;LATIN CAPITAL LETTER A DIAERESIS;;;00E4;
Das ist tatsächlich eine Zeile mit diversen Einträgen, die durch Semikolons getrennt werden. Das 00C4 ist der Codepunkt in hexadezimaler Schreibweise, danach kommt der offizielle Name. Interessant für uns sind noch die „0“ (nach dem „Lu“) und die „0041 0308“ zwei Felder später. Letzteres ist die offizielle kanonische Dekomposition des Zeichen „Ä“ in die Zeichen „A“ und Pünktchen. Es gibt noch eine andere Dekomposition für die anderen Normalisierungsformen, die in deren Feld dann mit einem Label wie „<font>“ beginnen, die sind erstmal nicht interessant.
Die „0“ steht für die Kombinierungsklasse des jeweiligen Zeichens. Zeichen mit einer Klasse 0 sind nicht-kombinierende Zeichen. Bei dem Ä-Pünktchen ist die Klasse jedoch nicht 0:
0308;COMBINING DIAERESIS;Mn;230;NSM;;;;;N;NON-SPACING DIAERESIS;Dialytika;;;
Dort ist die Klasse 230. Der genaue Zahlenwert zeigt die Positionierung des kombinierenden Zeichens am Basiszeichen an; wichtiger ist, dass „nicht 0“ anzeigt, dass es ein kombinierendes Zeichen ist. Mit diesem Wissen kann man aus UnicodeData.txt die wesentlichen Informationen extrahieren: erstens die Zeichen, von denen man weiß, daß sie kombinierende Zeichen sind und zweitens, die Zeichen, von denen man deren Dekompositionen kennt. Und deren Dekompositionen natürlich.
Ich habe das mit einem kleinen Python-Skript gemacht. Der erste Teil – so rund um die for-Schleife – öffnet UnicodeData.txt, geht diese Zeile für Zeile durch, extrahiert die wesentlichen Informationen und wenn diese für unsere Zwecke passend sind werden diese in einem Dictionary gespeichert. Der zweite Teil ist etwas wüster – ich war da unnötigerweise kryptisch – aber letztendlich tut dieser nichts anderes, als die gespeicherten Daten in zwei Javascript-Objekte in Javascript-Syntax in eine Datei zu schreiben. So sieht das dann aus:
Normalization.COMBINING_CHARS = {
"\uA806" : 9,
"\u302A" : 218,
"\u302B" : 228,
"..." : "..."
}
Normalization.DECOMPOSABLE_CHARS = {
"\u2000" : "\u2002",
"\u2F9DF" : "\u8F38",
"\u00C0" : "\u0041\u0300",
"..." : "...",
}
Beispielhaft gekürzt, weil die tatsächliche Datei normalization_tables.js dann doch recht groß ist. Das könnt man platzverbrauchstechnisch noch ein bisschen optimieren, ich wollte aber etwas haben, dass ich auf einen Blick Fehler kontrollieren kann. Normalization.COMBINING_CHARS ist ein Objekt sämtlicher Zeichen mit deren Kombinierungsklasse, wenn diese nicht Null ist. Dadurch kann man dann später schnell mittels COMBINING_CHARS[character]
nachgucken, welche Kombinierungsklasse ein Zeichen besitzt. Das Gleiche für DECOMPOSABLE_CHARS, man wirft ein Zeichen ein und kriegt dessen dekomposierte Form als ein anderes Zeichen oder eine Zeichenkette heraus.
Die Algorithmen zur Normalisierung sind allerdings ein bisschen komplexer oder zumindest nerviger als ein einfaches Nachschlagen. Ich habe diese in normalization_code.js untergebracht. Als ersten Schritt wird aus der Tabelle DECOMPOSABLE_CHARS die Umkehrtabelle COMPOSABLE_CHARS aufgebaut. In dieser schlägt man mit ein paar Zeichen nach, was das zusammengefügte Zeichen für diese Zeichen ist. Danach kommen kleinere Funktionen mit aufsteigender Komplexität nach unten. Mal durchkommentiert:
• isCombining() – Helferfunktion, die nachschlägt, ob ein Zeichen ein kombinierendes ist. • isStarter() – Deren Umkehrfunktion – ob ein Zeichen ein Basiszeichen ist. • decomposeChar() – Nimmt ein Zeichen und gibt es entweder zurück oder aber dessen dessen auseinandergefügte Version • composeSubstring() – Nimmt einen kleinen String und schlägt nach, ob es für diesen eine zusammengefügte Version gibt. Wenn nicht, dann nicht. • decomposeString() – Ist letztendlich die String-Version von decomposeChar(). • isBlocked() – Blockierung ist ein Konzept aus dem Annex 15 von oben, das entscheidet, ob ein kombinierendes Zeichen an einen Substring zum Zwecke der möglichen Komposition drangehängt werden darf. Festgemacht wird das an Basiszeichen und am Wert der Kombinationsklasse. • composeString() – Geht einen ganzen String durch und betrachtet Unterketten von Zeichen ob diese abhängig vom Basiszeichen-Seins und der Blockierung überhaupt zusammengefasst werden dürfen. Wenn ja, dann werden sie. Auch wenn diese Funktion etwas komplizierter aussieht, ist sie eigentlich nur eine Programmversion der simplen Worte in der Spezifikation von Annex 15.
Ganz unten werden dann die beiden Normalisierungsformen an String-Objekte gepappt. NFD ist nichts anderes als decomposeString(). NFC besteht aus dem Auseinanderrupfen des Strings nach NFD und dann aus der Zusammenkleben mit composeString(). Jetzt kann man "Äffin".toNFD()
nutzen.
Eine removeDiacritics()-Methode könnte dann so aussehen:
String.prototype.removeDiacritics = function () {
var str = this.toNFD();
var buffer = "";
var length = str.length;
for (var count = 0; count < length; count++) {
var current = str[count];
if (Normalization.isCombining(current)) {
continue;
} else {
buffer += current;
}
}
return buffer.toNFC();
}
Diese transformiert den String in die Normalform NFD, geht diesen Zeichen für Zeichen durch und behält nur die Zeichen, die nicht kombinierende Zeichen sind. Das abschließende Konvertieren ist mehr Cargo Kult denn Notwendigkeit, fällt mir da auf – der Grund für NFC ist ja zu dem Zeitpunkt schon verschwunden.
Zum Selber-Ausprobieren gibt es akzentlos.html. Wenn man diese Datei zusammen mit normalization_tables.js und normalization_code.js in einem Ordner abspeichert, hat man ein kleines Experimentierfeld. Allen Quellcode findet man hier, wenn Lust auf Erweiterung oder Verbesserung da ist.
Sonstiges: Ich bin mir nicht ganz sicher, ob ich besondere Spezialfälle und Ausnahmen in der Unicode-Datenbank nicht übersehen habe. Besonders die Hangul-Silben-Normalisierung habe ich wohl einfach übergangen. Es gibt noch zwei andere Normalisierungsformen, NFKD und NFKC, bei denen das „K” für „compatibility“ steht. Das sind die, deren Dekomposierungen in der Unicode-Datenbank mit einem Label versehen sind. Diese Normalformen lösen nicht nur Kombinationen auf, sondern bilden auch Zeichen auf andere Zeichen ab. Aus dem Zeichen Å (für die Längeneinheit Ångström) wird dann entweder Å (A mit Kringel drauf) oder A und Kombinierender Kringel. Dabei kann dann durchaus Bedeutung flöten gehen, weswegen ich die gelassen habe. Bei dem Verfahren IDNA für internationale Domainnamen wird das verwendet. Christian Kruse hat mal einen Konvertierer in Javascript geschrieben bzw. dorthin konvertiert. Im NamePrep-Verfahren werden dort diese Normalformen verwendet; der Quelltext kann also interessant (und korrekter als meiner) sein.
In der Realität ist diese Lösung natürlich viel zu überdimensioniert. Allein schon die Nachschlagtabellen haben ein Gewicht von 68 KiB. Diese will man nicht wirklich mit jeder Seite übertragen. Da wäre zu überlegen, ob man nicht einfach eine kleine handerstellte Ersetzungstabelle mit den gängigen zu erwartenden Zeichen nimmt.
Oder man lässt es gleich ganz, wie Gunnar schon vorschlug. Denn bei Licht betrachtet diskriminiert das Verfahren extrem gegenüber Sonderzeichen und das unnötigerweise; schließlich kann das Web durchaus gut mit Unicode umgehen. Für ein Beispiel in URLs muss man nicht weiter gucken als zu Wikipedia. Wenn serverseitig irgendwelche Probleme bestehen, sollte man das eher dort beheben und dann (normalisiertes) Unicode speichern.
Tim