- Project Runeberg -  About Project Runeberg /
Projekt Runebergs konvertering till Unicode

Table of Contents / Innehåll | << Previous | Next >>
  Project Runeberg | Like | Catalog | Recent Changes | Donate | Comments? |   

Projekt Runebergs konvertering till Unicode

Redovisning av ett projekt understött av Kungl. biblioteket, genomfört i december 2012 av Lars Aronsson.

Handläggare Göran Konstenius.

Diarienummer 51-KB 977-2011.

I september 2011 skrev jag en ansökan om medel till modernisering av Projekt Runebergs plattform, att genomföras på 100 arbetstimmar under första hälften av 2012. Av olika orsaker har detta blivit uppskjutet, men är nu genomfört under december 2012.

Det handlar om konvertering av Projekt Runebergs äldre samlingar, det som scannades före 2005, från teckenstandarden ISO 8859-1 (Latin-1) till UTF-8 (Unicode). Enklast och tydligast visas det av en 8 minuter lång video, som finns tillgänglig i två format på adresserna

Teckenkoder

Datorer arbetar internt med ettor och nollor, binära siffror, bitar, som kan kombineras i mönster för att uttrycka talvärden. För att återge skrivtecken, måste varje tecken tilldelas ett talvärde, enligt en översättningstabell som kallas teckenkod eller teckenstandard (character set).

Med 6 bitar kan 64 kombinationer göras, vilket räcker för engelska alfabetets 26 bokstäver, de 10 siffrorna och de vanligaste skiljetecknen. Men med en så begränsad teckenkod finns det inte plats för både stora och små bokstäver. Mer användbart för löpande text blir det med 7 bitar, 128 kombinationer, som användes av ASCII-koden som var vanlig på 1970- och 1980-talet.

Om man i ASCII-koden byter ut några mindre vanliga skiljetecken, så kan man få plats med svenska ÅÄÖ. Eller danska ÆØÅ. Men inte båda samtidigt. En mängd sådana nationella varianter standardiserades i början av 1970-talet som ISO 646. Textterminaler kunde ställas in för att visa den ena eller andra nationella standarden.

Med 8 bitar kan 256 kombinationer göras, vilket gör det möjligt att i samma kod rymma danska, svenska och andra tecken. Detta blev vanligt på 1980-talet med persondatorernas genombrott, och varje tillverkare hade sina egna varianter. Den internationella standarden hette ISO 8859 och fanns i ett dussintal varianter, varav den första ISO 8859-1 omfattade nord- och västeuropeiska språk. Den användes som default av fönstersystemet X-Windows och i webbsidor fram till HTML version 4.0.

Just 8 bitar, en "byte", är en naturlig informationsenhet i de flesta datorer och det tar emot att använda fler bitar för varje tecken. Nästa hanterbara storlek är 16 eller 32 bitar, och om de bara ska bära ett tecken, som i de flesta fall är en bokstav A-Ö, så kommer många bytes att vara tomma och innehållslösa. Nästa internationella standard ISO 10646 fick därför svårt att slå igenom. Den kallas populärt Unicode, den universella teckenkoden, och omfattar alla världens skrivtecken, från runor och kilskrift till kinesiska.

Den praktiska lösningen har fått namnet UTF-8 och är en kompaktare version av Unicode. Den föreslogs 1992 av IBM[1] och innebär att bara 8 bitar (en byte) används för de tecken som ingår i den gamla ASCII-standarden, medan 2, 3, 4 eller 5 bytes används för de mer ovanliga eller exotiska tecknen.

Projekt Runeberg

För Projekt Runeberg, som jag grundade i december 1992 baserat på mina erfarenheter av datorer och Internet sedan 1980-talet, var det naturligt att använda den då rådande internationella standarden ISO 8859-1. Den understöddes av de Unix-system jag använde och hade därför blivit rådande även på Internet. Den rymde tecken från alla nordiska språk, så att danska, norska, svenska och även isländska texter kunde visas sida vid sida. Den crowdsourcing som Projekt Runeberg redan från början tillämpade bestod i att läsare kunde höra av sig med e-post, skicka in texter som de hade knappat eller scannat in, och jag lade upp dem på servern. Ibland kom det ZIP-filer skapade på MS-DOS eller Macintosh som följde sina egna teckenkoder, men jag omvandlade allt till ISO 8859-1.

