Fall-Fall-Wahrheitsprogrammierer sollten es wissen

Auf der North Bay Python Conference im Jahr 2018 hielt ich einen Vortrag über Benutzernamen. Die meisten Informationen aus dem Vortrag wurden von mir über 12 Jahre lang gesammelt, um die Django-Registrierung zu unterstützen . Diese Erfahrung gab mir viel mehr Wissen als ich geplant hatte, um zu gewinnen, wie komplex „einfache“ Dinge sein können.



Zu Beginn meines Vortrags erwähnte ich jedoch, dass dies keine weitere Enthüllung aus der Reihe von "Missverständnissen über X, an die Programmierer glauben" sein wird. Sie können eine beliebige Anzahl solcher Enthüllungen finden. Diese Artikel gefallen mir jedoch nicht. Sie listen verschiedene Dinge auf, die angeblich falsch sind, erklären aber selten, warum dies so ist und was stattdessen getan werden sollte. Ich vermute, die Leute werden nur Artikel wie diese lesen, sich zu dieser Leistung beglückwünschen und dann interessante neue Wege finden, um Fehler zu machen, die in diesen Artikeln nicht erwähnt werden. Dies liegt daran, dass sie die Probleme, die diese Fehler verursachen, nicht wirklich verstanden haben.



Deshalb habe ich in meinem Bericht versucht, einige Probleme so gut wie möglich zu erklären und zu erklären, wie man sie löst - ich mag diesen Ansatz viel mehr . Eines der Themen, die ich nur nebenbei angesprochen habe (es war nur eine Folie und ein paar Erwähnungen auf anderen Folien), ist die Komplexität, die mit dem Fall von Charakteren verbunden sein kann. Es gibt eine offizielle Correct Answer ™ für das von mir diskutierte Problem - Vergleich der Groß- und Kleinschreibung ohne Berücksichtigung der Groß- und Kleinschreibung - und im Vortrag habe ich die beste Lösung gegeben, die ich kenne, wenn ich nur die Python-Standardbibliothek verwende.



Ich habe jedoch kurz die tieferen Komplexitäten der Unicode-Groß- und Kleinschreibung erwähnt und möchte einige Zeit der Beschreibung der Details widmen. Es ist interessant und das Verständnis kann Ihnen helfen, Entscheidungen beim Entwerfen und Schreiben von Textverarbeitungscode zu treffen. Deshalb biete ich Ihnen das Gegenteil der Artikel "Missverständnisse über X, an die Programmierer glauben" - "Wahrheiten, die Programmierer kennen sollten".



Noch etwas: Unicode ist voller Terminologie. In diesem Artikel werde ich hauptsächlich die Definitionen "Großbuchstaben" und "Kleinbuchstaben" verwenden, da der Unicode-Standardverwendet diese Begriffe. Wenn Sie andere Begriffe wie Klein- / Großbuchstaben mögen, ist das in Ordnung. Außerdem werde ich oft den Begriff "Symbol" verwenden, den manche als falsch empfinden. Ja, in Unicode ist das Konzept des "Charakters" nicht immer das, was die Leute erwarten. Daher ist es oft am besten, es durch die Verwendung anderer Begriffe zu vermeiden. In diesem Artikel werde ich jedoch den Begriff verwenden, wie er in Unicode verwendet wird - um eine abstrakte Entität zu beschreiben, die beansprucht werden kann. Wann immer es wichtig ist, verwende ich zur Verdeutlichung spezifischere Begriffe wie Code Point.



Es gibt mehr als zwei Register



Die Muttersprachler europäischer Sprachen sind daran gewöhnt, dass ihre Sprachen Groß- und Kleinschreibung verwenden, um bestimmte Dinge zu kennzeichnen. In englischen [und russischen] Sprachen beginnen wir Sätze normalerweise mit einem Großbuchstaben und fahren meistens mit Kleinbuchstaben fort. Außerdem beginnen Eigennamen mit Großbuchstaben, und viele Akronyme und Abkürzungen werden in Großbuchstaben geschrieben.



Und wir denken normalerweise, dass es nur zwei Register gibt. Es gibt den Buchstaben "A" und es gibt den Buchstaben "a". Eins in Großbuchstaben, eins in Kleinbuchstaben - ist das nicht so?



