

当看到哪吒2票房曲线宛如升旗仪式,作为程序猿的我坐不住了!
昨晚刷微博时,一条热搜让我笑喷:”建议哪吒2票房统计改用升旗仪式BGM”。点开实时票房一看,好家伙!96.27亿的曲线笔直向上,这哪是电影票房,分明是程序员的K线图啊!

作为技术宅的我突然上头:我要做个实时票房监控站!破百亿时自动触发天幕烟花!说干就干,抄起键盘就是一顿输出…
🚀 技术选型:AI编程的正确打开方式
凌晨2点的我打开Cursor(VSCode究极进化版),在对话框输入:
我需要一个实时票房监控网站,要求:
1. 每30秒自动刷新数据
2. 破百亿时全屏烟花特效
3. 使用现代前端框架
4. 绕过猫眼反爬机制
AI助手deepseek秒回方案:
建议技术栈:
前端:Astro + React + Tailwind(轻量高性能)
代理:Cloudflare Worker(解决跨域+反爬)
动画:Framer Motion + Canvas烟花特效
部署:Vercel(自动SSL+CDN加速)
看着这行云流水的方案,我直呼好家伙!这AI怕是偷偷刷了LeetCode?
🔍 破解猫眼反爬:与字体加密斗智斗勇
正当我美滋滋调用猫眼API时,一盆冷水浇下——返回的数据全是”口口口”!原来猫眼用了字体反爬,数字都是加密字体!

