輸入法詞庫解析(六)QQ 拼音分類詞庫.qpyd
阿新 • • 發佈:2022-05-27
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 }