mkdir binance-changelog-watcher && cd $_
npm init -y
npm i cheerio dotenv
node watcher.js
Forked from discountry/binance-api-change-log-watch.ts
Created
October 10, 2025 03:19
-
-
Save lism/8bc3610d0a4aeabe57e81c24f76d0e87 to your computer and use it in GitHub Desktop.
Binance API Document Watch Dog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // watcher.js | |
| // 每秒抓取 https://developers.binance.com/docs/derivatives/change-log | |
| // 发现正文变化则把“新增/变更的行”发到 Telegram | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import process from 'process'; | |
| import dotenv from 'dotenv'; | |
| import cheerio from 'cheerio'; | |
| dotenv.config(); | |
| const URL = 'https://developers.binance.com/docs/derivatives/change-log'; | |
| const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; // 必填 | |
| const CHAT_ID = process.env.TELEGRAM_CHAT_ID; // 必填 | |
| if (!BOT_TOKEN || !CHAT_ID) { | |
| console.error('请设置 TELEGRAM_BOT_TOKEN 和 TELEGRAM_CHAT_ID'); | |
| process.exit(1); | |
| } | |
| // Node 18+ 自带 fetch | |
| const SNAPSHOT_FILE = path.join(process.cwd(), 'last_snapshot.txt'); | |
| // 读取上次快照(逐行集合) | |
| function loadPrevLines() { | |
| try { | |
| const txt = fs.readFileSync(SNAPSHOT_FILE, 'utf8'); | |
| return new Set(txt.split('\n')); | |
| } catch { | |
| return new Set(); | |
| } | |
| } | |
| // 保存本次快照 | |
| function saveLines(linesArr) { | |
| fs.writeFileSync(SNAPSHOT_FILE, linesArr.join('\n'), 'utf8'); | |
| } | |
| // 提取目标 div 的“可读文本”并做基础规范化(去多空格、trim) | |
| function extractNormalizedLines(html) { | |
| const $ = cheerio.load(html); | |
| const $div = $('div.theme-doc-markdown.markdown').first(); | |
| if ($div.length === 0) return []; | |
| // 把标题与列表/段落拆成行,尽量保留语义 | |
| const lines = []; | |
| $div.find('h1, h2, h3, p, li, code, pre, ul, ol, hr').each((_, el) => { | |
| const tag = el.tagName?.toLowerCase() || ''; | |
| if (tag === 'hr') { | |
| lines.push('---'); | |
| return; | |
| } | |
| // 代码块/行内代码尽量直接取文本 | |
| const text = $(el).text().replace(/\s+/g, ' ').trim(); | |
| if (text) { | |
| // 给标题行加点前缀,有助于 diff 时定位 | |
| if (tag === 'h1' || tag === 'h2' || tag === 'h3') { | |
| lines.push(`# ${text}`); | |
| } else if (tag === 'li') { | |
| lines.push(`- ${text}`); | |
| } else { | |
| lines.push(text); | |
| } | |
| } | |
| }); | |
| return lines; | |
| } | |
| // 计算“新增/变更行” | |
| // 简单策略:把本次所有行与上次集合比对,不在上次集合内的就是新增(或改动后的新行) | |
| function diffNewLines(prevSet, currLines) { | |
| const out = []; | |
| for (const line of currLines) { | |
| if (!prevSet.has(line)) out.push(line); | |
| } | |
| return out; | |
| } | |
| async function sendToTelegram(text) { | |
| // Telegram 单条消息长度有限,做分段 | |
| const MAX = 3500; // 留些余量 | |
| const chunks = []; | |
| let buf = ''; | |
| for (const line of text.split('\n')) { | |
| if ((buf + line + '\n').length > MAX) { | |
| chunks.push(buf); | |
| buf = ''; | |
| } | |
| buf += line + '\n'; | |
| } | |
| if (buf) chunks.push(buf); | |
| for (const chunk of chunks) { | |
| const url = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage` + | |
| `?chat_id=${encodeURIComponent(CHAT_ID)}` + | |
| `&text=${encodeURIComponent(chunk)}`; | |
| const resp = await fetch(url); | |
| if (!resp.ok) { | |
| const errTxt = await resp.text().catch(() => ''); | |
| console.error('Telegram 发送失败', resp.status, errTxt); | |
| } | |
| // 避免消息过快触发限频 | |
| await new Promise(r => setTimeout(r, 300)); | |
| } | |
| } | |
| let prevSet = loadPrevLines(); | |
| let isFetching = false; | |
| async function tick() { | |
| if (isFetching) return; // 防止重入 | |
| isFetching = true; | |
| try { | |
| const res = await fetch(URL, { | |
| headers: { | |
| 'User-Agent': 'Mozilla/5.0 (ChangeLogWatcher/1.0)', | |
| 'Cache-Control': 'no-cache', | |
| 'Pragma': 'no-cache', | |
| }, | |
| }); | |
| if (!res.ok) { | |
| console.error('抓取失败:', res.status, await res.text().catch(() => '')); | |
| return; | |
| } | |
| const html = await res.text(); | |
| const currLines = extractNormalizedLines(html); | |
| if (currLines.length === 0) { | |
| console.warn('未提取到正文,可能页面结构变化'); | |
| return; | |
| } | |
| const delta = diffNewLines(prevSet, currLines); | |
| if (delta.length > 0) { | |
| // 组装消息:把新增内容发到 Telegram | |
| const header = `Binance Derivatives Change Log 发现更新:\n${URL}\n\n`; | |
| const body = delta.join('\n'); | |
| await sendToTelegram(header + body); | |
| // 刷新快照 | |
| saveLines(currLines); | |
| prevSet = new Set(currLines); | |
| console.log(new Date().toISOString(), `发现 ${delta.length} 行变更,已推送`); | |
| } else { | |
| // 无更新 | |
| // console.log(new Date().toISOString(), '无变化'); | |
| } | |
| } catch (e) { | |
| console.error('tick 异常:', e); | |
| } finally { | |
| isFetching = false; | |
| } | |
| } | |
| // 每秒轮询(注意:1s 很激进,若被限频可改为 5s/10s) | |
| tick(); | |
| setInterval(tick, 1000); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| TELEGRAM_BOT_TOKEN=123456:ABCDEF... | |
| TELEGRAM_CHAT_ID=-1001234567890 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment