将混合编码文件转换成UTF8的Perl问题

7

我正在将我们大学中文系古老的基于DOS的图书馆程序生成的文件转换为更有用和易于访问的内容。

我正在处理的问题之一是导出的文本文件(约80MB)采用混合编码。我使用的是Windows操作系统。

德语umlauts和其他高ASCII字符采用cp1252编码,CJK字符采用GB18030编码。由于“重叠”编码的原因,我不能只是将整个文件拖到Word或其他软件中进行转换,否则会得到类似以下内容:

原始:

+Autor:
-Yan, Lianke / ÑÖÁ¬¿Æ      # encoded Chinese characters
+Co-Autor:
-Min, Jie / (šbers.)       # encoded German U-umlaut (Ü)

结果:

+Autor:
-Yan, Lianke / 阎连科       # good
+Co-Autor:
-Min, Jie / (歜ers.)       # bad... (should be: "Übers.")

我写了一个脚本,包含多个子程序,可以将非ASCII字符转换为Unicode字符。它执行以下操作(还有其他操作):

  1. 用字母数字代码替换某些高阶ASCII字符(如š、á等)。例如:-Min, Jie / (šbers.) -> -Min, Jie / (uumlautgrossbers.)。注意:我手动制作了“转换表”,因此只考虑了文档中实际出现的特殊字符。因此,转换不完全,但在我的情况下可以产生足够的结果,因为我们的书大多是德语、英语和中文,只有极少数是意大利语、西班牙语、法语等语言,几乎没有捷克语等。

  2. 仅当它们不是前面或后面的另一个高ASCII范围内的字符\x80-\xFF时,将á、£、¢、¡、í替换为字母数字代码(这些是cp1252编码的ß、ú、ó、í和“带交叉线的小北欧o”版本,它们都出现在cp1252和GB18030编码的字符串中)。

  3. 读入整个文件,并将其从GB18030转换为UTF8,从而将编码的中文字符转换为真正的中文字符。

  4. 将字母数字代码转换回其Unicode等效项。

虽然脚本基本上可以工作,但存在以下问题:

  • 在转换原始80MB文件后,Notepad++仍然认为它是ANSI文件,并将其显示为ANSI。我需要按“编码->以UTF-8编码”才能正确显示。

我想知道的是:

  1. 通常,是否有更好的方法将混合编码文件转换为UTF-8?

  2. 如果没有,我应该使用use utf8,以便直接输入字符而不是它们在codes2char子程序中的十六进制表示形式吗?

  3. 文件开头加上BOM能否解决NP++最初将其显示为ANSI文件的问题?如果可以,我应该如何修改我的脚本,使输出文件具有BOM?

  4. 在转换之后,我可能需要调用一些其他子程序(例如,将整个文件转换为CSV或ODS格式)。我需要继续使用codes2char子程序的开头语句吗?

代码由多个子程序组成,这些子程序在结尾处被调用:

!perl -w
use strict; 
use warnings;
use Encode qw(decode encode); 
use Encode::HanExtra;

our $input = "export.txt";
our $output = "export2.txt";

sub switch_var {                # switch Input and Output file between steps
    ($input, $output) = ($output, $input);
}

sub specialchars2codes {
open our $in, "<$input" or die "$!\n"; 
open our $out, ">$output" or die "$!\n"; 

while( <$in> )  {   
    ## replace higher ASCII characters such as a-umlaut etc. with codes.
    s#\x94#oumlautklein#g;
    s#\x84#aumlautklein#g;
    s#\x81#uumlautklein#g;
    ## ... and some more. (ö, Ö, ä, Ä, Ü, ü, ê, è, é, É, â, á, à, ì, î, 
    ## û, ù, ô, ò, ç, ï, a°, e-umlaut and ñ in total.)

    ## replace problematic special characters (ß, ú, ó, í, ø, ') with codes.
    s#(?<![\x80-\xFF])\xE1(?![\x80-\xFF])#eszett#g;
    s#(?<![\x80-\xFF])\xA3(?![\x80-\xFF])#uaccentaiguklein#g;
    s#(?<![\x80-\xFF])\xA2(?![\x80-\xFF])#oaccentaiguklein#g;
    s#(?<![\x80-\xFF])\xA1(?![\x80-\xFF])#iaccentaiguklein#g;
    s#(?<![\x80-\xFF])\xED(?![\x80-\xFF])#nordischesoklein#g;

    print $out $_;
    }   
close $out;
close $in;
}

sub convert2unicode {

open(our $in,  "< :encoding(GB18030)", $input)  or die "$!\n";
open(our $out, "> :encoding(UTF-8)",  $output)  or die "$!\n";

print "Convert ASCII to UTF-8\n\n";

while (<$in>) {         
        print $out $_;      
}

close $in;
close $out;
}

