【已上传】《汉语大词典》2.0 光盘版 软件内提取数据

这两天因为一些检索任务,来论坛查询汉语大词典2.0的电子版,看能不能抄近道不用去软件一点一点复制粘贴,查了一些帖子发现目前最近的最全的数据似乎是22年10月的那版,往下一翻竟然是手动复制粘贴的,让我非常感动。也有很多人在对数据做着自己的努力,所以我也想贡献自己的力量。近日我结合前人分析了软件的压缩(其实是混淆编码)方法,解出了软件数据库的内容,包含了下划线位置的数据,不知道现在是否站点内还有人需要数据与数据提取方法,我先放张图上来。第一次发帖,多有冒犯还请各位海涵~


18 个赞

我个人觉得光盘的数据不必再做了,因为原始数据本来就残缺不全,有很多问题。你真喜欢那个光盘,可以用feiwu复制出来做的mdx。

你真有兴趣整理,可以利用方正版的文本去增补2.0光盘版mdx(我认为是目前最好的mdx,当然,问题仍然多如牛毛,乱似乱麻)。

当然,假如你的目的是研究提取光盘技术,那另当别论。

但是,光盘都过时了,现在也没什么光盘可以提取了,所以研究了技术,可能也成为屠龙之技了。

感谢,很需要,特别是原始数据,目前论坛里的数据全是加工过的。

好吧,主要是当时看到https://forum.freemdict.com/t/topic/7883/96前辈折腾实在是痛苦,想要帮他完成这个念想,以及见到对下划线之讨论https://forum.freemdict.com/t/topic/10567/2?u=naosei7dd8,不知道现在到底多少人还想要带下划线的数据。既然有更优秀的数据,那是更好的,不知道可否在此记录一下解数据的代码,也作为探索过的痕迹

1 个赞

目前是需要什么格式的呢?sqlite可以吗?

站长其实最鼓励讨论技术了。不过,讨论技术在别的分类。“汉汉”是发mdx词典用的。

last_idol、amob看来对这个兴趣还是很浓厚,你不妨把数据做了,也发了。我是真研究《汉语大词典》的,反而觉得不需要。

真研究技术的,可能是想看你怎样提取。虽然没有新光盘了,还是有一些旧的英文词典光盘存货的。

2 个赞

发发逆向教程,很好,网上几乎找不到词典这块的,冷门。有一堆二进制古早电子辞典系统数据,不会提取。

1 个赞

什么格式都可以,只要你方便,只要能保留完整的原始数据就行。

我是从技术角度看,手工修改后的数据,导致HTML的结构有很多瑕疵,不利于结构化提取,二次处理数据。

简单的逆向倒是还好,只要开发者不是专业搞计算机技术的以及有专业的反逆向心思的都比较好破。以及对应的代码、数据文件、方法我都在这个帖子里发吗?还是拆开,这个帖子发文件,然后在哪个版发帖子写逆向方法+代码,第一次来这个论坛不是很清楚

3 个赞

你随意,论坛没有严格的版规。

你假如不发mdx文件,把分类改成“资源分享”,附件是数据包,下面可以尽管讨论怎样提取,没有问题。

直接在这个贴子发就行,分类改成技术交流与词典编修。

本论坛就一个版块,是否另发帖子都是可以的。帖子分类什么的不重要,后面自己弄清情况了再修改个合适的就好。
乐于见到技术探讨的帖子。last_idol 兄是懂技术的

多一份数据自然是好事,可以比较看看

确实,很多MDX渲染后显示没问题甚至完美,但是html标签嵌套结构混乱,有的时候是修改者没搞清楚整体结构的情况下正则批量替换的结果。不仅仅是不利于结构化提取,有时改css用first-child / last-child 改了一通不起作用,最后发现是html标签嵌套问题,没法改,直接放弃了

1 个赞

好的,我先整理一下

1 个赞

汉语大词典 2.0 光盘版 提取全过程

数据

汉语大词典 2.0 光盘版 解密数据 共563M

在提取前将论坛中abs前辈发的iso和我手中的iso做比较,在数据部分(*.FPT+*.DAT)校验了哈希一致性,因此理论上用同一套代码提取的数据也是一样的。

其中只解密了:

· HYDC1(字音字义组词举例)

· HYDC2(字音字义以及所有字典词信息与详细解释,即此处网盘的"汉语大词典源数据合并"部分)

· HYDC8(看着像成语与详细解释)

· HYDC9(二字词语与详细解释)

· HYDCF(三字词语与详细解释)

· HYDCG(四字及以上词语与详细解释)