Omkring sekelskiftet 2000 började XML slå igenom, och till skillnad från HTML 3.0 och HTML 4.0 utgick XML från UTF-8. Wikipedia grundades i januari 2001 och använde ISO 8859-1 under sina första år. Med tiden blev det uppenbart att detta verkligt internationella projekt hade ett starkt behov av Unicode. Ett i taget bytte språkversionerna till UTF-8 och från sommaren 2005 har programvaran Mediawiki (version 1.5) helt upphört att stödja andra teckenkoder än UTF-8.

I februari 2005 infördes möjligheten att använda UTF-8 i Projekt Runeberg, genom att fältet "CHARSET: utf-8" lades till som ett tillval i metadata för varje bok. Snart infördes detta som standard för nya böcker, men de gamla fick ligga kvar orörda. Det gällde bland annat Nordisk familjebok (inscannad 2002-2003), Dansk biografisk Lexikon (2003) och de 8 första banden av Salmonsens konversationsleksikon (2004).

Under åren sedan dess har några av de äldre verken omvandlats till UTF-8. Detta hände exempelvis 2008 med Salmonsens konversationsleksion, när de resterande 18 banden digitaliserades. Men omvandligen gjordes improviserat och manuellt, utan systematik. Och totalt 200.000 boksidor låg ännu kvar i ISO 8859-1.

Svårigheter

Det borde vara enkelt att omvandla från ISO 8859-1 till UTF-8. Varje tecken i den ena standarden har en given representation i den andra. Det finns färdiga program för detta. Några heter GNU recode och iconv. Men till detta kommer några svårigheter.

ISO 8859-1 lämnar några talvärden odefinierade, vilket Microsoft har utnyttjat genom att skapa teckenkoden Windows-1252, som utökar standarden med några användbara tecken, däribland långa tankstreck. Filer som använder utvidgningen ser bra ut, men bara på Windows-system, vilket får sidoeffekten att Microsoft kan beskylla sina konkurrenter för att vara mindre kompetenta. Tveklöst har några sådana tecken smugit sig in i Projekt Runebergs filer, vilket gör att vi i praktiken har tillämpat Windows-1252 och inte ISO 8859-1.

När frivilliga medhjälpare har skickat in filer från MS-DOS eller Macintosh, har omvandligen till ISO 8859-1 inte alltid gått felfritt. Det kan finnas kvar rester som bryter mot standarden, och som får de färdiga programmen att sparka bakut. Deras feltolerans är inte den bästa.

Projekt Runeberg tillämpar versionshantering enligt systemet RCS, som arkiverar alla äldre versioner av en fil. En möjlighet vid övergången vore att bara konvertera den aktuella versionen och sedan checka in den i RCS. Det skulle dock göra det omöjligt att jämföra redigeringar mot äldre versioner, eftersom skillnaden i teckenkodning skulle överskugga användarnas redigeringar. Det önskvärda är att i stället konvertera hela RCS-arkivet, så att alla äldre versioner följer den nya kodningen.

RCS sparar aktuell version och skillnader mot äldre versioner i en gemensam textfil (RCS-filen) tillsammans med redigeringskommentarer. Ibland har det hänt att filens innehåll i ISO 8859-1 har kombinerats med kommentarer skrivna i UTF-8. Båda teckenkoderna kan förekomma i samma fil.

Med RCS är det emellertid så, att filinnehåll och kommentarer alltid står på olika rader. Det som krävs är ett program som går igenom filen rad för rad och översätter rader i ISO 8859-1 till UTF-8, men låter de rader vara oförändrade som redan är skrivna i UTF-8. Ett sådant program får fördelen att det kan köras upprepade gånger utan skada, för när hela filen redan är i UTF-8 ändras ju ingenting. Programmet behöver också kunna undersöka om en konvertering är möjlig, innan man kör det i skarpt läge för att genomföra ändringarna.

Eftersom inte alla filer i Projekt Runeberg är incheckade i RCS, behöver programmet kunna undersöka om en RCS-fil finns.

För att konvertera RCS-filer, som är skrivskyddade och kan ägas av olika användaridentiteter, måste konverteringsprogrammet köras med systemrättigheter, vilket görs genom att starta det med "sudo".

Eftersom alla äldre versioner konverteras, som om de från början hade varit skrivna i UTF-8, kommer konverteringen inte att lämna några spår i RCS-historiken. RCS-filens tidsstämpel och rättigheter ska återställas till sina värden före omvandlingen.

Projekt Runeberg anger teckenkodning för varje bok eller volym, det vill säga ett band av ett flerbandsverk eller en årgång av en tidskrift. För varje sådan volym finns metadata som anger kodningen och en innehållsförteckning som räknar upp de filer som ingår i volymen och som måste konverteras i grupp. Volymen är den enhet som kan testas eller konverteras på riktigt.

