尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

油猴脚本实战:自动完成必应积分每日搜索任务

油猴脚本实战:自动完成必应积分每日搜索任务 我是从2019年开始折腾必应积分Microsoft Rewards的那会儿每天手动点搜索框、敲关键词、等下轮刷新一套流程走下来最少也要几分钟。后来工作一忙经常忘记做每日任务眼睁睁看着积分断签兑换礼品卡的进度条卡在半路心里特别不爽。于是我花了几个晚上写了一个油猴脚本挂在浏览器里让它自动跑必应的每日搜索任务用到现在一年多基本没再手动碰过这活儿。这篇文章就把这个脚本从设计思路、代码实现到踩坑记录完整拆开讲一遍。它不是什么高深项目但里面涉及的页面自动化思路、随机化策略、状态管理还有油猴脚本的调试技巧换到其他网页自动化场景里一样能复用。如果你也想用油猴脚本解决重复性网页操作或者单纯想给必应积分这摊事找个省心方案这篇内容值得你花十分钟看完。1. 项目整体设计与方案选型1.1 必应积分任务到底在做什么先花点时间说清楚必应积分这个机制。它是必应搜索内置的奖励体系你使用必应搜索时系统会按搜索次数给你积分。每天搜索一定次数就能拿满当天的搜索积分连续登录也有额外奖励积分攒够了可以兑换礼品卡、游戏会员或者捐赠出去。听起来很简单但真做起来很烦人。桌面端必应搜索积分并不是搜一次就完事的你得在一天内完成多轮搜索每轮之间还得隔开时间间隔连续快速搜索会被系统判定为异常行为。我用计时器测过手动完成一轮得分搜索大概需要1到2分钟算上中间等待时间一天至少要花五六分钟在上面。这事儿本身不难难就难在它是个纯重复性的机械操作放在每天要做的事列表里纯属浪费生命。脚本要解决的就这么个痛点自动打开搜索页面自动输入关键词并触发搜索按合理的间隔跑完当天所有搜索轮次同时把状态记录下来避免重复执行。整个过程不需要用户参与浏览器挂在那里就行。1.2 为什么选油猴脚本而不是独立脚本方案选型的时候我其实纠结过。最开始想过用Python脚本配合requests直接请求必应的搜索接口这种方式实现起来非常快代码量也少但有一个致命问题必应对非浏览器的请求检测非常严格我从头到尾试过几次要么立刻触发安全验证要么直接返回异常页面Cookie的维护也特别麻烦。后来又考虑过Selenium这类浏览器自动化工具功能确实强但为了跑一个搜索脚本就起一个浏览器引擎资源占用太大了而且Selenium的指纹特征很固定反而更容易被服务端识别出自动化行为。最后选了油猴脚本也就是用户脚本原因有三点。第一脚本运行在真实浏览器环境里从服务端看就是一次正常的用户访问没有所谓的自动化指纹伪装度最高第二Tampermonkey、Violentmonkey这类管理工具在Chrome、Edge、Firefox都有对应版本一个脚本写好了全平台通用第三油猴脚本天然支持GM_setValue这类本地存储接口状态管理不需要额外搭后端或配置文件。说白了油猴脚本就是用真实用户的操作方式去执行自动化这也是它比HTTP请求模拟和Selenium都更体面的根本原因。页面自动化项目选技术方案的时候优先考虑运行在真实浏览器环境中的方案这个经验在别的网页脚本项目里同样适用。1.3 脚本核心能力拆解动手写代码之前我先把这个脚本要做的事拆成了几个独立模块。第一个模块是页面任务识别脚本加载后需要判断当前页面是不是必应搜索页以及今天还差多少轮搜索这是所有后续操作的前提第二个模块是搜索动作执行主要包括怎么定位搜索框、怎么输入关键词、怎么触发搜索第三个模块是执行节奏控制两轮搜索之间必须有一个随机的时间间隔这个间隔既要够长以避开风控又不能长到让用户等得心焦第四个模块是本地状态持久化用油猴的存储接口把今天已经跑了多少轮、跑到了第几个关键词这些信息记录下来防止浏览器刷新后状态丢失。模块拆分好之后整个脚本的架构就非常清晰了。每个模块之间通过一个最小的接口通信比如任务识别模块告诉核心调度器还有几轮要做调度器再决定是排队执行下一轮还是直接收工。没有绕来绕去的状态纠缠后面加功能、改逻辑都方便。对我这种喜欢写完就扔一边、隔几个月再回来改的人来说清晰的模块边界比什么都重要。2. 上手实操搭建脚本骨架2.1 安装油猴管理器与新建脚本用油猴脚本跑自动化第一步是装一个用户脚本管理器。目前最主流的方案就是TampermonkeyChrome、Edge、Firefox都支持Firefox用户也可以用Violentmonkey两个管理器对标准GM API的支持都很到位代码基本可以直接通用。装好管理器之后在浏览器工具栏找到Tampermonkey图标点开后选择添加新脚本就会进入脚本编辑页面。这个页面其实就是个简单的代码编辑器左边写代码右边有语法高亮和基础校验对日常写脚本来说完全够用。建议不要直接在一个空白脚本里开写每次新建脚本都先保存一个模板把常用的元数据块后续讲和基础骨架代码放进去新建脚本之后直接改名字和匹配规则就行能省不少事。2.2 元数据块配置的正确姿势油猴脚本的顶部有一块固定格式的注释这块东西叫做元数据块脚本的名称、匹配的网址、执行的时机、需要调用的权限接口全在这里声明。新手最容易在这里翻车经常是元数据写错了脚本控制台里报错还一头雾水。我这份脚本的元数据块最初长这样// UserScript // name 必应每日任务 // namespace local.scripts.bing-rewards // version 1.2.0 // description 自动完成必应积分每日搜索任务 // author yourname // match https://www.bing.com/* // match https://cn.bing.com/* // grant GM_setValue // grant GM_getValue // run-at document-idle // noframes // /UserScript这里逐个解释关键字段。match 声明了这个脚本在哪些网址下生效我列了 www.bing.com 和 cn.bing.com两个域名下的内容和Cookie体系不完全一样积分任务也是独立的两个都匹配上可以灵活切换。但要注意必应搜索的积分入口在不同地区可能跳转到不同子域如果你的账号区域不是中国大陆可能要再加一条对应的 match 规则。grant 用来声明脚本需要调用的油猴扩展接口这里只用了GM_setValue和GM_getValue所以就把这两个单独列出来。多余的权限不要申请减少暴露面这是写油猴脚本的一条通用原则。run-at document-idle 表示在页面文档加载完成、DOM可操作之后再执行脚本。千万别漏了这行如果不声明默认执行时机可能太早搜索框还没渲染出来脚本根本找不到元素。noframes 表示脚本只在顶层页面运行不在页面内部的iframe里注入。必应的搜索结果页里面会有搜索结果的新窗口渲染加了这个可以有效避免脚本在冗余frame里重复初始化。2.3 页面元素定位的稳定写法脚本写出来能不能稳定跑一大半取决于你是不是找到了一个可靠的页面元素定位方案。必应首页的结构不算复杂搜索框的ID是sb_form_q大部分情况下直接用 document.getElementById 就能拿到。但这里有一个坑必应首页和搜索页面的DOM结构不完全一致。首页是一个完整的搜索表单ID是sb_form里面的输入框是sb_form_q而搜索结果页面顶部有一个简化版的搜索框虽然ID同样是sb_form_q但外层容器的class随时会变。所以我干脆统一了一个逻辑不管在什么页面先通过ID找sb_form_q找不到再用name属性兜底再找不到就抛异常并记录日志。至于触发搜索的方式最稳妥的做法不是手动构造键盘事件而是找到包含搜索框的那个form元素直接调用form的submit方法。这种方式的兼容性最好不会因为少了某个事件监听器就开始行为异常。我自己实际测试下来用form.submit()比模拟回车的成功率更高尤其是在必应搜索结果页内部再触发搜索的场景里submit几乎能保证每一次都触发页面的跳转逻辑而模拟Enter键偶尔会因为焦点不在输入框上而失效。3. 核心功能实现搜索任务自动执行3.1 模拟搜索信息的随机化策略必应的风控规则我不会去正面硬碰硬我的原则很简单让脚本行为尽量像一个正常用户。这体现在两个细节上。第一搜索关键词必须多样化。很多人写类似脚本的时候用一个关键词列表循环取词但如果你每天搜索的词永远来自同一批固定的词条过不了几天服务端就可能通过统计发现了端倪。我准备了一个较大的词库每次从里面随机取词同时结合当前时间戳生成一个种子词拼接进去。比如搜索天气预报 20240608这样带日期的组合看起来更像是有真实信息需求的用户。第二搜索间隔必须随机化。人类不可能每隔1秒就精确地搜索一次也不可能每天两次搜索的间隔都一模一样。我实现了一个随机间隔生成器核心算法是返回一个符合正态分布的随机时间基本范围控制在20到50秒之间偶尔允许出现比较大的跳动模拟用户中途去看了会儿新闻、接了个电话之类的场景。实测下来这种分布比固定延时或者纯均匀分布的请求模式要自然不少。这里给出随机间隔的核心函数function randomDelay(minMinutes, maxMinutes) { // 生成符合近似正态分布的随机分钟数 const u Math.random() Math.random() Math.random(); const base minMinutes (maxMinutes - minMinutes) * (u / 3); return Math.max(minMinutes, Math.min(maxMinutes, Math.round(base * 60))); }三个Math.random()相加再取平均是为了让中间值出现概率更高两端概率更低。整体效果就是大部分间隔落在中间区间偶尔出现短间隔或长间隔模拟真人节奏。3.2 状态记录与去重机制脚本一旦跑起来就要一直跟踪当前进度否则刷新一下页面就忘了跑到第几轮第二天又从头开始跑很容易超出每日上限反而触发风控。我用了GM_getValue和GM_setValue来做本地持久化存储的数据有三个今天的日期字符串、今天已经完成的搜索轮次、当前取到的关键词索引。日期存储的意义在于判断今天是否需要重置状态——如果存储的日期不是今天的日期那说明距离上次执行已经跨天了所有计数清零重新开始。判断代码很简单const TODAY new Date().toISOString().slice(0, 10); if (GM_getValue(lastDate, ) ! TODAY) { GM_setValue(completedCount, 0); GM_setValue(lastDate, TODAY); }这里要注意toISOString是UTC标准时间如果你的用户在中国时区到了深夜的时候UTC和本地时间会产生偏差。我当时就踩过这个坑凌晨1点跑脚本UTC还是前一天导致状态没被正确重置。后来改用本地时间的格式化函数问题才解决。关键词索引的作用是防止同一轮执行中点击了太多次搜索、或者某一轮超时重试时用掉同一个关键词。每跑完一轮就把索引加一下次取词从下一个位置开始。这个细节看似微小但恰恰是脚本稳定性的关键之一。3.3 完整代码实现与参数说明核心逻辑写完之后整个脚本大概长这样我只保留主流程和调度部分词库因为太长就不全部贴了(function () { use strict; const CONFIG { dailyTarget: 30, // 每日目标搜索轮次 minDelayMin: 0.3, // 最小时间间隔分钟 maxDelayMin: 1.2, // 最大时间间隔分钟 firstDelaySec: 8, // 脚本启动后的首次延迟秒 keywords: [...], // 关键词库建议200条以上 }; function localDateStr() { const d new Date(); const pad (n) (n 10 ? 0 n : n); return d.getFullYear() - pad(d.getMonth() 1) - pad(d.getDate()); } function getDelaySeconds() { const u Math.random() Math.random() Math.random(); const avg (CONFIG.minDelayMin CONFIG.maxDelayMin) / 2 * 60; const spread (CONFIG.maxDelayMin - CONFIG.minDelayMin) * 60; const seconds avg (u - 1.5) * spread / 1.5; return Math.max(CONFIG.minDelayMin * 60, Math.min(CONFIG.maxDelayMin * 60, seconds)); } function buildSearchUrl(keyword) { return https://www.bing.com/search?q encodeURIComponent(keyword); } function processSearch() { const count GM_getValue(completedCount, 0); if (count CONFIG.dailyTarget) { console.log([BingTask] 今日任务已完成共 count 轮); return; } const index GM_getValue(keywordIndex, 0); const keyword CONFIG.keywords[index % CONFIG.keywords.length] localDateStr(); GM_setValue(keywordIndex, index 1); window.location.href buildSearchUrl(keyword); setTimeout(function () { GM_setValue(completedCount, count 1); console.log([BingTask] 已完成 (count 1) 轮等待下一轮); setTimeout(processSearch, getDelaySeconds() * 1000); }, 6000); } function init() { const today localDateStr(); if (GM_getValue(lastDate, ) ! today) { GM_setValue(completedCount, 0); GM_setValue(keywordIndex, 0); GM_setValue(lastDate, today); } if (window.location.pathname.indexOf(/search) ! -1) { const count GM_getValue(completedCount, 0); if (count CONFIG.dailyTarget) { // 当前在搜索结果页启动下一轮倒计时 setTimeout(processSearch, getDelaySeconds() * 1000); } } else { setTimeout(processSearch, CONFIG.firstDelaySec * 1000); } } if (document.readyState loading) { document.addEventListener(DOMContentLoaded, init); } else { init(); } })();这个版本是一个整页跳转式的实现方式逻辑非常直白每次搜索直接通过window.location.href跳转到搜索结果页新页面加载完成后脚本再次被注入然后检查状态、决定要不要继续下一轮。优势是写起来简单不需要处理任何页面元素的点击事件任何页面的结构变化都不会影响核心流程。缺点是每一次跳转都会重新加载整个页面比较消耗网络资源而且如果必应搜索页加载速度慢了6秒的等待时间可能不够导致计数器误判。实际使用中我更推荐的其实是一种更优雅的原地触发模式就是在当前页面里直接找到搜索框填入关键词并触发表单提交不跳引擎。这种方式对用户来说更安静脚本本身也不需要顶层逻辑那么重的初始化。但原地触发的坑在于必应对form submit之后的DOM变化有自己的判断有时候搜索框会短暂地进入disabled状态我刚写第一版的时候这个状态就导致任务卡死所以我最终采用了一个混合方案启动时如果是从首页开启任务直接跳转到搜索结果页并延迟计数搜索页内的后续轮次则优先用form submit。不过关键点是一样的——在执行过程中不要过度等待用setTimeout去预判断页面可能已经加载完比用一个固定长延时等页面加载事件要稳定得多。4. 常见问题与排查技巧4.1 脚本不触发怎么办脚本装上了访问必应搜索页却没有任何反应这个是遇到频率最高的问题。按这个顺序排查基本都能解决。先打开浏览器的开发者工具F12切到Console面板看有没有报错信息。最常见的报错是黄色的跨域警告和红色的GM_getValue is not defined前者一般不影响脚本运行后者说明脚本管理器的API接口没被授权回元数据块检查一下有没有写对应的grant声明。然后是确认脚本是否真的匹配了当前网址。Tampermonkey图标上会显示当前页面脚本的运行数量如果显示0说明匹配规则没覆盖到当前域名。这个情况在你使用地区跳转域名时最容易发生一种处理办法是把 match 写宽松一些// match *://*.bing.com/*匹配规则写为 *.bing.com 之后无论必应把你跳到哪个子域名都能覆盖。注意这条规则的写法在Tampermonkey里是支持的但部分旧版脚本管理器对通配符支持不完整升级管理器版本可以顺手把这个问题也解决了。4.2 执行了但没积分脚本跑得很欢日志里每一轮计数都在涨但第二天打开必应官网一看积分一分没加这个情况我之前也遇到过。排查到后来发现问题多半出在关键词相似度上。我之前图省事拿今天的天气明天的天气后天的天气这种高度相似的词来做搜索词系统很容易就把它们合并判断为同一个搜索意图连续几次都在刷同一类内容不给你算有效搜索。必应对关键词的判定有自己的规则我实测下来搜索词的多样性和主题宽度比次数本身更重要。建议词库里的词尽量覆盖多个领域旅游攻略、菜谱、电子产品评测、新闻热点、影视剧信息、编程问题什么都有一些这样从外部看才更像一个真实用户的搜索行为。另外还有一个冷门问题部分国外区域的必应账号搜索积分只认当地语言和当地搜索页面。如果你账号区域设置是美国却在中国版必应页面上跑任务也可能会出现跑了几百次但积分不涨的情况。这种情况需要你手动把必应区域切回账号对应的区域再重新构建搜索URL。4.3 页面改版后脚本失效必应搜索页面平均每隔一段时间就会做一次前端调整比如搜索框的class名称改动、表单结构变化这是所有网页自动化脚本都躲不过的命运。我的脚本第一版基于2022年的页面结构写的当年12月页面改版之后form.submit()方法依然能用只是定位方式出过几次小问题。应对改版的核心思路是把对结构的依赖降到最低。搜索这个动作的本质是什么是往搜索框里填内容然后提交所以我最终定位搜索框的方式退回到最基本的地道战法先用ID找再用name找再回退到querySelector查找input[typetext]并配合搜索按钮的位置做二次确认。只要搜索框还是一个输入框这套逻辑就能继续工作。还有一个技巧是给脚本加一个selector调试模式在URL后面附加 ?bb_debug1 参数时脚本会打印所有尝试过的选择器和关联结果。这个功能平时静默页面改版后打开调试模式能极大加快定位问题。这个思路在操作任何第三方网页的自动化脚本里都适用。4.4 日志调试与保存状态清理脚本跑久了本地状态可能会出现数据不一致的情况。最常见的场景是跨时区日期判断逻辑出错或者某一天脚本中途崩溃导致completedCount永远卡在一个不到目标值的数字上之后再跑就开始反复重试同一轮。排查方法也很简单打开Tampermonkey的管理面板在已安装脚本的页面里找到存储标签页里面能直接看到脚本存了哪些键值对。如果发现completedCount或者keywordIndex明显异常直接把对应数据删掉让脚本下一轮自己重建状态。日志方面我习惯在所有关键步骤里写console.log输出当前时间、轮次、关键词和下一次等待的时间。这个习惯在其他编程场景里也是一样——输出日志是成本最低的调试手段没有之一。你永远不可能在一个没有日志的系统里快速追踪到问题所在这一点在油猴脚本的调试上被体现得淋漓尽致。最后再分享一个我后期加上的小功能脚本完成当日所有任务后会在console里输出一句完成的提示如果想要更直观的反馈可以改成调浏览器的通知接口弹出系统通知。大多数浏览器对Notification权限有弹窗限制所以要在脚本初始化时先申请权限这块工作量不大但使用体验提升非常明显。我第一次加上这个通知的时候看着桌面弹出今日任务已完成的提示才真正体会到什么叫自己动手解放双手。
返回列表