Repository: metowolf/qqwry.dat Branch: main Commit: 83ff6a8a0a13 Files: 8 Total size: 22.1 KB Directory structure: gitextract_ujgryagc/ ├── .github/ │ └── workflows/ │ └── release.yml ├── .gitignore ├── README.md ├── package.json ├── protocol.md ├── src/ │ ├── build.js │ └── packer.js └── version.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: workflow_dispatch: schedule: - cron: '15 8,20 * * *' jobs: build: runs-on: ubuntu-latest steps: - uses: pnpm/action-setup@v4 with: version: 9 - name: Checkout uses: actions/checkout@v3 - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOWNLOAD_TOKEN: ${{ secrets.DOWNLOAD_TOKEN }} CZDB_TOKEN: ${{ secrets.CZDB_TOKEN }} run: | pnpm i pnpm run build ================================================ FILE: .gitignore ================================================ dist/ node_modules/ temp/ ================================================ FILE: README.md ================================================ 纯真 IP 数据库自动同步仓库 # 使用说明 下载最新版本 ``` wget https://github.com/metowolf/qqwry.dat/releases/latest/download/qqwry.dat ``` 获取最新版本号 YYYYMMDD 格式 ``` curl https://raw.githubusercontent.com/metowolf/qqwry.dat/main/version.json | jq -r .latest ``` ================================================ FILE: package.json ================================================ { "name": "qqwry.dat", "version": "1.0.0", "type": "module", "repository": "git@github.com:metowolf/qqwry.dat.git", "author": "metowolf ", "license": "MIT", "scripts": { "build": "node src/build.js" }, "devDependencies": { "@ipdb/czdb": "^0.0.2", "execa": "^9.5.2", "iconv-lite": "^0.6.3", "lib-qqwry": "^1.3.4" } } ================================================ FILE: protocol.md ================================================ # File Structure All integers are stored in little-endian format. ``` ┌─────────────────────────────────┐ │ File Header │ 8 bytes ├─────────────────────────────────┤ │ Record Zone │ Variable length ├─────────────────────────────────┤ │ Index Zone │ 7 bytes × n entries └─────────────────────────────────┘ ``` ## File Header Detail ``` ┌───────────────┬───────────────┐ │ First Index │ Last Index │ │ Offset │ Offset │ │ 4 bytes │ 4 bytes │ └───────────────┴───────────────┘ ``` ## Record Zone Entry Detail All strings are GBK encoded and null-terminated. ``` ┌──────────┬───────────┬──────────┐ │ End IP │ Country │ Area │ │ 4 bytes │ Variable │ Variable │ └──────────┴───────────┴──────────┘ A. Direct String: ┌─────────────┬───┐ │ String │ 0 │ └─────────────┴───┘ B. Redirect Mode 1 (0x01): ┌────┬───────────┐ │ 01 │ Offset │ → Points to [Country][Area] └────┴───────────┘ 3 bytes C. Redirect Mode 2 (0x02): ┌────┬───────────┐ │ 02 │ Offset │ └────┴───────────┘ 3 bytes ``` ## Index Zone Entry Detail ``` ┌──────────────┬─────────────┐ │ Start IP │ Offset │ │ 4 bytes │ 3 bytes │ └──────────────┴─────────────┘ Points to Record Zone ``` ## Link - https://web.archive.org/web/20140423114336/http://lumaqq.linuxsir.org/article/qqwry_format_detail.html ================================================ FILE: src/build.js ================================================ import fs from 'fs' import { execa } from 'execa' import libqqwry from 'lib-qqwry' import Decoder from '@ipdb/czdb' import QQWryPacker from './packer.js' const DOWNLOAD_TOKEN = process.env.DOWNLOAD_TOKEN const CZDB_TOKEN = process.env.CZDB_TOKEN const download = async () => { const url = `https://www.cz88.net/api/communityIpAuthorization/communityIpDbFile?fn=czdb&key=${DOWNLOAD_TOKEN}` await fs.promises.mkdir('./temp', { recursive: true }) await execa('wget', ['--timeout=30', '--tries=3', '-O', './temp/download.zip', url]) // 解压 await execa('unzip', ['./temp/download.zip', '-d', './temp']) } const extract = async () => { const qqwryPacker = new QQWryPacker() const decoder = new Decoder('./temp/cz88_public_v4.czdb', CZDB_TOKEN) decoder.dump(info => { const { startIp, endIp, regionInfo } = info // 过滤 IPv6 if (startIp.includes(':')) { return } // 分离 geo, isp const [geo, isp] = regionInfo.split('\t', 2) // 生成记录 qqwryPacker.insert(startIp, endIp, geo, isp) }) // 生成二进制文件 const buffer = qqwryPacker.build() await fs.promises.mkdir('./dist', { recursive: true }) fs.writeFileSync('./dist/qqwry.dat', buffer) } const parseQQwryInfo = async () => { const qqwry = libqqwry(true, './dist/qqwry.dat') const info = { count: 0, unique: 0, } const unique = new Set() let ip = '0.0.0.0' while (true) { let data = qqwry.searchIPScope(ip, ip)[0] // stat info.count += 1 const hashkey = `${data.Country}${data.Area}` if (!unique.has(hashkey)) { info.unique += 1 unique.add(hashkey) } if (data.endIP === '255.255.255.255') break ip = libqqwry.intToIP(data.endInt + 1) } return info } const readInfo = () => { const data = fs.readFileSync('./version.json', 'utf-8') return JSON.parse(data) } const parseQQWryVersion = () => { const qqwry = libqqwry(true, './dist/qqwry.dat') const info = qqwry.searchIP('255.255.255.255') return info.Area.match(/(\d+)/gi).join('') } const release = async () => { const info = await readInfo() const currentVersion = parseQQWryVersion() if (info.latest === currentVersion || info.versions[currentVersion]) { console.log('No new version, skip') return } const currentInfo = await parseQQwryInfo() if (!info.versions[currentVersion]) { info.versions[currentVersion] = currentInfo if (info.latest < currentVersion) { info.latest = currentVersion } fs.writeFileSync('./version.json', JSON.stringify(info, null, 2)) console.log({ info, currentVersion, currentInfo }) await execa('gh', ['release', 'create', currentVersion, '-t', currentVersion, '-n', `QQWry version: ${currentVersion}`, './dist/qqwry.dat']) await execa('git', ['config', 'user.name', 'github-actions']) await execa('git', ['config', 'user.email', 'i@i-meto.com']) await execa('git', ['add', './version.json']) await execa('git', ['commit', '-m', `chore: update version info to ${currentVersion}`]) await execa('git', ['push']) } } const main = async () => { // 0. 下载 czdb 并解压 await download() console.log('Downloaded') // 1. 反解压 czdb 并生成 qqwry.dat await extract() console.log('Extracted') // 2. 生成版本信息 await release() console.log('Released') } main() ================================================ FILE: src/packer.js ================================================ import iconv from 'iconv-lite' class QQWryPacker { constructor() { this.indexList = [] // IP索引区 this.recordList = [] // 记录区 this.ipTree = new Map() // IP树 this.stringCache = new Map() // 字符串缓存 this.maxRecordOffset = 8 } // 插入一条IP记录 insert(startIP, endIP, country, area) { const startIPInt = this._ipToInt(startIP) const endIPInt = this._ipToInt(endIP) const geoOffset = this.maxRecordOffset this._createRecord(endIPInt, country, area || 'CZ88.NET') this.ipTree.set(startIPInt, { endIPInt, geoOffset, country, area: area || 'CZ88.NET' }) } // 生成最终的二进制文件 build() { // 1. 构造数据区 const recordBuffer = Buffer.concat(this.recordList) // 2. 构造索引区 const sortedIPs = Array.from(this.ipTree.keys()).sort((a, b) => a - b) const indexList = [] for (let i = 0; i < sortedIPs.length; i++) { const startIPInt = sortedIPs[i] const { geoOffset } = this.ipTree.get(startIPInt) const indexRecord = Buffer.alloc(7) indexRecord.writeUInt32LE(startIPInt, 0) if (geoOffset > 0xFFFFFF) { throw new Error('Offset overflow') } indexRecord.writeUInt8((geoOffset >> 0) & 0xFF, 4) indexRecord.writeUInt8((geoOffset >> 8) & 0xFF, 5) indexRecord.writeUInt8((geoOffset >> 16) & 0xFF, 6) indexList.push(indexRecord) } // 3. 构造文件头 const headerBuffer = Buffer.alloc(8) headerBuffer.writeUInt32LE(8 + recordBuffer.length, 0) headerBuffer.writeUInt32LE(8 + recordBuffer.length + sortedIPs.length * 7 - 7, 4) console.log([ '文件头长度:', headerBuffer.length, '记录区长度:', recordBuffer.length, '索引区长度:', Buffer.concat(indexList).length, '记录数:', sortedIPs.length, ]) // 4. 合并所有部分 return Buffer.concat([ headerBuffer, // 文件头 recordBuffer, // 记录区 ...indexList // 索引区 ]) } _ipToInt(ip) { const parts = ip.split('.') // 使用无符号右移确保结果为正数 return ((parseInt(parts[0]) << 24) | (parseInt(parts[1]) << 16) | (parseInt(parts[2]) << 8) | parseInt(parts[3])) >>> 0 } _createRecord(endIPInt, country, area) { // 写入 endIP const recordBuf = Buffer.alloc(4) recordBuf.writeUInt32LE(endIPInt, 0) this.recordList.push(recordBuf) this.maxRecordOffset += 4 // country + area 都有的记录 if (this.stringCache.has(`${country}\t${area}`)) { const redirectBuf = Buffer.alloc(4) redirectBuf.writeUInt8(0x01, 0) const offset = this.stringCache.get(`${country}\t${area}`) redirectBuf.writeUInt8((offset >> 0) & 0xFF, 1) redirectBuf.writeUInt8((offset >> 8) & 0xFF, 2) redirectBuf.writeUInt8((offset >> 16) & 0xFF, 3) this.recordList.push(redirectBuf) this.maxRecordOffset += 4 return } // country, area 分开都有的记录 if (this.stringCache.has(country) && this.stringCache.has(area)) { const countryOffset = this.stringCache.get(country) const areaOffset = this.stringCache.get(area) // 生成缓存键:基于两个偏移的组合 const ptrCacheKey = `ptr:${countryOffset}:${areaOffset}` // 检查是否已有相同的指针组合 if (this.stringCache.has(ptrCacheKey)) { // 复用:用 0x01 重定向到已有的 8 字节块 const existingOffset = this.stringCache.get(ptrCacheKey) const redirectBuf = Buffer.alloc(4) redirectBuf.writeUInt8(0x01, 0) redirectBuf.writeUInt8((existingOffset >> 0) & 0xFF, 1) redirectBuf.writeUInt8((existingOffset >> 8) & 0xFF, 2) redirectBuf.writeUInt8((existingOffset >> 16) & 0xFF, 3) this.recordList.push(redirectBuf) this.maxRecordOffset += 4 return } // 首次出现:直接写入 8 字节(省略 0x01 层) const currentOffset = this.maxRecordOffset const countryBuf = Buffer.alloc(4) countryBuf.writeUInt8(0x02, 0) countryBuf.writeUInt8((countryOffset >> 0) & 0xFF, 1) countryBuf.writeUInt8((countryOffset >> 8) & 0xFF, 2) countryBuf.writeUInt8((countryOffset >> 16) & 0xFF, 3) const areaBuf = Buffer.alloc(4) areaBuf.writeUInt8(0x02, 0) areaBuf.writeUInt8((areaOffset >> 0) & 0xFF, 1) areaBuf.writeUInt8((areaOffset >> 8) & 0xFF, 2) areaBuf.writeUInt8((areaOffset >> 16) & 0xFF, 3) this.recordList.push(countryBuf) this.recordList.push(areaBuf) this.maxRecordOffset += 8 // 缓存这个 8 字节块的位置 this.stringCache.set(ptrCacheKey, currentOffset) // 保持原有的组合缓存(用于策略 1) this.stringCache.set(`${country}\t${area}`, currentOffset) return } // country 有 area 没有的记录 if (this.stringCache.has(country)) { const currentOffset = this.maxRecordOffset const countryOffset = this.stringCache.get(country) const countryBuf = Buffer.alloc(4) countryBuf.writeUInt8(0x02, 0) countryBuf.writeUInt8((countryOffset >> 0) & 0xFF, 1) countryBuf.writeUInt8((countryOffset >> 8) & 0xFF, 2) countryBuf.writeUInt8((countryOffset >> 16) & 0xFF, 3) const areaBuf = Buffer.concat([ iconv.encode(area || '', 'gbk'), Buffer.from([0x00]) ]) this.recordList.push(countryBuf) this.recordList.push(areaBuf) this.maxRecordOffset += countryBuf.length + areaBuf.length // 缓存 const areaOffset = currentOffset + countryBuf.length this.stringCache.set(area, areaOffset) this.stringCache.set(`${country}\t${area}`, currentOffset) return } // 其他情况 const currentOffset = this.maxRecordOffset const countryBuf = Buffer.concat([ iconv.encode(country || '', 'gbk'), Buffer.from([0x00]) ]) const areaBuf = Buffer.concat([ iconv.encode(area || '', 'gbk'), Buffer.from([0x00]) ]) this.recordList.push(countryBuf) this.recordList.push(areaBuf) this.maxRecordOffset += countryBuf.length + areaBuf.length // 缓存 const areaOffset = currentOffset + countryBuf.length this.stringCache.set(`${country}`, currentOffset) this.stringCache.set(area, areaOffset) this.stringCache.set(`${country}\t${area}`, currentOffset) } } export default QQWryPacker ================================================ FILE: version.json ================================================ { "latest": "20260415", "versions": { "20221005": { "count": 530599, "unique": 156555 }, "20221012": { "count": 530606, "unique": 156559 }, "20221019": { "count": 530598, "unique": 156557 }, "20221026": { "count": 530613, "unique": 156558 }, "20221102": { "count": 530632, "unique": 156559 }, "20221109": { "count": 530283, "unique": 156534 }, "20221116": { "count": 530310, "unique": 156530 }, "20221123": { "count": 530387, "unique": 156538 }, "20221207": { "count": 530416, "unique": 156532 }, "20221214": { "count": 530424, "unique": 156532 }, "20221221": { "count": 530422, "unique": 156532 }, "20221228": { "count": 530429, "unique": 156599 }, "20230104": { "count": 530432, "unique": 156599 }, "20230111": { "count": 530438, "unique": 156601 }, "20230118": { "count": 530452, "unique": 156603 }, "20230125": { "count": 530460, "unique": 156606 }, "20230201": { "count": 530475, "unique": 156608 }, "20230208": { "count": 530501, "unique": 156610 }, "20230215": { "count": 530548, "unique": 156615 }, "20230222": { "count": 530586, "unique": 156623 }, "20230301": { "count": 530599, "unique": 156664 }, "20230308": { "count": 530600, "unique": 156725 }, "20230315": { "count": 530585, "unique": 157012 }, "20230322": { "count": 530523, "unique": 160559 }, "20230405": { "count": 530571, "unique": 162291 }, "20230419": { "count": 530638, "unique": 161940 }, "20230426": { "count": 530697, "unique": 161953 }, "20230510": { "count": 530805, "unique": 161980 }, "20230517": { "count": 530831, "unique": 161989 }, "20230524": { "count": 530870, "unique": 162003 }, "20230607": { "count": 530889, "unique": 162009 }, "20230614": { "count": 531340, "unique": 162133 }, "20230621": { "count": 531380, "unique": 162138 }, "20230628": { "count": 531400, "unique": 162144 }, "20230705": { "count": 531635, "unique": 162206 }, "20230726": { "count": 545203, "unique": 162299 }, "20230802": { "count": 545277, "unique": 162316 }, "20230809": { "count": 545361, "unique": 162343 }, "20230823": { "count": 545617, "unique": 162419 }, "20230913": { "count": 546118, "unique": 162597 }, "20230920": { "count": 546212, "unique": 162606 }, "20230927": { "count": 546633, "unique": 162627 }, "20231011": { "count": 546712, "unique": 162648 }, "20231018": { "count": 546739, "unique": 162655 }, "20231025": { "count": 546956, "unique": 162667 }, "20231108": { "count": 547183, "unique": 162691 }, "20231115": { "count": 547242, "unique": 162691 }, "20231122": { "count": 547299, "unique": 162704 }, "20231213": { "count": 547514, "unique": 162726 }, "20231220": { "count": 547557, "unique": 162729 }, "20231227": { "count": 547635, "unique": 162740 }, "20240103": { "count": 547639, "unique": 162740 }, "20240110": { "count": 547681, "unique": 162741 }, "20240117": { "count": 547698, "unique": 162742 }, "20240124": { "count": 547722, "unique": 162745 }, "20240131": { "count": 547744, "unique": 162750 }, "20240207": { "count": 547753, "unique": 162751 }, "20240214": { "count": 547760, "unique": 162751 }, "20240221": { "count": 547783, "unique": 162765 }, "20240228": { "count": 547947, "unique": 162769 }, "20240306": { "count": 547750, "unique": 162753 }, "20240313": { "count": 547762, "unique": 162754 }, "20240320": { "count": 547857, "unique": 162753 }, "20240327": { "count": 548329, "unique": 162823 }, "20240403": { "count": 578950, "unique": 163491 }, "20240410": { "count": 591425, "unique": 164525 }, "20240417": { "count": 599415, "unique": 165770 }, "20240424": { "count": 600034, "unique": 165952 }, "20240508": { "count": 628031, "unique": 166831 }, "20240515": { "count": 1221918, "unique": 167456 }, "20240522": { "count": 1465151, "unique": 167531 }, "20240529": { "count": 1465430, "unique": 167550 }, "20240605": { "count": 1465577, "unique": 167562 }, "20240612": { "count": 1465857, "unique": 167598 }, "20240619": { "count": 1465450, "unique": 166536 }, "20240626": { "count": 1463756, "unique": 166554 }, "20240703": { "count": 1463859, "unique": 166564 }, "20240710": { "count": 1463934, "unique": 166579 }, "20240911": { "count": 1470296, "unique": 167555 }, "20240925": { "count": 1491882, "unique": 170650 }, "20241225": { "count": 1497246, "unique": 171027 }, "20250101": { "count": 1497606, "unique": 171048 }, "20250108": { "count": 1499133, "unique": 171292 }, "20250115": { "count": 1499426, "unique": 171320 }, "20250122": { "count": 1499518, "unique": 171336 }, "20250129": { "count": 1500053, "unique": 171366 }, "20250205": { "count": 1500234, "unique": 171374 }, "20250212": { "count": 1500678, "unique": 171382 }, "20250219": { "count": 1502228, "unique": 171565 }, "20250226": { "count": 1502538, "unique": 171589 }, "20250305": { "count": 1502784, "unique": 171597 }, "20250312": { "count": 1503240, "unique": 171602 }, "20250319": { "count": 1503712, "unique": 171606 }, "20250326": { "count": 1503908, "unique": 171605 }, "20250402": { "count": 1504242, "unique": 171610 }, "20250409": { "count": 1504441, "unique": 171615 }, "20250416": { "count": 1504873, "unique": 171623 }, "20250423": { "count": 1505280, "unique": 171645 }, "20250430": { "count": 1505524, "unique": 171641 }, "20250507": { "count": 1505651, "unique": 171643 }, "20250514": { "count": 1505876, "unique": 171645 }, "20250521": { "count": 1506135, "unique": 171649 }, "20250528": { "count": 1506279, "unique": 171671 }, "20250604": { "count": 1506465, "unique": 171669 }, "20250611": { "count": 1506669, "unique": 171676 }, "20250618": { "count": 1507011, "unique": 171683 }, "20250625": { "count": 1507345, "unique": 171691 }, "20250702": { "count": 1508023, "unique": 171761 }, "20250709": { "count": 1508640, "unique": 171846 }, "20250716": { "count": 1509472, "unique": 171917 }, "20250723": { "count": 1510061, "unique": 171957 }, "20250730": { "count": 1510563, "unique": 172002 }, "20250806": { "count": 1511161, "unique": 172012 }, "20250813": { "count": 1512917, "unique": 172032 }, "20250820": { "count": 1513246, "unique": 172048 }, "20250827": { "count": 1513404, "unique": 172057 }, "20250903": { "count": 1513733, "unique": 172065 }, "20250910": { "count": 1513841, "unique": 172066 }, "20250917": { "count": 1513968, "unique": 172076 }, "20250924": { "count": 1514285, "unique": 172085 }, "20251001": { "count": 1514499, "unique": 172073 }, "20251008": { "count": 1514536, "unique": 172079 }, "20251015": { "count": 1514763, "unique": 172094 }, "20251022": { "count": 1514904, "unique": 172096 }, "20251029": { "count": 1515943, "unique": 172092 }, "20251105": { "count": 1516117, "unique": 172097 }, "20251112": { "count": 1516398, "unique": 172108 }, "20251119": { "count": 1516499, "unique": 172126 }, "20251126": { "count": 1516582, "unique": 172112 }, "20251203": { "count": 1516869, "unique": 172114 }, "20251210": { "count": 1516927, "unique": 172129 }, "20251217": { "count": 1517124, "unique": 172130 }, "20251224": { "count": 1517155, "unique": 172098 }, "20251231": { "count": 1516654, "unique": 172067 }, "20260107": { "count": 1516779, "unique": 172069 }, "20260114": { "count": 1516888, "unique": 172079 }, "20260121": { "count": 1516987, "unique": 172093 }, "20260128": { "count": 1517149, "unique": 172075 }, "20260204": { "count": 1517652, "unique": 172078 }, "20260211": { "count": 1517758, "unique": 172085 }, "20260218": { "count": 1518892, "unique": 172108 }, "20260225": { "count": 1518990, "unique": 172121 }, "20260304": { "count": 1519155, "unique": 172139 }, "20260311": { "count": 1519910, "unique": 172153 }, "20260318": { "count": 1520032, "unique": 172165 }, "20260325": { "count": 1521275, "unique": 172372 }, "20260401": { "count": 1521384, "unique": 172383 }, "20260408": { "count": 1521597, "unique": 172401 }, "20260415": { "count": 1522039, "unique": 172421 } } }