有道离线字典拆解分析

There's more than one way to do it!

有道离线字典拆解分析

帖子523066680 » 2016-06-26 8:42

去年就开始拆了,发现译文信息里一部分翻译是明文,一部分又是给出了另一个字典的索引
要自己挖,还分层的挖,烦了就停了。今年想起来又接着搞,挖成明文后对数据格式化,改
成Perl的hash/array复合结构。分成了五六个脚本吧,拆解部分不一一张贴,主要说方法。

准备
    安装 有道字典增强版 (下载的V6.2),安装目录下会有 localdicts 文件夹,包含字典文
    件以及索引文件。
    代码: 全选
    C:\USERS\%USERNAME%\APPDATA\LOCAL\YOUDAO\DICT\APPLICATION\LOCALDICTS
        2016/06/06  09:20    <DIR>          ..
    2016/06/06  09:20        28,460,334 21EC.ydic
    2016/06/06  09:20         8,094,269 basicCE.ydic
    2016/06/06  09:20        16,692,913 basicEC.ydic
    2016/06/06  09:20         3,737,882 CE.idx
    2016/06/06  09:20               250 CE.ifo
    2016/06/06  09:20         7,991,892 EC.idx
    2016/06/06  09:20               334 EC.ifo
    2016/06/06  09:20         9,900,103 newCE.ydic
    2016/06/06  09:20         5,475,078 phrase.ydic
    2016/06/06  09:20        42,980,352 sentenceDict.sql
    .ifo 文件是摘要信息(比如某个字典有多少单词量)
    .idx 文件分两部分,第一部分是索引数据,第二部分是单词内容
    .ydic 文件是翻译信息
分析

    先针对英汉字典(EC.idx basicCE.ydic)

    比较业余,用了几种方法,就.idx文件来说,已经知道是索引,将其转成16进制按N组一行的方式
    排列(用的Perl提取划分,虽然我知道有些工具更好用),发现其中的规律是,每12个字节为一组
    数据,在一组里面,每4个字节合为一个数值(低位在前,这是后来发现的)。以及按4列16进制值显示后
    发现到了某个位置规律就不一样了。
    代码: 全选
    1057288 16 6a 39 00
    1057289 83 b6 fe 00
    1057290 17 00 00 00
    1057291 1b 6a 39 00
    1057292 9a b6 fe 00
    1057293 17 00 00 00  //以下为第二部分,没有明显规律
    1057294 37 73 71 65
    1057295 63 75 37 73
    1057296 75 60 64 37
    1057297 74 7f 72 75
    1057298 37 74 62 79
    然后对ydic分析字节频率,以下是频率较高的字节,左边是出现的次数
    00337934 ac
    00352074 6d
    00352432 6b
    00378084 22
    00477148 f5
    00563980 2a
    00567693 75
    00692001 21
    00997268 33
    02211802 32

    然而并没有什么卵用,6d 和 6b 33 和 32 是什么? m k 3 2 ?写了一个脚本: 输入16进制值,试转UTF、GBK
    代码: 全选
    input:6d 6b 33 32
    mk32    gbk     109
    mk32    big5    109
    mk32    utf8    109
    歭社    utf16-le        27501
    not utf16-be
    但是好在想起了单词数量,在ifo文件:WordCount:352431
    6b 和 6d的出现次数和这个非常接近,可能作为分隔符。

解码:
    随便指定一个单词,比如 area,用类似二分法的方式,断开网络,对ydic翻译文件对半裁剪。
    用有道词典查询该单词,不能查到就提取另一半/继续对半裁剪,最后定位数据:
    6b 32 60 32 2a 32 37 de a5 d9 89 62 79 d9 89 32 3c 32 75 32 2a 4b 32 7e 3e 33 33 30 f5 9c aa f5 8f 8f ff ac 9c
    f5 8c a0 f5 9c aa ff ac 8b f9 8d b2 f7 b7 bf ff ac 8b f8 9c 93 f5 8b a4
    32 4d 3c 32 75 68 32 2a 32 21 33 22 26 22 29 27
    29 23 26 33 26 20 20 32 3c 32 75 68 32 2a 32 22 33 23 26 23 26 23 29 23 33 23 21 27 23 32 6d

    offset: 0d6130
    length: 0x6c

    将翻译内容:“n. 区域,地区;面积;范围”, 试转为不同编码
    gbk: 6e 2e 20 c7 f8 d3 f2 a3 ac b5 d8 c7 f8 a3 bb c3 e6 bb fd a3 bb b7 b6 ce a7
    big5: 6e 2e 20 3f b0 ec a1 41 a6 61 3f a1 46 ad b1 3f a1 46 ad 53 3f
    utf8: 6e 2e 20 e5 8c ba e5 9f 9f ef bc 8c e5 9c b0 e5 8c ba ef bc 9b e9 9d a2 e7 a7 af ef bc 9b e8 8c 83 e5 9b b4
    utf16-le: 6e 0 2e 0 20 0 3a 53 df 57 c ff 30 57 3a 53 1b ff 62 97 ef 79 1b ff 3 83 f4 56

    总结出:采用UTF8编码,并做了简单的处理,hex的高位如果是奇数,高位-1,否则高位+1 , 还原数据:
    {"p":"'εəriə","e":["n.## 区域,地区;面积;范围"],"ex":"1#26297936#600","ex":"2#3636393#3173"}

    "ex":"1#26297936#600" 表示引用字典 21EC.ydic 的数据,offset 为 26297936,600是数据长度
    "ex":"2#3636393#3173" 表示引用字典 phrase.ydic的数据,offset 为 3636393,3173是数据长度

    用二分法锁定字节区域是碰了运气,因为有很多数据都是引用另一个字典,再绕一点都会烦得不行。