sub codes2char {

open(our $in,  "< :encoding(UTF-8)", $input)    or die "$!\n";
open(our $out, "> :encoding(UTF-8)", $output)   or die "$!\n";

print "replace Codes with original characters.\n";


    while (<$in>) {
        s#lidosoumlautklein#\xF6#g;
        s#lidosaumlautklein#\xE4#g;
        s#lidosuumlautklein#\xFC#g;
        ## ... and some more.
        s#eszett#\xDF#g;
        s#uaccentaiguklein#\xFA#g;
        s#oaccentaiguklein#\xF3#g;
        s#iaccentaiguklein#\xED#g;
        s#nordischesoklein#\xF8#g;

        print  $out $_;
    }
close($in)   or die "can't close $input: $!";
close($out)  or die "can't close $output: $!";
}

##################
## Main program ##
##################

&specialchars2codes;
&switch_var;
&convert2unicode;
&switch_var;
&codes2char;

哇,这很长。希望不会太复杂。

编辑

这是上面示例字符串的十六进制转储:

01A36596                                                        2B 41                    +A
01A365A9   75 74 6F 72 3A 0D 0A 2D  59 61 6E 2C 20 4C 69 61  6E 6B 65   utor:  -Yan, Lianke
01A365BC   20 2F 20 D1 D6 C1 AC BF  C6 0D 0A 2B 43 6F 2D 41  75 74 6F    / ÑÖÁ¬¿Æ  +Co-Auto
01A365CF   72 3A 0D 0A 2D 4D 69 6E  2C 20 4A 69 65 20 2F 20  28 9A 62   r:  -Min, Jie / (šb
01A365E2   65 72 73 2E 29 0D 0A                                         ers.)  

and another two to illustrate:

1.

000036B3                                                     2D 52 75                   -Ru
000036C6   E1 6C 61 6E 64 0D 0A                                         áland  

2.

015FE030            2B 54 69 74 65  6C 3A 0D 0A 2D 57 65 6E  72 6F 75      +Titel:  -Wenrou
015FE043   64 75 6E 68 6F 75 20 20  CE C2 C8 E1 B6 D8 BA F1  20 28 47   dunhou  ÎÂÈá¶Øºñ (G
015FE056   65 6E 74 6C 65 6E 65 73  73 20 61 6E 64 20 4B 69  6E 64 6E   entleness and Kindn
015FE069   65 73 73 29 2E 0D 0A                                         ess).  

在这两种情况下,都有十六进制值为E1。在第一种情况下,它代表一个德语sharp-s字母(ß,“Rußland”=“Russia”),而在第二种情况下,它是多字节CJK字符柔 (读作:"rou")的一部分。
在库程序中,中文字符是通过另一个必须首先加载的附加程序输入和显示的。据我所知,该程序与图形驱动程序相连,以低级方式捕获编码的中文字符并将其显示为字符,同时保留其他所有内容。德语umlauts等则由库程序本身处理。
我不完全理解这是如何工作的,即程序如何知道HexE1是要被视为单个字符á,因此根据codepage X进行转换,还是它是多字节字符的一部分,因此根据codepage Y进行转换。
我找到的最接近的近似值是,如果在特殊字符之前或之后有其他特殊字符,则特殊字符很可能是中文字符串的一部分。(例如:ÎÂÈá¶Øºñ)

1
通过查看Windows解码后的字符串,您引入了额外的复杂性。避免这种情况,它会导致您做出错误的假设。提供十六进制转储,它们不会产生歧义。š是CP850编码,视为Windows-1252。ÑÖÁ¬¿Æ是GB18030编码,视为Windows-1252。 - daxim
回答已经修正,附带十六进制转储和进一步的澄清。 - screen12345
那应该是这样写的:“问题已经附带十六进制转储和更多澄清信息。” - screen12345
1个回答

2
  1. 如果混合编码的每行/记录/字段/任何内容都是一致的编码,您可以逐个读取和转换每行/记录/字段/任何内容。但这似乎不是这种情况。
  2. 这不是一个坏主意。
  3. UTF-8通常不使用BOM,尽管如果您真的想尝试输出字符U+FEFF(在UTF-8中,这是3个字节ef bb bf)。最好的方法是找出为什么NP++错误地检测到文件。
  4. 当读取UTF-8编码的文件时,使用UTF-8输入层打开它是一个好主意。如果您愿意,<:utf8< :encoding(UTF-8)的缩写等效形式。

至于原始混乱的工作方式,似乎“附加程序”只是将任何看起来像汉字的东西转换成中文,并保留其他内容(标准驱动程序然后使用欧洲编码显示),而“库程序”只是输出接收到的任何代码。因此,转换文件的更直接的方法可能是模仿这一点:使用:encoding(latin-1)(或其他)读取文件,然后替换中文字符(例如s/\xc8\xe1/柔/)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接