偶遇:Java 从 .rft 文件读取生僻字造成乱码


一见未必钟情

这是第一次遇到 .rtf 格式文件乱码问题,之前也处理过类似的问题:从 .rtf 文件中读取字符串,但一直都没有遇到乱码直到今天。

原始文件内容如下:(在 Windows 下用 Word 和 写字板就可以打开)

大小便失禁及心悸、胸闷、发热,亦无头痛,视物旋转,复视,黑曚,意识障碍等症。

原始文件对应的 .rtf 格式字符串序列(用记事本直接打开就可以查看):

{\rtf1\ansi\ansicpg936\deff0\nouicompat\deflang1033\deflangfe2052{\fonttbl{\f0\fnil\fcharset134 \’cb\’ce\’cc\’e5;}}
{*\generator Riched20 6.3.9600}\viewkind4\uc1
\pard\nowidctlpar\qj\kerning2\f0\fs24\’b4\’f3\’d0\’a1\’b1\’e3\’ca\’a7\’bd\’fb\’bc\’b0\’d0\’c4\’bc\’c2\’a1\’a2\’d0\’d8\’c3\’c6\’a1\’a2\’b7\’a2\’c8\’c8\’a3\’ac\’d2\’e0\’ce\’de\’cd\’b7\’cd\’b4\’a3\’ac\’ca\’d3\’ce\’ef\’d0\’fd\’d7\’aa\’a3\’ac\’b8\’b4\’ca\’d3\’a3\’ac\’ba\’da\’95\’e4\’a3\’ac\’d2\’e2\’ca\’b6\’d5\’cf\’b0\’ad\’b5\’c8\’d6\’a2\’a1\’a3\par
}

一般 Java 从 .rtf 文件读取字符串的处理方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.rtf.RTFEditorKit;

public class TestRtf
{


/**
* 从 .rtf 文件中读取字符串。
*
* @param rtfFile
* 文件
*
* @return 获得的字符串
*
* @see {@link #getTextFromRtfFile(File, String)} outputCharset="GB2312"
*/

public static String readFromRtfFile( File rtfFile )
{

return readFromRtfFile( rtfFile, "GB2312" );
}

/**
* 从 .rtf 文件中读取字符串。
*
* @param rtfFile
* 带读取的文件
* @param outputCharset
* 输出字符串的编码
*
* @return 读取到的字符串
*/

public static String readFromRtfFile( File rtfFile, String outputCharset )
{

String result = null;
try
{
DefaultStyledDocument styledDoc = new DefaultStyledDocument();
InputStream is = new FileInputStream( rtfFile );
new RTFEditorKit().read( is, styledDoc, 0 );
result = new String( styledDoc.getText( 0, styledDoc.getLength() ).getBytes( "ISO8859_1" ), outputCharset );
// 提取文本,读取中文需要使用ISO8859_1编码,否则会出现乱码
}
catch( IOException e )
{
e.printStackTrace();
}
catch( BadLocationException e )
{
e.printStackTrace();
}
return result;
}

/**
* 从 Rtf 格式的字符序列中读取字符串。
*
* @param rftString
* Rtf 格式的字符序列
* @param outputCharset
* 输出字符串的编码
*
* @return 输出的字符串
*/

public static String readFromRtfString( String rftString, String outputCharset )
{

String result = null;
try
{
DefaultStyledDocument styledDoc = new DefaultStyledDocument();
InputStream inStream = new ByteArrayInputStream( rftString.getBytes() );

new RTFEditorKit().read( inStream, styledDoc, 0 );

result = new String( styledDoc.getText( 0, styledDoc.getLength() ).getBytes( "ISO8859_1" ), outputCharset );
}
catch( IOException e )
{
e.printStackTrace();
}
catch( BadLocationException e )
{
e.printStackTrace();
}
return result;
}

/**
* 从 Rtf 格式的字符序列中读取字符串。
*
* @param rftString
* Rtf 格式的字符序列
*
* @return 输出的字符串
*
* @see {@link #readFromRtfString(String, String)}
*/

public static String readFromRtfString( String rftString )
{

return readFromRtfString( rftString, "GB2312" );
}

}

