Unicode i Cerebrum

Informasjon om hvordan unicode bør benyttes i Cerebrum.

1   Historikk

Da Cerebrum ble startet på, foregikk nesten all representasjon og utveksling av tekst (på universitetet) i Latin-1.

Likevel var intensjonen at Cerebrum ikke skulle være bundet til et tegnsett. Mulighet for internasjonalisering var nøye gjennomtenkt, men behovet var ikke tilstede i stor nok grad til å ta høyde for språkstøtte (og dermed heller ikke tegnsett) i Cerebrum.

Fordi unicode-objektet ikke ble tatt i bruk tidlig, ble det også fort problemer å kombinere tegnsett. I praksis ble Cerebrum videreutviklet for å benytte seg av ett tegnsett, fra ende til annen.

2   Begrunnelse

Cerebrum trenger støtte for Unicode-data, av flere grunner:

  1. støtte for et bredere sett med tegn i Cerebrum-data
  2. støtte for integrasjon med systemer som benytter seg av andre tegnsett
  3. kompabilitet med Python 3

3   Hva skal være unicode?

Ikke alt kan være unicode. Vi trenger fremdeles bytestrings, f.eks. i kommunikasjon med andre systemer, og i persistent lagring.

Primært vil det være behov for å benytte unicode i all kommunikasjon, hvor tekstdata utveksles med andre systemer:

  • Importer
  • Eksporter
  • Bofhd-klienter
  • API-klienter

Interne data, spesielt string literals, trenger ikke være unicode med mindre det er behov for å representere tekst som ikke er i ascii-tegnsettet, eller det er behov for å kombinere tekstdata med annen tekstdata.

4   Unicode-sandwich

Det aller viktigste når det kommer til unicode og bytestrings, er å holde styr på hvordan tekstdata er representert.

Den enkleste måten å gjøre dette på, er å benytte unicode for all tekstdata. Da slipper man å sende metadata (enkoding) med tekstdata som man sender rundt, samt at unicode-objekter har støtte for å representere det meste man kan finne på å representere med tekst.

Dette betyr at vi må:

  • Dekode ved input (lese fra fil, request til api-er)
  • Enkode ved output (skrive til fil, response til klient)
  • Benytte en unicode-kompatibel enkoding i database-kommunikasjon (utf-8), og konfigurere database-modul til å ta imot og gi ut unicode-objekter.

Eventuell bruk av bytestrings internt i Cerebrum må være åpenbart eller godt dokumentert/kommentert. Et typisk område man fremdeles må bruke bytestrings, er bibliotek-metoder som krever dette.

Merk: Encode/decode-sandwich bør også inkludere normalisering, muligens med mulighet for unntak.

5   Normalisering

All unicode-tekst som taes inn i Cerebrum må normaliseres. Uten normalisering mister vi mulighet for å sammenligne tekst. Alle input-data til Cerebrum (importer, API-er) bør normaliseres ved import, altså så tidlig som mulig.

For data internt i Cerebrum, vil det være naturlig å benytte NFC, slik at tegn enkodet i latin-1 dekodes og mappes til "Latin-1 supplement"-blokken i Unicode (æ → U+00E6, ø → U+00F8, å → U+00E5). Dette gjør også at data kommunisert vha. latin-1 til database er normalisert korrekt pr.dd.

Ved output vil det potensielt være behov for/ønske om annen normalisering, og det er helt greit.

For normalisering benyttes unicodedata.normalize.

Det er ikke behov for å gjøre normalisering på innhold som er kjent, og allerede normalisert. F.eks.:

  • Dersom import_from_FS normaliserer, trenger man ikke normalisere innhold fra FS-fildumper.
  • Tekst som legges inn i database skal allerede være normalisert ved insert/update. Eventuelle feil bør løses av migrasjonsscript.

TODO: Implementere normalisering-wrappers.

6   Translitterasjon

Pr. dd. er det kun behov for translitterasjon ved generering av brukernavn og e-postadresser.

Her finnes allerede funksjonalitet for å behandle et subsett av Latin-1 til ASCII. Det samme subsettet finnes i Unicode, og funksjonaliteten kan portes til å behandle unicode-objekter på samme måte.

Annen eventuell translitterering må taes etter behov (f.eks. eksporter som fremdeles må foregå i en eller annen gitt enkoding).

7   Sortering

Ved behov for sortering av unicode-tekststrenger (hvor tekst kan være forventet å være utenfor ascii), bør vi benytte en modul som gjør dette riktig.

En god kandidat er pyuca.

## six

8   Praktisk

Her kommer en del eksempler og praktiske retningslinjer.

8.1   six

Enkelte steder vil man ha behov for kondisjonell bruk av bytes/unicode avhenging av Python-versjon. Dette kan f.eks. være et metode-kall i standardbiblioteket.

I disse tilfellene skal man benytte seg av six.text_type. Ved første bruk må biblioteket legges inn som requirement.

8.2   logging

All logging skal benytte seg av unicode-objekter. Logg-handler tar seg av enkoding.

8.3   print

print-statements bør generelt ikke benyttes i Cerebrum, men der det evt. forekommer, skal man printe unicode-objekter. Eventuell enkoding skal håndteres automatisk (PYTHONIOENCODING).

8.4   lese og skrive til fil

Filer som skal leses fra og skrives til, bør åpnes med forventet/tilsiktet enkoding (codecs.open). Man kan da skrive unicode-objekter som tekst, og få lest tekst som unicode-objekter.

>>> x = codecs.open('/tmp/foo', mode='Uw', encoding='utf-8')
>>> x.write(u"blåbærsyltetøy\n")
>>> x.close()
$ file /tmp/foo
/tmp/foo: UTF-8 Unicode text
$ cat /tmp/foo
blåbærsyltetøy
$ hexdump -C /tmp/foo
00000000  62 6c c3 a5 62 c3 a6 72  73 79 6c 74 65 74 c3 b8  |bl..b..rsyltet..|
00000010  79 0a                                             |y.|
00000012

Forøvrig oppfordres det til bruk av verktøy i codecs for transparent enkoding/dekoding av data ved behov.

8.5   literals

Bruk av literals i kode bør benytte seg av bytestrings, så lenge dette er mulig. Dette gjelder altså tekst som kun skal/kan inneholde tegn fra ASCII-tegnsettet, og som ikke skal kombineres med unicode-objekter.

8.6   tbd

  • regex-modul?
  • wrapping av stdin/stdout/stderr streams?

9   Fallgruver

9.1   Implisitt dekoding i Python 2

I Python 2 vil bytestrings implisitt dekodes dersom man forsøker å interpolere/konkatenere unicode-objekter og bytestrings:

>>> u"foo" + "bar"
u'foobar'
>>> u"foo%s" % "bar"
u'foobar'
>>> u"foo{}".format("bar")
u'foobar'

Dette vil ikke fungere i Python 3:

>>> "foo" + b"bar"  # typeerror
...
TypeError: must be str, not bytes
>>> "foo%s" %  b"bar"  # repr
"foob'bar'"
>>> "foo{}".format(b"bar")  # repr
"foob'bar'"

9.2   Sammenligning i Python 2

I Python 2 vil et unicode-objekt og en bytestring med samme innhold være like:

>>> "foo" == u"foo"
True
>>> "foo".__eq__(u"foo")
NotImplemented
>>> u"foo".__eq__("foo")
True

Dette er ikke tilfelle i Python 3:

>>> "foo" == b"foo"
False
>>> "foo".__eq__(b"foo")
NotImplemented
>>> b"foo".__eq__("foo")
NotImplemented

Dette påvirker også sett, e.g. Python 2:

>>> s = set([u"foo"])
>>> "foo" in s
True

Og Python 3:

>>> s = set([b"foo"])
>>> "foo" in s
False

9.3   Matching/operasjoner på unicode-tekst

Noen ganger kan det være nødvendig å lese filer hvor dekoding gjøres etter annen tekstbehandling (som f.eks. splitting på linje) ↓.

>>> a = u'hello\N{LINE SEPARATOR}world'
>>> len(a.split('\n'))
1
>>> len(a.splitlines())
2
Av fhl
Publisert 8. jan. 2018 17:28