Es gibt jedoch drei Register in Unicode. Es gibt Großbuchstaben, Kleinbuchstaben und Titelbuchstaben [Titelbuchstaben]. Im Englischen werden Namen so geschrieben. Zum Beispiel "Avengers: Infinity War". Normalerweise wird der erste Buchstabe jedes Wortes einfach in Großbuchstaben geschrieben (und abhängig von verschiedenen Regeln und Stilen werden einige Wörter, wie z. B. Artikel, nicht groß geschrieben).



Der Unicode-Standard gibt ein Beispiel für ein Zeichen in Großbuchstaben: U + 01F2 LATEINISCHER GROSSBUCHSTABE D MIT KLEINEM Z. Es sieht folgendermaßen aus: Dz.



Solche Zeichen sind manchmal erforderlich, um die negativen Folgen einer der frühesten Lösungen für den Unicode-Standard zu bewältigen: Abwärtskompatibilität mit vorhandenen Textcodierungen. Für Unicode wäre es bequemer, Sequenzen unter Verwendung der Zeichenkombination des Standards zu erstellen. In vielen bestehenden Systemen wurde jedoch bereits Platz für vorgefertigte Sequenzen zugewiesen. In ISO-8859-1 ("lateinisch-1") hat das Zeichen "é" beispielsweise eine vorgefertigte Form mit der Nummer 0xe9. In Unicode ist es vorzuziehen, diesen Buchstaben mit einem separaten "e" und einem Akzentzeichen zu schreiben. Um jedoch die vollständige Abwärtskompatibilität mit vorhandenen Codierungen wie Latin-1 zu gewährleisten, weist Unicode auch Codepunkte für vorgefertigte Zeichen zu. Zum Beispiel U + 00E9 LATEINISCHER KLEINBUCHSTABE E MIT AKUT.



Obwohl die Codeposition dieses Zeichens mit dem Latin-1-Byte-Wert übereinstimmt, sollten Sie sich nicht darauf verlassen. Es ist unwahrscheinlich, dass die Zeichencodierung in Unicode diese Positionen beibehält. Beispielsweise wird in UTF-8 die Codeposition U + 00E9 als Bytefolge 0xc3 0xa9 geschrieben.



Und natürlich gibt es Zeichen in den vorhandenen Codierungen, die bei Verwendung der Großbuchstaben eine besondere Behandlung erfordern, weshalb sie "wie sie sind" in Unicode enthalten sind. Wenn Sie sie anzeigen möchten, durchsuchen Sie Ihre bevorzugte Unicode-Datenbank nach Zeichen aus der Kategorie Lt ("Letter, titlecase").



Es gibt verschiedene Möglichkeiten, den Fall zu definieren



Der Unicode-Standard (§4.2) listet drei verschiedene Falldefinitionen auf. Vielleicht wird die Wahl eines der drei von Ihrer Programmiersprache für Sie getroffen; Andernfalls hängt Ihre Wahl von Ihrem spezifischen Ziel ab. Diese Definitionen sind:

  1. Das Zeichen wird in Großbuchstaben geschrieben, wenn es sich in der Kategorie Lu befindet ("Buchstabe, Großbuchstabe"), und in Kleinbuchstaben, wenn es sich in der Kategorie Ll befindet ("Buchstabe, Kleinbuchstaben"). Der Standard erkennt die Einschränkungen dieser Definition an: Jedes spezifische Symbol muss nur einer der Kategorien zugeordnet werden. Aus diesem Grund erfüllen viele Zeichen, die in Groß- oder Kleinbuchstaben „sein müssen“, diese Anforderung nicht, da sie zu einer anderen Kategorie gehören.
  2. Das Zeichen wird in Großbuchstaben geschrieben, wenn es die Eigenschaft Großbuchstaben erbt, und in Kleinbuchstaben, wenn es die Eigenschaft Kleinbuchstaben erbt. Es ist eine Kombination der Definition von eins mit anderen Zeicheneigenschaften, einschließlich Groß- und Kleinschreibung.
  3. Ein Zeichen wird in Großbuchstaben geschrieben, wenn es sich nach der Zuordnung zu Großbuchstaben nicht ändert. Ein Zeichen wird in Kleinbuchstaben geschrieben, wenn es sich nach der Zuordnung zu Kleinbuchstaben nicht ändert. Dies ist eine ziemlich allgemeine Definition, kann sich aber auch nicht intuitiv verhalten.