Implementationen

Jag har skrivit programmet unicode.pl, vars källkod bifogas nedan. Det är ett Perl-program på 236 rader, varav 40 rader inledande kommentarer. Det utför sin egen konvertering från ISO 8859-1 eller rättare sagt Windows-1252 till UTF-8. Subrutinerna är:

#!/usr/bin/perl -w

# Aronsson was here, 10 December 2012
#
# Convert a single file or one complete Project Runeberg volume from
# 8-bit (Latin-1 and/or Windows-1252) to UTF-8. If filenames are given
# on the command line, convert the named files. If no filenames are
# given, convert the volume in the current directory (but not
# subvolumes).
#
# Source code is UTF-8 (åäö), input and output should be binary raw bytes
#
# If an RCS file is available, convert it, then check out the current
# version.  Note: All changes should be checked in before running this
# program.
#
# The owner, permissions and time of the converted file is preserved.
# Because of this, the program should run with sudo.
#
# Files are converted line by line. Lines in ASCII or already in UTF-8
# are kept without changes. Only lines with Latin-1 and Windows-1252
# characters are converted. Thus, it is possible to run the program
# repeatedly.  It will die if it finds errors.
#
# The converted file will only overwrite the old file if "--do" is
# given as the first command line argument.  It is practical to follow
# this routine:
#
# 1. Find a volume that is not yet converted, e.g.
#    cd /home/runeberg/texter/x/xyzzy
# 2. Check in all working files in RCS,
#    sh /home/runeberg/bin/rcs-force.sh
# 3. Run dry,
#    sudo perl /home/runeberg/bin/unicode.pl
# 4. If all goes well, run for real,
#    sudo perl /home/runeberg/bin/unicode.pl --do
# 5. Edit Metadata to say CHARSET: utf-8
#    Note that Metadata is still in Latin-1 (December 2012)
# 6. make
# 7. Check that no temporary files are left,
#    ls -a

use strict;
use utf8;
# use encoding "utf8";
use IO::Handle;			# autoflush
STDOUT->autoflush();
binmode(STDIN);
binmode(STDOUT); # , ":utf8");

my $dontmove = 1;
if ($#ARGV >= 0 && $ARGV[0] eq "--do") {
    $dontmove = 0;
    shift;
}

my @win1252 = (
    # 8x
    "\xe2\x82\xac", "",             "\xe2\x80\x9a", "\xc6\x92",
    "\xe2\x80\x9e", "\xe2\x80\xa6", "\xe2\x80\xa0", "\xe2\x80\xa1",
    "\xcb\x86",     "\xe2\x80\xb0", "\xc5\xa0",     "\xe2\x80\xb9",
    "\xc5\x92",     "",             "\xc5\xbd",     "",
    # 9x
    "",             "\xe2\x80\x98", "\xe2\x80\x99", "\xe2\x80\x9c",
    "\xe2\x80\x9d", "\xe2\x80\xa2", "\xe2\x80\x93", "\xe2\x80\x94",
    "\xcb\x9c",     "\xe2\x84\xa2", "\xc5\xa1",     "\xe2\x80\xba",
    "\xc5\x93",     "",             "\xc5\xbe",     "\xc5\xb8",
    );

my $win1252chars = 0;
my $latin1chars = 0;
sub unify_char ($) {
    # Do convert one character
    my ($arg) = @_;
    if (ord($arg) >= 0x80 && ord($arg) <= 0x9f) { # Windows 1252
	if ($win1252[ord($arg)-128] eq "") {
	    die sprintf("File contains 0x%02x\n", ord($arg));
	}
	$win1252chars++;
	return $win1252[ord($arg)-128];
    } elsif (ord($arg) >= 0xa0 && ord($arg) <= 0xff) { # Latin-1
	$latin1chars++;
	return sprintf("%c%c", 0xc0 + (ord($arg) >> 6), 0x80 + (0x3f & ord($arg)));
    } else {
	return $arg;
    }
}

sub unify ($) {
    # Look at one line of text, maybe convert it
    my ($arg) = @_;
    if ($arg =~ /^[\x01-\x7f]*$/) {
	# ASCII
    } elsif ($arg =~ /^([\x01-\x7f]|[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf]|[\xf0-\xf7][\x80-\xbf][\x80-\xbf][\x80-\xbf])*$/) {
	# UTF-8
    } else {
	# Windows 1252 + Latin-1
	$arg =~ s/(.)/unify_char($1)/ge;
    }
    return $arg;
}