然后加上 main 方法进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main( String[] args )
{

String rtfString = "{\\rtf1\\ansi\\ansicpg936\\deff0\\nouicompat\\deflang1033\\deflangfe2052{\\fonttbl{\\f0\\fnil\\fcharset134 \\'cb\\'ce\\'cc\\'e5;}}"
+ "{\\*\\generator Riched20 6.3.9600}\\viewkind4\\uc1 "
+ "\\pard\\nowidctlpar\\qj\\kerning2\\f0\\fs24\\'b4\\'f3\\'d0\\'a1\\'b1\\'e3\\'ca\\'a7\\'bd\\'fb\\'bc\\'b0\\'d0\\'c4\\'bc\\'c2\\'a1\\'a2\\'d0\\'d8\\'c3\\'c6\\'a1\\'a2\\'b7\\'a2\\'c8\\'c8\\'a3\\'ac\\'d2\\'e0\\'ce\\'de\\'cd\\'b7\\'cd\\'b4\\'a3\\'ac\\'ca\\'d3\\'ce\\'ef\\'d0\\'fd\\'d7\\'aa\\'a3\\'ac\\'b8\\'b4\\'ca\\'d3\\'a3\\'ac\\'ba\\'da\\'95\\'e4\\'a3\\'ac\\'d2\\'e2\\'ca\\'b6\\'d5\\'cf\\'b0\\'ad\\'b5\\'c8\\'d6\\'a2\\'a1\\'a3\\par"
+ "}";

System.out.println( readFromRtfString( rtfString ) );
System.out.println( readFromRtfString( rtfString, "GBK" ) );
System.out.println( "===============================================" );
System.out.println( readFromRtfFile( new File( "C:\\Users\\Leo\\Desktop\\1.rtf" ) ) );
System.out.println( readFromRtfFile( new File( "C:\\Users\\Leo\\Desktop\\1.rtf" ), "GBK" ) );
}

// 输出结果如下:
// -------------------------下面是输出结果----------------------------------
// 大小便失禁及心悸、胸闷、发热,亦无头痛,视物旋转,复视,黑洌?意识障碍等症。
//
// 大小便失禁及心悸、胸闷、发热,亦无头痛,视物旋转,复视,黑洌馐墩习戎ⅰ?
//
// ===============================================
// 大小便失禁及心悸、胸闷、发热,亦无头痛,视物旋转,复视,黑洌?意识障碍等症。
//
// 大小便失禁及心悸、胸闷、发热,亦无头痛,视物旋转,复视,黑洌馐墩习戎ⅰ?

哎呀,天呐,乱码出现了!My God!

山重水复疑无路

细心的你也许看到了方法中的注释,读取中文的时候要使用 ISO8859_1 编码读取,实验证明确实如此(哎呀,原来是中文大神!)。输出编码也许你会说我只用了 GBK、GB2312 编码,还没有使用 UTF-8 编码,那么我告诉你 UTF-8 更惨(惨目忍睹啊!)。

比较原始文件和输出日志,你应该发现了,原来是“曚”这个字捣的鬼。思路有了,查看下“曚”的 GBK 编码,发现编码为 95E4 (为啥不查 GB2312,额 (⊙o⊙)… 因为 GB2312 编码中不包含这个字,^O^ 。),那我们只要根据 rtf 文件编码格式来核查下“曚”这个字的编码(嘿嘿 ~~~),就知道问题出现在哪儿。在【原始文件对应的 .rtf 格式字符串序列】中确定了这个字的编码为 \'95\'e4,尼玛,这不就是 GBK 编码格式吗,为啥还乱码。

凌乱中。。。。。。

柳暗花明又一村

凌乱了 n 久,已经接近要放弃治疗了的那一刻,我突然想到了我们是以 ISO8859_1 编码来读取的,以 GBK 或 GB2312 编码输出的。那是不是在以 ISO8859_1 读取的过程中出现了问题?
敢想敢干才是真本事,我们将“曚”的 16 进制编码 \'95\'e4 中的 95 转换成十进制数,转换后是 149 。到维基百科 ISO_8859-1 上面一查,发现了为【未建立】(英文为:nicht belegt)。啊哈哈,问题就在这里了。那对于未建立的编码,str.getBytes( "ISO8859_1" ) 会如何处理呢,答案是忽略掉,对,直接忽略掉 \'95 , 下面我们验证下,怎么验证,只需要修改下 readFromRtfString( String rftString, String outputCharset ) 方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* 从 Rtf 格式的字符序列中读取字符串(输出中间态)。
*
* @param rftString
* Rtf 格式的字符序列
* @param outputCharset
* 输出字符串的编码
*
* @return 输出的字符串
*/

public static String readFromRtfString( String rftString, String outputCharset )
{

String result = null;
try
{
DefaultStyledDocument styledDoc = new DefaultStyledDocument();
InputStream inStream = new ByteArrayInputStream( rftString.getBytes() );

new RTFEditorKit().read( inStream, styledDoc, 0 );

try
{
for( byte b : styledDoc.getText( 0, styledDoc.getLength() ).getBytes( "ISO8859_1" ) )
{
System.out.printf( "\\\'%x", b );

}
}
catch( UnsupportedEncodingException e )
{
e.printStackTrace();
}

System.out.println();

result = new String( styledDoc.getText( 0, styledDoc.getLength() ).getBytes( "ISO8859_1" ), outputCharset );
}
catch( IOException e )
{
e.printStackTrace();
}
catch( BadLocationException e )
{
e.printStackTrace();
}
return result;
}


public static void main( String[] args )
{

String rtfString = "{\\rtf1\\ansi\\ansicpg936\\deff0\\nouicompat\\deflang1033\\deflangfe2052{\\fonttbl{\\f0\\fnil\\fcharset134 \\'cb\\'ce\\'cc\\'e5;}}"
+ "{\\*\\generator Riched20 6.3.9600}\\viewkind4\\uc1 "
+ "\\pard\\nowidctlpar\\qj\\kerning2\\f0\\fs24\\'b4\\'f3\\'d0\\'a1\\'b1\\'e3\\'ca\\'a7\\'bd\\'fb\\'bc\\'b0\\'d0\\'c4\\'bc\\'c2\\'a1\\'a2\\'d0\\'d8\\'c3\\'c6\\'a1\\'a2\\'b7\\'a2\\'c8\\'c8\\'a3\\'ac\\'d2\\'e0\\'ce\\'de\\'cd\\'b7\\'cd\\'b4\\'a3\\'ac\\'ca\\'d3\\'ce\\'ef\\'d0\\'fd\\'d7\\'aa\\'a3\\'ac\\'b8\\'b4\\'ca\\'d3\\'a3\\'ac\\'ba\\'da\\'95\\'e4\\'a3\\'ac\\'d2\\'e2\\'ca\\'b6\\'d5\\'cf\\'b0\\'ad\\'b5\\'c8\\'d6\\'a2\\'a1\\'a3\\par"
+ "}";

System.out.println( rtfString );
readFromRtfString( rtfString, "GB2312" );

}

// 输出结果如下【System.out.println( rtfString )的输出】稍微做了处理,为了显示进行了断行处理:
// -------------------------下面是输出结果----------------------------------
//
// \'b4\'f3\'d0\'a1\'b1\'e3\'ca\'a7\'bd\'fb\'bc\'b0\'d0\'c4\'bc\'c2\'a1\'a2\'d0\'d8\'c3\'c6\'a1\'a2\'b7\'a2\'c8\'c8\'a3\'ac
// \'d2\'e0\'ce\'de\'cd\'b7\'cd\'b4\'a3\'ac\'ca\'d3\'ce\'ef\'d0\'fd\'d7\'aa\'a3\'ac\'b8\'b4\'ca\'d3\'a3\'ac\'ba\'da\'95\'e4\'a3
// \'ac\'d2\'e2\'ca\'b6\'d5\'cf\'b0\'ad\'b5\'c8\'d6\'a2\'a1\'a3
//
// \'b4\'f3\'d0\'a1\'b1\'e3\'ca\'a7\'bd\'fb\'bc\'b0\'d0\'c4\'bc\'c2\'a1\'a2\'d0\'d8\'c3\'c6\'a1\'a2\'b7\'a2\'c8\'c8\'a3\'ac
// \'d2\'e0\'ce\'de\'cd\'b7\'cd\'b4\'a3\'ac\'ca\'d3\'ce\'ef\'d0\'fd\'d7\'aa\'a3\'ac\'b8\'b4\'ca\'d3\'a3\'ac\'ba\'da\'e4\'a3
// \'ac\'d2\'e2\'ca\'b6\'d5\'cf\'b0\'ad\'b5\'c8\'d6\'a2\'a1\'a3

可以在输出中清楚的看到 \'95 被直接被忽略掉了,这也难怪后面持续显示乱码了。

究极还是纠结

怎么处理呢?

方法一:替换生僻字为常用字,比如将“曚”替换成“蒙”,对应的编码替换为将 \'95\'e4 替换成 \'c3\'c9
缺点:太明显了,那么多生僻字怎么替换的完。

方法二:自己解析 .rtf 格式的字符序列,然后存为 byte 数组。然后用 GBK 编码解析。
缺点:可能要多花一点时间。