Wenn Sie mit einer begrenzten Teilmenge von Symbolen (insbesondere Buchstaben) arbeiten, reicht möglicherweise eine Definition für Sie aus. Wenn Ihr Repertoire breiter ist - es enthält buchstabenähnliche Symbole, die keine Buchstaben sind -, ist die 2. Definition möglicherweise für Sie geeignet. Es wird vom Unicode-Standard §4.2 empfohlen:

Programmierer, die Unicode-Zeichenfolgen bearbeiten, sollten mit Zeichenfolgenfunktionen wie isLowerCase (und seinem funktionalen Cousin toLowerCase) arbeiten, wenn sie nicht direkt mit Zeicheneigenschaften arbeiten.




Die hier erwähnte Funktion ist in §3.13 des Unicode-Standards definiert. Formal verwendet Definition 3 die Funktionen isLowerCase und isUpperCase aus §3.13, die in Bezug auf die festen Positionen in toLowerCase bzw. toUpperCase definiert sind.



Wenn Ihre Programmiersprache über Funktionen zum Überprüfen oder Konvertieren des Falls von Zeichenfolgen oder einzelnen Zeichen verfügt, sollten Sie untersuchen, welche der oben genannten Definitionen in der Implementierung verwendet werden. Wenn Sie interessiert sind, verwenden die Methoden isupper () und islower () in Python die 2. Definition.



Es ist unmöglich, den Fall eines Charakters anhand seines Aussehens oder Namens zu verstehen



An der Erscheinung vieler Zeichen können Sie erkennen, in welchem ​​Fall sie sich befinden. Zum Beispiel ist "A" in Großbuchstaben. Dies geht auch aus dem Namen des Symbols hervor: "LATIN CAPITAL LETTER A". Manchmal funktioniert diese Methode jedoch nicht. Nehmen Sie den Codepunkt U + 1D34. Es sieht so aus: ᴴ. In Unicode wird ihm der Name zugewiesen: MODIFIER LETTER CAPITAL H. Es ist also Großbuchstaben, richtig?



Tatsächlich erbt es die Kleinbuchstaben-Eigenschaft, so dass es per Definition # 2 in Kleinbuchstaben geschrieben ist, obwohl es visuell einem Großbuchstaben H ähnelt und der Name das Wort "CAPITAL" enthält.



Einige Charaktere haben überhaupt keinen Fall



Definition 135 in §3.13 des Unicode-Standards besagt:

C unterscheidet genau dann zwischen Groß- und Kleinschreibung, wenn C über eine Eigenschaft in Klein- oder Großbuchstaben verfügt oder die Kategorie General_Category Titlecase_Letter ist.




Dies bedeutet, dass viele Unicode-Zeichen - tatsächlich die meisten - ohne Gehäuse sind. Fragen zu ihrem Fall sind nicht sinnvoll, und Falländerungen wirken sich nicht auf sie aus. Wir können jedoch die Antwort auf diese Frage per Definition # 3 erhalten.



Einige Zeichen verhalten sich so, als hätten sie mehrere Register



Die Implikation ist, dass Sie die Antwort "Ja" erhalten, wenn Sie Definition 3 verwenden und fragen, ob ein Zeichen ohne Groß- oder Kleinschreibung in Groß- oder Kleinbuchstaben geschrieben ist.



Der Unicode-Standard enthält ein Beispiel (Tabelle 4-1, Zeile 7) für das Zeichen U + 02BD MODIFIER LETTER REVERSED COMMA (das so aussieht: ʽ). Es verfügt nicht über die geerbten Eigenschaften für Klein- oder Großbuchstaben, gehört nicht zur Kategorie Lt und hat daher keine Groß- / Kleinschreibung. Gleichzeitig ändert sich durch die Konvertierung in Großbuchstaben nichts, und durch die Konvertierung in Kleinbuchstaben wird dies nicht geändert. Nach der dritten Definition werden beide Fragen mit "Ja" beantwortet: "Sind Sie in Großbuchstaben?" und "bist du klein geschrieben?"