和AI大战三百回合后,我们写出字体解密算法(已失效):
// 字体映射缓存
const fontMappingCache = new Map();
// 历史值缓存
const valueHistoryCache = new Map();
// 字体映射表 - 更新为更完整的映射
const FONT_MAPPINGS = {
'75e5b39d': {
'ec68': '3', 'e5ac': '0', 'e274': '1', 'e6d5': '2',
'f615': '4', 'eab3': '5', 'f66d': '6', 'ef74': '7',
'e1b7': '8', 'e916': '.', 'f05a': '8', 'e132': '9',
'eac9': '0', 'f477': '1', 'f47a': '2', 'ee5d': '3',
'e0b8': '5', 'e260': '6', 'f1a1': '7', 'e6f0': '8',
'eff9': '9'
},
'20a70494': {
'e886': '0', 'f23f': '1', 'eba2': '2', 'f16b': '3',
'ec4b': '4', 'e583': '5', 'ed8f': '6', 'f11c': '7',
'f3e8': '8', 'f7d2': '9', 'e916': '.', 'f05a': '8',
'e8d7': '0', 'f85e': '1', 'e8ee': '2', 'e99c': '3',
'e9ea': '4', 'f1fc': '5', 'eb92': '6', 'f726': '7',
'f7ff': '8', 'e132': '9', 'eac9': '0', 'f477': '1'
},
'432017e7': {
'f0f0': '0', 'f70e': '1', 'ed4f': '2', 'ed98': '3',
'e85f': '4', 'efe9': '5', 'f7b3': '6', 'f11c': '7',
'f3e8': '8', 'f7d2': '9', 'e916': '.', 'f05a': '8',
'e132': '9', 'eac9': '0', 'ee5d': '3', 'e0b8': '5'
},
'e3dfe524': {
'e886': '0', 'f23f': '1', 'eba2': '2', 'f16b': '3',
'ec4b': '4', 'e583': '5', 'ed8f': '6', 'f11c': '7',
'f3e8': '8', 'f7d2': '9', 'e916': '.', 'f05a': '8',
'e3ec': '4', 'eb19': '5', 'ed30': '6', 'e3df': '7',
'ea60': '8', 'ef28': '3', 'ea6f': '4', 'e132': '9'
},
'2a70c44b': {
'e886': '0', 'f23f': '1', 'eba2': '2', 'f16b': '3',
'ec4b': '4', 'e583': '5', 'ed8f': '6', 'f11c': '7',
'f3e8': '8', 'f7d2': '9', 'e916': '.', 'f05a': '8',
'e83d': '0', 'e132': '9'
}
};
// 数据波动检测配置
const FLUCTUATION_CONFIG = {
MAX_PERCENTAGE: 100, // 增加到100%以适应大盘数据的波动
MIN_VALUE: 0.1,
MAX_VALUE: 1000000, // 增加最大值范围
HISTORY_SIZE: 5 // 增加历史记录大小以提高准确性
};
/**
* 优化的字体ID提取函数
*/
const extractFontId = (fontStyle) => {
if (!fontStyle) return null;
const cached = fontMappingCache.get('lastFontId');
if (cached && fontStyle.includes(cached)) return cached;
// 增加更多的字体URL匹配模式
const fontUrls = fontStyle.match(/url\(['"]*([^'")]+)['"]*\)/g) || [];
for (const fontUrl of fontUrls) {
const url = fontUrl.match(/url\(['"]*([^'")]+)['"]*\)/)[1];
const fontId = url.split('/').pop().split('.')[0];
if (FONT_MAPPINGS[fontId]) {
fontMappingCache.set('lastFontId', fontId);
return fontId;
}
}
return null;
};
/**
* 优化的数值波动检查函数
*/
const isFluctuationReasonable = (currentValue, prevValue, key) => {
if (!prevValue) return true;
if (currentValue < FLUCTUATION_CONFIG.MIN_VALUE || currentValue > FLUCTUATION_CONFIG.MAX_VALUE) return false;
const percentageChange = Math.abs((currentValue - prevValue) / prevValue * 100);
// 检查是否是单位变化(万到亿)
if (percentageChange > FLUCTUATION_CONFIG.MAX_PERCENTAGE) {
const scaledCurrent = currentValue * (currentValue < prevValue ? 10000 : 0.0001);
const scaledPercentageChange = Math.abs((scaledCurrent - prevValue) / prevValue * 100);
if (scaledPercentageChange < FLUCTUATION_CONFIG.MAX_PERCENTAGE) {
return true;
}
console.warn(`数据波动异常 [${key}]:`, { current: currentValue, previous: prevValue, change: percentageChange.toFixed(2) + '%' });
return false;
}
return true;
};
/**
* 优化的HTML实体解码函数
*/
const decodeHtmlEntity = (text, fontStyle, key = 'default') => {
if (!text || !fontStyle) return '0';
try {
const fontId = extractFontId(fontStyle);
if (!fontId) {
console.warn(`未找到有效的字体ID [${key}]`);
return valueHistoryCache.get(key)?.[0]?.toString() || '0';
}
// 获取字体映射
let numMap = fontMappingCache.get(fontId);
if (!numMap) {
numMap = FONT_MAPPINGS[fontId];
if (!numMap) {
console.warn(`未找到字体映射: ${fontId}`);
return valueHistoryCache.get(key)?.[0]?.toString() || '0';
}
fontMappingCache.set(fontId, numMap);
}
// 记录原始编码用于调试
const originalCodes = text.match(/&#x([0-9a-f]{4});/gi) || [];
console.log(`原始编码 [${key}]:`, originalCodes.join(', '));
// 使用正则一次性替换所有编码
const decodedText = text.replace(/&#x([0-9a-f]{4});/gi, (match, code) => {
const decoded = numMap[code.toLowerCase()];
if (!decoded) {
console.warn(`未找到字符映射 [${key}]: ${code}`);
return '0';
}
return decoded;
});
console.log(`解码结果 [${key}]:`, decodedText);
// 获取历史值并检查波动
const history = valueHistoryCache.get(key) || [];
const currentValue = parseFloat(decodedText);
// 针对大盘票房数据的特殊处理
if (key === 'nationBox' || key === 'nationSplitBox') {
// 如果是大盘数据,允许更大的波动范围
if (currentValue > 0 && currentValue < FLUCTUATION_CONFIG.MAX_VALUE) {
// 检查数值是否合理
if (history.length > 0) {
const prevValue = history[0];
const percentageChange = Math.abs((currentValue - prevValue) / prevValue * 100);
if (percentageChange > FLUCTUATION_CONFIG.MAX_PERCENTAGE) {
console.warn(`大盘数据波动过大 [${key}]:`, {
current: currentValue,
previous: prevValue,
change: `${percentageChange.toFixed(2)}%`
});
return prevValue.toString();
}
}
history.unshift(currentValue);
if (history.length > FLUCTUATION_CONFIG.HISTORY_SIZE) {
history.pop();
}
valueHistoryCache.set(key, history);
return decodedText;
}
} else if (!isFluctuationReasonable(currentValue, history[0], key)) {
return history[0]?.toString() || '0';
}
// 更新历史值
history.unshift(currentValue);
if (history.length > FLUCTUATION_CONFIG.HISTORY_SIZE) {
history.pop();
}
valueHistoryCache.set(key, history);
return decodedText;
} catch (error) {
console.error('解码错误:', error);
return valueHistoryCache.get(key)?.[0]?.toString() || '0';
}
};
这段代码就像密码本,把猫眼的加密字体转成真实数字。期间AI甚至教我如何动态更新字体映射表,这学习能力比我带的实习生都强!
💻 前端实现:让数据会跳舞
用Astro搭建的页面加载速度惊人,搭配React的动态组件,数据刷新如丝般顺滑:
<AnimatedNumber value={currentBoxOffice}>
{(value) => (
<motion.div
className="text-6xl font-bold text-gradient"
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 0.5 }}
>
¥{value.toFixed(2)}亿
</motion.div>
)}
</AnimatedNumber>
当票房变化时,数字会先放大再缩回,配合渐变色背景,仿佛数字在呼吸!
🎆 百亿烟花:代码人的浪漫
最让我惊喜的是canvas-confetti配合framer-motion实现的烟花效果,破百亿瞬间触发:
// 增强票房动画效果
const triggerCelebration = () => {
// 多彩五彩纸屑
const colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff'];
// 从底部发射
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.9 },
colors: colors,
});
// 从左侧发射
setTimeout(() => {
confetti({
particleCount: 50,
angle: 60,
spread: 55,
origin: { x: 0, y: 0.5 },
colors: colors,
});
}, 250);
// 从右侧发射
setTimeout(() => {
confetti({
particleCount: 50,
angle: 120,
spread: 55,
origin: { x: 1, y: 0.5 },
colors: colors,
});
}, 400);
// 从中间向四周爆发
setTimeout(() => {
confetti({
particleCount: 150,
spread: 360,
startVelocity: 45,
decay: 0.9,
gravity: 1,
drift: 0,
ticks: 200,
origin: { x: 0.5, y: 0.5 },
colors: colors,
shapes: ['square', 'circle'],
});
}, 600);
// 最后的烟花效果
setTimeout(() => {
const end = Date.now() + 1000;
const colors = ['#ff0000', '#ffd700', '#00ff00', '#0000ff', '#ff00ff'];
(function frame() {
confetti({
particleCount: 5,
angle: 60,
spread: 55,
origin: { x: 0 },
colors: colors
});
confetti({
particleCount: 5,
angle: 120,
spread: 55,
origin: { x: 1 },
colors: colors
});
if (Date.now() < end) {
requestAnimationFrame(frame);
}
}());
}, 800);
};
当票房突破100亿的瞬间,整个屏幕绽放金色烟花,配合《哪吒》经典BGM,程序员的浪漫就是这么朴实无华!

🚨 踩坑实录:那些年我们遇到的坑
- 跨域问题:猫眼的API直接调用会CORS报错,最后用Cloudflare Worker做代理:
const targetUrl = `......`
const headers = {
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Referer': 'https://piaofang.maoyan.com/dashboard/movie',
'Origin': 'https://piaofang.maoyan.com',
'Host': 'piaofang.maoyan.com',
'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin'
};
const response = await fetch(targetUrl, {
headers,
cf: {
cacheTtl: 5,
cacheEverything: true
}
});
🌈 成果展示:从代码到百万浏览
经过3小时爆肝,最终成果:
- 实时票房监控:nezha2.vercel.app
- GitHub源码:github.com/paulloo/nezha2
💡 后记:AI时代程序员生存指南
这次经历让我深刻体会到:
- AI不是替代者,而是增幅器:它能在10分钟内写出我1小时才能完成的代码
- 前端已进入”组装时代”:合理使用现成组件库+AI生成,效率提升10倍不止
- 数据可视化是刚需:一个动效精美的数字,抵得过千行控制台日志
最后分享一句哪吒的台词:”我命由我不由天!”——在AI时代,程序员不是被取代,而是进化成了更强大的物种。
正文完