/** * 今彩539 增量更新腳本 (Incremental Update) * 只抓取比現有資料更新的期號,速度快、省流量 * 用法: node update.js */ const https = require('https'); const fs = require('fs'); const DATA_FILE = 'lottery_539_data.json'; const API_URL = 'https://www.pilio.idv.tw/Json_ltonew.asp'; // ── Load existing data ──────────────────────────────────────────────────────── const existing = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); const lastDex = Math.max(...existing.draws.map(d => parseInt(d.dex))); console.log(`📂 現有資料:${existing.draws.length} 期 (最新 dex=${lastDex})`); // ── Fetch helper ────────────────────────────────────────────────────────────── function fetchJson(lindex, ldesc) { return new Promise((resolve, reject) => { const postData = `Lkind=lto539&Lindex=${lindex}&Ldesc=${ldesc}`; const opts = { hostname: 'www.pilio.idv.tw', port: 443, path: '/Json_ltonew.asp', method: 'POST', headers: { 'User-Agent': 'Mozilla/5.0', 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; const req = https.request(opts, res => { let raw = ''; res.on('data', c => raw += c); res.on('end', () => { try { resolve(JSON.parse(raw)); } catch { reject(new Error('JSON parse failed: ' + raw.slice(0, 100))); } }); }); req.on('error', reject); req.write(postData); req.end(); }); } function pad(n) { return String(parseInt(n)).padStart(2, '0'); } function parseDate(raw) { const clean = raw.replace(/<[^>]+>/g, ' ').trim(); const m = clean.match(/^(\d{2})[\/-](\d{2})\s+(\d{2})\((.)\)/); if (!m) return clean; const yr = parseInt(m[3]) < 50 ? `20${m[3]}` : `19${m[3]}`; return `${yr}/${m[1]}/${m[2]} (${m[4]})`; } // ── Fetch only new draws ────────────────────────────────────────────────────── async function fetchNewDraws() { const newDraws = []; console.log('🔍 檢查是否有新期數...'); let currentIndex = lastDex; while (true) { let batch; try { batch = await fetchJson(currentIndex, 0); } // 0 is ascending (old to new) catch (e) { console.log('⚠️ API Error:', e.message); break; } const arr = batch.lotto || []; if (!arr.length) break; for (const item of arr) { const idx = parseInt(item.dex); if (idx <= lastDex) { continue; } const numText = item.num.replace(/<[^>]+>/gi, '').replace(/ /gi, ' '); const nums = numText.match(/\d{1,2}/g).map(n => pad(n)); if (nums.length < 5) continue; newDraws.push({ date: parseDate(item.date), dateSort: (() => { const clean = item.date.replace(/<[^>]+>/g, ' ').trim(); const m = clean.match(/^(\d{2})[\/-](\d{2})\s+(\d{2})\(/); if (m) { const y = parseInt(m[3]); const fullYr = y < 50 ? `20${String(y).padStart(2,'0')}` : `19${String(y).padStart(2,'0')}`; return `${fullYr}${m[1]}${m[2]}`; } return ''; })(), numbers: nums.slice(0,5), dex: item.dex, }); } currentIndex = arr[arr.length - 1].dex; } // API fetches descending, but our newDraws needs to be appended in ascending order later // wait, we just return it, the main function sorts it: .sort((a,b)=>parseInt(a.dex)-parseInt(b.dex)) return newDraws; } // ── Prediction (M3 SpreadOverdue) ───────────────────────────────────────────── const BUCKETS = [ [1,2,3,4,5,6,7,8], [9,10,11,12,13,14,15], [16,17,18,19,20,21,22], [23,24,25,26,27,28,29,30], [31,32,33,34,35,36,37,38,39], ]; function predict(draws) { const gaps = {}; for (let i = 1; i <= 39; i++) gaps[pad(i)] = draws.length; for (let i = draws.length - 1; i >= Math.max(0, draws.length - 600); i--) for (const n of draws[i].numbers) if (gaps[pad(n)] === draws.length) gaps[pad(n)] = draws.length - 1 - i; return BUCKETS.map(b => b.map(n => ({ n: pad(n), s: gaps[pad(n)] || 0 })).sort((a,b)=>b.s-a.s)[0].n ).sort((a,b) => parseInt(a)-parseInt(b)); } // ── Main ────────────────────────────────────────────────────────────────────── (async () => { const newDraws = await fetchNewDraws(); if (newDraws.length === 0) { console.log('✅ 已是最新資料,無需更新'); return; } console.log(`✨ 新增 ${newDraws.length} 期:`); for (const d of newDraws) console.log(` 期 ${d.dex} ${d.date} ${d.numbers.join(', ')}`); // Merge & sort const merged = [...existing.draws, ...newDraws] .sort((a, b) => parseInt(a.dex) - parseInt(b.dex)); // Deduplicate by dex const seen = new Set(); const deduped = merged.filter(d => { if (seen.has(d.dex)) return false; seen.add(d.dex); return true; }); // Update prediction const prediction = predict(deduped); console.log(`🎯 新預測號碼:${prediction.join(', ')}`); const out = { ...existing, draws: deduped, prediction, generatedAt: new Date().toISOString(), modelVersion: 'M3_SpreadOverdue', modelNote: '回測≥1命中率 51.3%', }; fs.writeFileSync(DATA_FILE, JSON.stringify(out, null, 2), 'utf8'); console.log(`✅ lottery_539_data.json 已更新 (共 ${deduped.length} 期)`); console.log('\n▶ 接著請執行: node backtest.js'); })();