现在回去看idx文件:

好像也是通过对半截取的方式,找到了对应的单词,因为,如果单词或者索引删掉了,会导致搜不到。
(去年探索的,具体忘了。。。)加密方式和译文的一样。

总结:
idx文件,第一部分为索引,索引数据长度 = 352431(单词量) * 12(数据段) = 4229172
4个字节为一个值,低位在前。后面部分为单词,单词部分没有划分符,根据索引进行提取划分,
单词部分采用非常简单的加密:高位奇数时-1,偶数时+1

00 00 00 00 第一单词起点
00 00 00 00 译文起点
16 00 00 00 译文长度
06 00 00 00 第二单词起点
16 00 00 00 译文起点
16 00 00 00 译文长度
0b 00 00 00 。。。
2c 00 00 00
16 00 00 00

拆解
此处略过,因为是不同时间去折腾的,分了多个脚本和步骤,再整理一遍真是烦

转为明文后的字典下载地址

http://523066680.ys168.com/ [临时]文件夹
Youdao_En_Cn_PerlStruct 202016-06.7z
Youdao EN2CN OneFile 2016-06.7z

字典搜索示例
代码: 全选
use Encode;
use YAML 'Dump';

my $PATH = "D:\\Local\\Dict\\Youdao\\Analyse_EC";
open READ,"<:raw", "$PATH\\EN2CN.txt";

my $s;
our $data;

while ($s = <READ>)
{
    if ($s=~/^words :/i)
    {
        $s=~/^(.+) \: (.*)\r?\n/i;
        eval "\$data = $2";
        delete $data->{'pr'};      #不显示“相关词组”信息

        print utf8_to_gbk( Dump($data) );
    }
}

close READ;

sub utf8_to_gbk
{
    return encode('gbk', decode('utf8', $_[0]));
}
显示结果
---
e:
- n. 字(word的复数);话语;言语
- v. 用言语表达(word的三单形式)
p: w?:dz


音标包含一些gbk以外的符号,在终端显示不全
论坛已转移 Code-By.Org 群号 322023604
头像
523066680
版主
 
帖子: 1680
注册: 2012-03-06 15:08

提取字典并生成明文对照表的代码

帖子523066680 » 2016-06-26 15:44

basicCE.ydic, CE.idx, newCE.ydic
重新整合输出明文的完整代码:

Syntax: (Analyse.pl) [ Download ] [ Hide ]
use Encode;
use IO::Handle;
STDOUT->autoflush(1);
STDERR->autoflush(1);

my $SRC = "D:\\Youdao\\Source";  #包含离线字典文件的路径
my $DST = $SRC;                         #输出目录和离线字典位置相同

my $YDIC;
my $IDX;
my $IDX_DT;
my $WRT;
my $EXT1;

open $YDIC,  "<:raw", "$SRC\\basicCE.ydic" or die $!;
open $IDX,   "<:raw", "$SRC\\CE.idx" or die $!;
open $IDXDT, "<:raw", "$SRC\\CE.idx" or die $!;
open $EXT1,  "<:raw", "$SRC\\newCE.ydic" or die "$!";

open $WRT,   ">:raw", "$DST\\CE_plaintext.txt";

my $edge = 176382 * 4 * 3;  #单词量 * 12字节索引信息

seek($IDXDT, $edge, 0);
my @idx;

=idx_format
    (以下每个数值为 LONG 型,包含4个字节)
     0  0 22  第一个词汇起点  译文起点  译文长度
    11 22 34  第二个单词起点  译文起点  译文长度
    18 56 28
    29 84 34
=cut

seek($IDX, 4, 0);  #第一个单词起点信息skip,以第二个起点作为有效数据
my $prev_w = 0;
my $prev_t = 0;
my $buff;
my $word;
my $trans;

do
{
    for (0..2)
    {
        read($IDX, $buff, 4, 0);
        $idx[$_] = unpack "L", $buff;
    }

    read($IDXDT, $word, $idx[2] - $prev_w , 0);
    print $WRT word_decode( $word ),"\t";

    seek($YDIC, $idx[0], 0);
    read($YDIC, $trans, $idx[1], 0);
    $trans = get_ex_trans( word_decode( $trans ) );

    #格式化数据结构
    to_perl_struct( \$trans );

    print $WRT $trans ,"\n";
    $prev_w = $idx[2];
}
until ( tell($IDX) >= $edge - 12 );