剩下的数据没看懂是什么含义,就没有解密。光盘版的这个软件功能很多很全,可以通过各种方式检索字,估计其他的库里就放着注音、偏旁部首、笔画等信息,有兴趣的可以自己解密。

再转换过程中,存在一些问题,其中HYDC2存在多条空白记录(不确定是什么作用,估计可能因为当时光盘软件制作时录入的问题),其他部分也有编码失败的问题,分析了其中一个例子,应该是光盘或软件制作时就有的问题(比如一个字节变成空格),软件版查时也不能正确编码。我在代码里指定如果编码错误则将对应字节用"backslashreplace"方法,如"\x78",全局搜索\x即可找到。

因此,记录错误有两种方式体现,一种是\x,一种是空白记录,这也许会给这版本词典修订提供一些线索。

如果不关心技术只关心数据看完这就可以划走了。

基础背景

这里是来自abs前辈发现的成果

首先根据上述的成果反编译出汉语大词典2.0的代码,以及数据库相关的知识abs前辈也已经补充上了。只不过有一点,还原数据看上去并不需要SSS文件(即索引),索引是类似于查询加速的作用,有了索引就不用每一条遍历地查询,因此并不需要SSS文件,只要有全套FPT和DAT就可以了。

前辈卡在了VfpDecompress上,以及下面回复的linbai前辈提到VfpDeCompress 和 VfpCompress函数是olefx32.dll的内置函数。然而网上并没有关于这两个函数的资料,因此直接IDA启动!

代码

import codecs
from dbfread import DBF
import sqlite3
import os
from tqdm import tqdm


# 辅助函数:向左和向右旋转一个字节
def rol(byte, shift):
    return ((byte << shift) & 0xff) | (byte >> (8 - shift))


def ror(byte, shift):
    return (byte >> shift) | ((byte << (8 - shift)) & 0xff)


def is_bytearray_all_digits(byte_array):
    # 定义ASCII码的数字范围
    ASCII_ZERO = 48
    ASCII_NINE = 57

    # 检查bytearray中的每个元素是否为ASCII码表示的数字
    return all(ASCII_ZERO <= byte <= ASCII_NINE for byte in byte_array)


# 自定义编解码器类
class CustomCodec(codecs.Codec):

    # 初始化随机数生成器的状态
    def __init__(self):
        self.v2 = 1  # initial state for the LCG

    # 编码方法
    def encode(self, input, errors='strict'):
        return input.encode("utf-8"), len(input)

    # 解码方法
    def decode(self, input, errors='strict'):
        # print(bytearray(input)[0])
        if bytearray(input)[0] == ord("F"):
            result = bytearray(input).decode("gb18030")
            return result, len(result)
        if is_bytearray_all_digits(bytearray(input)):
            result = bytearray(input).decode("gb18030")
            return result, len(result)
        try:
            result = decode_HYDC(bytearray(input))
            result = result.decode("gb18030")
            return result, len(result)
        except UnicodeDecodeError:
            # print(bytearray(input))
            result = bytearray(input).strip().decode("gb18030",errors='backslashreplace')
            return result, len(result)