Es scheint, dass dies zu unnötiger Verwirrung führen kann, aber der Punkt ist, dass Definition Nr. 3 mit jeder Folge von Unicode-Zeichen funktioniert und es Ihnen ermöglicht, die Konvertierungsalgorithmen für Groß- und Kleinschreibung zu vereinfachen (Zeichen ohne Gehäuse werden einfach zu sich selbst).



Groß- und Kleinschreibung ist kontextsensitiv



Wenn Unicode-Fallkonvertierungstabellen alle Zeichen abdecken, geht es bei dieser Konvertierung möglicherweise nur darum, den richtigen Platz in der Tabelle zu finden. In der Unicode-Datenbank heißt es beispielsweise, dass U + 0041 LATIN CAPITAL LETTER A in Kleinbuchstaben geschrieben ist. U + 0061 LATIN SMALL LETTER A. Einfach, nicht wahr?



Ein Beispiel, bei dem dieser Ansatz nicht funktioniert, ist Griechisch. Das Zeichen Σ - dh U + 03A3 GREEK CAPITAL LETTER SIGMA - wird bei der Konvertierung in Kleinbuchstaben zwei verschiedenen Zeichen zugeordnet, je nachdem, wo es sich im Wort befindet. Wenn es am Ende eines Wortes steht, wird es in Kleinbuchstaben geschrieben (U + 03C2 GREEK SMALL LETTER FINAL SIGMA). An anderer Stelle wird es σ sein (U + 03C3 GREEK SMALL LETTER SIGMA).



Dies bedeutet, dass das Register nicht eins zu eins oder transitiv ist. Ein anderes Beispiel ist ß (U + 00DF LATIN SMALL LETTER SHARP S oder Escet ). Es wird "SS" in Großbuchstaben sein, obwohl es jetzt eine andere Großbuchstabenform gibt (ẞ, U + 1E9E LATIN CAPITAL LETTER SHARP S). Die Konvertierung von "SS" in Kleinbuchstaben führt zu "ss". Daher (unter Verwendung der Unicode-Terminologie für die Konvertierung von Groß- und Kleinschreibung): toLowerCase (toUpperCase (ß))! = Ss.



Der Fall ist vom Gebietsschema abhängig



Unterschiedliche Sprachen haben unterschiedliche Regeln für die Konvertierung von Groß- und Kleinschreibung. Das beliebteste Beispiel: i (U + 0069 LATEINISCHER KLEINBUCHSTABE I) und I (U + 0049 LATEINISCHER GROSSBUCHSTABE I) werden in den meisten Regionen ineinander konvertiert - die meisten, aber nicht alle. In den Gebietsschemas az und tr (türkische Sprachen) ist der Großbuchstabe i İ (U + 0130 LATEINISCHER GROSSBUCHSTABE I MIT DEM OBEN PUNKT) und der Kleinbuchstabe I ı (U + 0131 LATEINISCHER KLEINBUCHSTABE DOTLESS I). Manchmal bedeutet es wirklich den Unterschied zwischen Leben und Tod, es richtig zu machen.



Unicode selbst behandelt nicht alle möglichen Regeln für die Konvertierung von Groß- und Kleinschreibung für alle Gebietsschemas. Die Unicode-Datenbank enthält nur allgemeine Regeln zum Konvertieren aller Zeichen, die nicht spezifisch für das Gebietsschema sind. Es gibt auch spezielle Regeln für einige Sprachen und zusammengesetzte Formen - Litauisch, Türkisch, einige Merkmale des Griechischen. Alles andere ist nicht da. §3.13 des Standards erwähnt dies und empfiehlt die Einführung von länderspezifischen Übersetzungsregeln, falls erforderlich.