=decrease
    8094027     121 1621295
    8094148     121 -1460395742  最后一段译文起点  译文长度  词汇起点内容
    最后一个词汇只有起点信息,没有结束信息,且其本身设计为:和倒数第二词
    汇重复,可以略过
=cut


close $YDIC;
close $IDX;
close $IDXDT;
close $WRT;

#扩展翻译,获取并替换
sub get_ex_trans
{
    my $trans = shift;
    my $buff;
    my $ex_trans;
    my ($info_A, $dict, $offset, $len);

                  #字典代号, offset, 长度
    while ($trans =~/"(\d)#(\d+)#(\d+)"/)   #循环替换,如果出现多个
    {
        ($info_A, $dict, $offset, $len) = ( $&, $1, (sprintf "%08d", $2), $3 );
        seek($EXT1, $offset, 0);
        read($EXT1, $buff, $len, 0);

        $ex_trans = word_decode( $buff );
        $trans=~s/${info_A}/${ex_trans}/;
    }

    ex_format(\$trans);

    return $trans;
}

#清理"ex":{ ... } ,保留花括号内的内容
sub ex_format
{
    my $ref = shift;
    #${$ref}=~s/\"ex\"\:\{(.*)\}/$1/g;
    ${$ref}=~s/##//g;
}


#简单解密
sub word_decode
{
    my $word = shift;
    my $real = "";
    my $code;
    grep
    {
        $code = ord( $_ );
        $real .= chr( $code + !( ($code >> 4) % 2) * 0x20 - 0x10 );
                            #高位 % 2 = 0 或 1 => * 0x20 - 0x10 = 0x10 或 -0x10
    }
    split("", $word);

    return $real;
}

# 转为Perl数据格式
sub to_perl_struct
{
    my $ref = shift;
    #$$ref =~s/\Q},"ex":{\E/, /g;
    $$ref =~s/":/" => /g;
    $$ref =~s/",/", /g;
    $$ref =~s/\'/\\\'/g;
    $$ref =~s/\$/\\\$/g;
    $$ref =~s/\@/\\\@/g;
}

sub xcode {
    $_[1]='
x' if (not defined $_[1]);
    for my $v ( split(//,$_[0]) ) {
        print sprintf ("%l$_[1] ",ord($v));
    }
    print "\n\n";
}
论坛已转移 Code-By.Org 群号 322023604
头像
523066680
版主
 
帖子: 1680
注册: 2012-03-06 15:08

单词查询工具

帖子523066680 » 2016-06-26 15:48

在已经提取字典明文对照表的前提下,可通过以下工具查询某个词汇信息,示例
C:\>ce.pl 单词
[dān cí]
[语] word
seperate word
single word
discrete word

[dān cí]
◎{语} (词) individual word; word (区别于“词组”)
◎(由一个词素构成的词; 单纯词) single-morpheme word (如“茶”、“走”、“蟋蟀”、“玲珑”等, 区别于“合成词”)
◎{计} (字码) word


ce.pl: SELECT ALL
=info
调用参数:输入包含空格的词组时,使用双引号即可
=cut

use Encode;
use YAML 'Dump';

our $PATH = "D:\\Local\\Dict\\Youdao\\Analyse_CE";

open $DICT,"<:raw", "$PATH\\CE_plaintext.txt" or die;

our %hash;
my $word;
my $i = 0;
my $info;
my $dt;
my $first;

$word = defined $ARGV[0] ? gbk_to_utf8( $ARGV[0] ) : "缓";

while ($line = <$DICT>)
{
if ( $line=~/^$word\t(.*)/ ) #如果要显示大小写不同时的翻译结果,加上/i
{
$info = $1;
eval "\$dt = $info";
short_print( $dt );
undef $dt;
}
}
exit;

sub short_print
{
my $ref = shift;
my $indent = " ";

PRINT:
printf "[%s]\n", utf8_to_gbk( $ref->{'p'} ) if (exists $ref->{'p'});
printf "[%s]\n", utf8_to_gbk( $ref->{'w'} ) if (exists $ref->{'w'});
for my $v ( @{ $ref->{'e'} } )
{
print $indent, utf8_to_gbk( $v ),"\n";
}

if (exists $ref->{'ex'})
{
$ref = $ref->{'ex'};
$indent .= " ";
print "\n";
goto PRINT;
}

return;
}

sub utf8_to_gbk
{
return encode('gbk', decode('utf8', $_[0]));
}

sub gbk_to_utf8
{
return encode('utf8', decode('gbk', $_[0]));
}
论坛已转移 Code-By.Org 群号 322023604
头像
523066680
版主
 
帖子: 1680
注册: 2012-03-06 15:08


回到 Perl

在线用户

正在浏览此版面的用户:没有注册用户 和 1 位游客

cron
Not able to open ./cache/data_global.php