def decode_HYDC(input):
    v2 = 1
    data = input
    # 确保在数据的末尾有一个 null 字节(0)
    data.append(0)
    # 数据的总长度
    total_length = len(data) - 1
    if total_length > 0:
        tmp_length = total_length
        while tmp_length > 0:
            # 执行循环左移操作
            data[total_length - tmp_length] = rol(data[total_length - tmp_length], 3)
            # 更新 v2 的值
            v2 = (1199 * v2 + 19782) % 0x1DF0DD
            # 更新数据字节
            data[total_length - tmp_length] ^= int((v2 * 0.0000045866768) // 1)
            # 移动到下一个字节
            tmp_length -= 1
    # 移除末尾的 null 字节并返回去混淆后的字节串
    result = bytes(data[:-1])
    return result


# 注册编解码器
def custom_codec_search_function(encoding):
    if encoding == 'customcodec':
        return codecs.CodecInfo(
            name='customcodec',
            encode=CustomCodec().encode,
            decode=CustomCodec().decode,
        )
    return None


class SQLiteDBHandler:
    def __init__(self, db_file):
        self.db_file = db_file
        self.conn = None
        self._create_connection()

    def _create_connection(self):
        """创建数据库连接"""
        try:
            self.conn = sqlite3.connect(self.db_file)
        except sqlite3.Error as e:
            print(e)

    def _create_table(self, data_dict, table_name):
        """根据dict的键创建表"""
        columns = ", ".join([f"{key} TEXT" for key in data_dict.keys()])
        create_table_sql = f"CREATE TABLE IF NOT EXISTS {table_name} ({columns});"

        try:
            cursor = self.conn.cursor()
            cursor.execute(create_table_sql)
        except sqlite3.Error as e:
            print(e)

    def _insert_values(self, data_dict, table_name):
        """插入字典数据到表"""
        placeholders = ", ".join(["?"] * len(data_dict))
        columns = ", ".join(data_dict.keys())
        values = list(data_dict.values())

        insert_sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"

        try:
            cursor = self.conn.cursor()
            cursor.execute(insert_sql, values)
            self.conn.commit()
        except sqlite3.Error as e:
            print(e)

    def close_connection(self):
        """关闭数据库连接"""
        if self.conn:
            self.conn.close()

    def write_dict_to_table(self, table_name, data_dicts):
        """将字典数据写入到指定的SQLite表"""
        if not data_dicts:
            print("No data provided to write to the table.")
            return

        # 假设所有字典在data_dicts中具有相同的键
        first_dict = data_dicts[0]
        if self.conn is not None:
            self._create_table(first_dict, table_name)
            for data_dict in data_dicts:
                self._insert_values(data_dict, table_name)
        else:
            print("Connection not established.")


def check_and_process_dict(d, keys_to_check):
    """
    检查并处理字典中指定键的值,如果是数字则调用process函数处理。

    :param d: 待处理的字典。
    :param keys_to_check: 需要检查的键的列表。
    :return: None,字典将直接被修改。
    """
    for key in keys_to_check:
        value = str(d.get(key, ''))  # 确保值是字符串类型
        if value.isdigit():  # 如果值是数字
            d[key] = decode_HYDC(bytearray(value.encode("gb18030"))).decode("gb18030")  # 调用处理函数
    return d


def find_dbf_files(directory):
    dbf_files = []
    # os.walk遍历目录
    for root, dirs, files in os.walk(directory):
        for file in files:
            if file.lower().endswith('.dat'):  # 检查文件后缀是否为.dbf
                relative_path = os.path.relpath(os.path.join(root, file), ".")
                dbf_files.append(relative_path)  # 将相对路径添加到列表
    return dbf_files


# content_key_list = ["F1", "F3", "F6", "F7", "F9", "F11"]
HYDC1_key_list = ["F1", "F8"]
HYDC2_key_list = ["F1", "F4", "F5"]
HYDC8_key_list = ["F1", "F3", "F6", "F7", "F9", "F11"]
HYDC9_key_list = ["F1", "F3", "F6", "F7"]
HYDCF_key_list = ["F1", "F3", "F6", "F7", "F9"]
HYDCG_key_list = ["F1", "F3", "F6", "F7", "F9", "F11"]


def extract(name, key_list):
    db_handler = SQLiteDBHandler(f'{name}.db')
    db_handler.write_dict_to_table(name,
                                   [check_and_process_dict(dict(record), key_list)
                                    for record in DBF(os.path.join("HYDC", f"{name}.DAT"), encoding="customcodec")]
                                   )
    db_handler.close_connection()


if __name__ == '__main__':
    codecs.register(custom_codec_search_function)

    pbar = tqdm(total=6)
    extract("HYDC1", HYDC1_key_list)
    pbar.update(1)
    extract("HYDC2", HYDC2_key_list)
    pbar.update(1)
    extract("HYDC8", HYDC8_key_list)
    pbar.update(1)
    extract("HYDC9", HYDC9_key_list)
    pbar.update(1)
    extract("HYDCF", HYDCF_key_list)
    pbar.update(1)
    extract("HYDCG", HYDCG_key_list)
    pbar.update(1)

逆向部分

我用了ida提取了olefx32.dll中VfpDecompress的函数逻辑,拉到gpt4里写的python解密代码,并用ollydbg(毕竟是32位,吾爱的od比x64dbg好用)动态调试验证…这里的工作其实就不是什么写出来就有的普适的经验,我先把数据+脚本发出来,逆向过程我考虑一下怎么写比较好

13 个赞

我觉得可以,有什么需要做逆向还原数据的都可以叫我,只要有空就会看,毕竟我觉得数据解出来会更好的向更多人传递知识,以及可以极大的减少大家工作的负担

2 个赞

HYDC2表中的两个字段F4和F5,是一样的数据吗?

查看数据文件,可以使用Sqlite Browser导出成CSV文件,任意文本编辑器可打开。

菜单-文件-导出-导出表到CSV文件

2 个赞