sub test () {
    my %txt = (
	# ASCII
	"abcdef" => "abcdef",
	# UTF-8
	"\xc3\xa5\xc3\xa4\xc3\xb6" => "\xc3\xa5\xc3\xa4\xc3\xb6",
	# Latin-1
	"\xe5\xe4\xf6" => "\xc3\xa5\xc3\xa4\xc3\xb6",
	# Windows-1252
	"\x86" => "\xe2\x80\xa0",
	# mixed Latin-1 and Windows-1252
	"\xe5\x96\xf6" => "\xc3\xa5\xe2\x80\x93\xc3\xb6",
    );
    foreach (keys %txt) {
	die "Failed to convert $_, became " . unify($_)
	    unless ($txt{$_} eq unify($_));
	# printf "text %s - ok\n", $txt{$_};
    }
    printf "all tests ok\n";
}
test();

sub unifyfile ($) {
    # convert a single file to Unicode,
    # regardless of whether this is a plain file or RCS file.
    # return 1 if anything was changed, or else 0
    my ($arg) = @_;
    my $tmp = sprintf(".tmp-unicode-%05d.txt", int(rand(100000)));
    open OUTP, ">$tmp" or die "Failed to write $tmp";
    open INP, "<$arg" or die "Failed to read $arg";
    my $diff = 0;		# no difference yet
    while (<INP>) {
	my $out = unify($_);
	$diff = 1 if ($_ ne $out);
	printf OUTP "%s", $out;
    }
    close(INP);
    close(OUTP);
    if ($diff) {
	# TODO: avoid system()
	system("touch -r $arg $tmp && " .
	       "chmod --reference=$arg $tmp && " .
	       "chown --reference=$arg $tmp") == 0
	    or die "Failed to touch and chown $tmp";
	if ($dontmove) {
	    unlink($tmp)
		or die "Failed to unlink $tmp";
	} else {
	    rename $tmp, $arg
		or die "Failed to rename $tmp to $arg";
	}
	return 1;
    } else {
	unlink($tmp)
	    or die "Failed to unlink $tmp";
	return 0;		# no difference
    } 
}

sub onefile ($) {
    my ($arg) = @_;
    my $diff = 0;
    if (! -f $arg) {
	printf("File missing: %s\n", $arg);
	return;
    }
    my $rcs = $arg;
    $rcs =~ s|([^/]*)$|RCS/$1,v|;
    if (-f $rcs) {
	# printf "looking at %s\n", $rcs;
	$diff = unifyfile($rcs);
	if ($diff && !$dontmove) {
	    system("co $arg && chown \${SUDO_USER:-\$USER} $arg"); # refresh
	}
	$arg = $rcs;
    } else {
	# printf "looking at %s\n", $arg;
	$diff = unifyfile($arg);
    }
    if ($diff) {
	printf "Successfully converted %s\n", $arg;
    } else {
	printf "No changes in %s\n", $arg;
    }
}

sub onevolume () {
    # check in all RCS working files
    system("sh /home/runeberg/bin/rcs-force.sh");
    # "Metadata" should remain Latin-1 for now
    onefile("Makefile");
    onefile("index.html");
    onefile("Articles.lst");
    if (open LST, "<Articles.lst") {
	while (<LST>) {
	    chomp;
	    next if /^\#/;
	    s/\|.*//;
	    next if (/^$/);
	    if (/\/$/) {
		printf "Remember to convert subdirectory %s\n", $_;
		next;
	    }
	    next if (/^index$/);
	    next if (/^-$/);
	    onefile($_ . ".html");
	}
	close LST;
    }
    onefile("Pages.lst");
    if (open LST, "<Pages.lst") {
	while (<LST>) {
	    chomp;
	    next if /^\#/;
	    s/\|.*//;
	    next if (/^$/);
	    onefile("Pages/" . $_ . ".txt");
	}
	close LST;
    }
}

# main
$win1252chars = 0;
$latin1chars = 0;
if ($#ARGV >= 0) {
    foreach (@ARGV) {
	onefile($_);
    }
} else {
    onevolume();
}
printf "Converted $latin1chars Latin-1 "
    . "and $win1252chars Windows-1252 characters.\n";

[1] Rob Pike förnekar IBM:s inblandning.


Project Runeberg, Thu Dec 20 03:34:58 2012 (aronsson) (diff) (history) (download) << Previous Next >>
http://runeberg.org/admin/20121219-unicode.html

Valid HTML 4.0! All our files are DRM-free