1. 程式人生 > 其它 >輸入法詞庫解析(六)QQ 拼音分類詞庫.qpyd

輸入法詞庫解析(六)QQ 拼音分類詞庫.qpyd

qpyd 格式的難點主要是碼錶經過了 zlib 壓縮,解壓後的資料很好解析。

原始檔案

0x38 後跟的 4 位元組表示壓縮資料開始的位元組。

0x44 後跟的 4 位元組表示詞條數。

壓縮的資料

使用了 zlib 格式。

golang 解壓 zlib :

	// 解壓資料
	zrd, err := zlib.NewReader(r)
	if err != nil {
		log.Panic(err)
	}
	defer zrd.Close()
	buf := new(bytes.Buffer)
	buf.Grow(r.Len())
	_, err = io.Copy(buf, zrd)
	if err != nil {
		log.Panic(err)
	}

我們看看解壓後的資料是什麼形式

可以發現它分為兩部分,前部分每 10 個一組,總長 10*詞條數。

放到文字編輯器裡分析一下,這裡取了前後兩部分前三條。

可以看到前部分是編碼長和詞長資訊,後半部分 ascii 的編碼 + utf-16le 的詞條。

詳解

前半部分儲存了所有詞條的編碼長,詞長,索引位置。

佔用位元組數 描述
1 拼音的長度
1 詞位元組長
4 未知,全是00 00 80 3F
4 詞條的索引位置

後半部分就是詞條本身了,拼音和詞,詞條之間都沒有多餘位元組。

前面是編碼,框裡的是詞。

程式碼實現


func ParseQqQpyd(rd io.Reader) []Pinyin {
	ret := make([]Pinyin, 0, 1e5)
	data, _ := ioutil.ReadAll(rd)
	r := bytes.NewReader(data)

	// utf-16le 轉換器
	decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()

	// 0x38 後跟的是壓縮資料開始的偏移量
	r.Seek(0x38, 0)
	tmp := make([]byte, 4)
	r.Read(tmp)
	startZip := bytesToInt(tmp)
	// 0x44 後4位元組是詞條數
	r.Seek(0x44, 0)
	r.Read(tmp)
	dictLen := bytesToInt(tmp)
	// 0x60 到zip資料前的一段是一些描述資訊
	r.Seek(0x60, 0)
	head := make([]byte, startZip-0x60)
	r.Read(head)
	b, _ := decoder.Bytes(head)
	fmt.Println(string(b))

	// 解壓資料
	zrd, err := zlib.NewReader(r)
	if err != nil {
		log.Panic(err)
	}
	defer zrd.Close()
	buf := new(bytes.Buffer)
	buf.Grow(r.Len())
	_, err = io.Copy(buf, zrd)
	if err != nil {
		log.Panic(err)
	}
	// 解壓完了
	r.Reset(buf.Bytes())

	for i := 0; i < dictLen; i++ {
		// 讀碼長、詞長、索引
		addr := make([]byte, 10)
		r.Read(addr)
		idx := bytesToInt(addr[6:]) // 後4位元組是索引
		r.Seek(int64(idx), 0)       // 指向索引
		codeSli := make([]byte, addr[0])
		r.Read(codeSli)
		wordSli := make([]byte, addr[1])
		r.Read(wordSli)
		wordSli, _ = decoder.Bytes(wordSli)
		ret = append(ret, Pinyin{string(wordSli), strings.Split(string(codeSli), "'"), 1})
		// 指向下一條
		r.Seek(int64(10*(i+1)), 0)
	}
	return ret
}