Ein Beispiel wäre ein englischsprachiges Zeichen - dies ist der Titelfall bestimmter Namen. "O'brian" muss in "O'Brian" (nicht "O'brian") konvertiert werden. "Es ist" muss jedoch in "Es ist" und nicht in "Es ist" konvertiert werden. Ein weiteres Beispiel, das in Unicode nicht behandelt wird, ist die niederländische Buchstabenkombination "ij", die bei der Konvertierung in Titel in Großbuchstaben konvertiert werden muss, wenn sie am Wortanfang erscheint. Die größte Bucht der Niederlande im Titelregister ist somit "IJsselmeer" und nicht "Ijsselmeer". Unicode hat die Zeichen IJ U + 0132 LATIN CAPITAL LIGATURE IJ und ij U + 0133 LATIN SMALL LIGATURE IJ, falls erforderlich. Standardmäßig werden sie bei der Konvertierung in Groß- und Kleinschreibung konvertiert (obwohl Unicode-Normalisierungsformulare mit Kompatibilitätsäquivalenz sie in zwei separate Zeichen aufteilen).





Zurück zum im Bericht vorgestellten Material. Aufgrund der Komplexität der Unicode-Fallverwaltung können Vergleiche ohne Berücksichtigung der Groß- und Kleinschreibung nicht mit den in vielen Programmiersprachen üblichen Standardfunktionen zur Konvertierung von Klein- oder Großbuchstaben durchgeführt werden. Für solche Vergleiche hat Unicode das Konzept der Fallfaltung, und §3.13 des Standards definiert die Funktionen toCaseFold und isCaseFolded.



Sie könnten denken, dass das Gießen in gefaltete Fälle dem Gießen in Kleinbuchstaben ähnlich ist - aber nicht. Der Unicode-Standard warnt davor, dass eine Zeichenfolge in gefalteter Groß- und Kleinschreibung nicht in Kleinbuchstaben geschrieben werden muss. Als Beispiel wird die Cherokee-Sprache angegeben - dort werden in einer Zeichenfolge in gefalteter Schreibweise auch Großbuchstaben angezeigt.



In einer der Folien in meinem Vortrag ist der Unicode Technical Report # 36 so vollständig wie möglich in Python implementiert. Die NFKC-Normalisierung wird durchgeführt, und dann wird die casefold () -Methode (nur in Python 3+ verfügbar) für die resultierende Zeichenfolge aufgerufen. Trotzdem fallen einige Randfälle heraus, und dies wird für den ID-Vergleich nicht wirklich empfohlen. Die schlechte Nachricht zuerst: Python macht nicht genügend Unicode-Eigenschaften verfügbar, um Zeichen herauszufiltern, die nicht in XID_Start oder XID_Continue enthalten sind, oder Zeichen, die eine Default_Ignorable_Code_Point-Eigenschaft haben. Soweit ich weiß, wird die NFKC_Casefold-Zuordnung nicht unterstützt. Es gibt auch keine einfache Möglichkeit, die modifizierte NFKC UAX # 31§5.1 zu verwenden.



Die gute Nachricht ist, dass die meisten dieser Randfälle keine wirklichen Sicherheitsrisiken beinhalten, die von den fraglichen Symbolen ausgehen. Und das Fallfalten wird im Prinzip nicht als normalisierungserhaltende Operation definiert (daher das NFKC_Casefold-Mapping, das nach dem Fallfalten wieder auf NFC normalisiert wird). Im Vergleich ist es im Allgemeinen egal, ob beide Zeichenfolgen nach der Vorverarbeitung normalisiert werden. Sie kümmern sich darum, ob die Vorverarbeitung nicht inkonsistent ist und ob sie garantiert, dass nur Zeilen, die sich danach "unterscheiden" sollten, danach anders sind. Wenn Sie diesbezüglich Bedenken haben, können Sie nach dem Hinzufügen des Registers manuell neu normalisieren.



Genug für jetzt



Dieser Artikel ist wie der vorherige Bericht nicht erschöpfend und es ist kaum möglich, all dieses Material in einem einzigen Beitrag zusammenzufassen. Ich hoffe, dies war ein nützlicher Überblick über die Komplexität dieses Themas und bietet genügend Ausgangspunkte, um nach weiteren Informationen zu suchen. Daher können Sie hier grundsätzlich aufhören.



Wäre es nicht naiv zu hoffen, dass andere Leute aufhören, Belichtungen aus der Reihe von "Missverständnissen über X, an die Programmierer glauben" zu schreiben, und Artikel wie "die Wahrheit, die Programmierer wissen sollten" schreiben?



All Articles