前言:本程序限制较大,后期功能可以考虑慢慢加装
使用框架:koishi+napcat构建
需要知识:熟悉koishi的使用以及自编写koishi插件,JavaScript,napcat的相关操作
使用平台:QQ
缺点:根据需求的不同,本项目需要你提前了解有什么问题,我要选哪个做一个预设值。
速度:大概在2秒左右,根据问题数量依次提升
危险风度:可能会出现风控等原因导致的封号,目前正在慢慢寻找方法
正文:问卷星自动预设执行程序,也就是自动对问卷星中的内容执行固定的操作,来做到可以快速反应的作用。在设计开始前,需要思考两个问题,该如何检索到链接,该如何处理。这是两个不可避免必须要先解决的问题,得益于之前开发过QQ机器人,帖主我一下子就想到了使用QQ三方机器人来接受消息的方法,通过Koishi+napcat的框架构建,我们可以搭建一个三方QQ机器人来接受信息,并再额外对koishi内部写一个插件来自动填写。
使用koishi+napcat构建:
首先我们应该要明白如何使用koishi+napcat构建,参考文章:使用 1Panel 部署基于 Koishi + Napcat 的 QQ 机器人 - Bronya Zaychik (bronya-zaychik.cn),虽然docker确实非常的好用,但是你仍然可以通过本地下载一键集合包的方法来使用napcat,下载链接:https://github.com/NapNeko/NapCatQQ/releases/,一般来说你下载win64无头的即可使用。关于napcat和koishi的连接搭配就不必多说了,连接完成之后,你的机器人就可以接受到消息并对消息进行处理了,可以提前将机器人部署进入群内挂好以便及时反应。
第二,便是关于这个插件的编写,说是用koishi写插件,其实和写JavaScript差不多,用到了puppeteer库来模拟人工操作,首先先用npm下载puppeteer库,但是我推荐使用yarn来安装这个库,npm实在是太慢了,安装好了之后,便开始准备写吧,在写之前,我们要先去看一下问卷星的前端状态,我们需要了解如何快速找寻并填入,这里我自己开了一个问卷星来观察,进入浏览器点开f12观察前端代码即可。
点开f12,首先先观察填空框和提交组价。
这是第一个填空框。
这是第二个填空框,第三个设计的内容我隐藏掉了,但是还是能在前端查找到。
这是最后提交按钮的前端代码。
接下来就是喜闻乐见的找规律时刻,如果你熟悉html+css语言的话,你可以发现,这个填空真正起到输入作用的只有一个地方,那就是<input>标签,并且他们都含有一个独一无二的标识符,问题一的id就对应着q1,问题二的id就对应着q2,这是一个非常关键的标识符,提交按钮的标识符也是一样,通过观察,你可以发现他本质上是一个含有click事件的盒子,那么对于提交按钮,我们只需要模拟点击盒子就可以进行提交了。那么我们就可以尝试直接写了。
首先,就是接受消息并做一个正则比对,判断经过的信息流中是否含有问卷星标签的相关链接,如果有,那么便接受处理,如果没有,那么就过滤掉。
接下来开始使用puppeteer库来启动一个浏览器来帮助模拟用户操作,相关设置为如此,并且为了防止网页因网络波动原因导致的错误,额外添加一个含有重试机制的导航函数。
之后设置视口和User-Agent
在实际开发过程中,一定要学会在各处添加报错排查,那么前两个问题便可以这么写,并且可以在下面代码中的page.type当中添加你自己的预设值。
接下来进行最后的操作处理,在实际运行过程中,waitForNavigation很容易出现超时报错问题,官方的解决方法是,将提交的按键与切换导航给链接在一起执行。
之后结尾代码为:
但是在实际运行过程中发现,运行的速度甚至不如手速快一点的输入,通过不断查找资料,了解到是加载了过多的资源导致的,那么我们可以自动剔除掉不需要的资源渲染。
其中配置浏览器启动参数列表里面,headless一般设置为true,设置为false的原因是通过观察为了方便排查出错原因,在页面加载策略当中,可以尽可能的将一些不需要的资源给过滤掉。那么实际运行到这里,填空的自动输入就已经完成了。接下来就是一个有点坑的单选和多选了,首先依旧是观察他们的前端。
这是一个盒子,有一个input值被隐藏了,和填空差不多是吧,哈哈,如果你是这么想的话你就错了,是不是以为只需要简单的click一下那个input标签就是正确的,其实并不是,在实际运行过程中,我明白了这是一种操作管理<a>标签来实现的,也就是说,真正起到作用的内容实际上是标签<a>,这才是需要点击的,如果你去click那个input标签,那么在实际运行过程中,你只会看到好想确实被点击了,但是系统并没有认为被点击,那么我们就可以这样来编写。
通过索引第三个ui-radio事件当中的class=jqradio来帮助定位。只有这样,在实际运行中才是正确的,最后,附加上完整代码。
import { Context, Schema } from 'koishi'
const puppeteer = require('puppeteer')
export const name = 'getmessage'
export interface Config {}
export const Config: Schema<Config> = Schema.object({})
export function apply(ctx: Context) {
// write your plugin here
let browser;
puppeteer.launch({ headless: true }).then(instance => browser = instance)
// 监听消息事件
ctx.on('message', async (session) => {
// 使用正则匹配问卷星链接
const wjxRegex = /(https?:\/\/(www\.)?wjx\.cn\/[^\s]+)/i
const match = session.content.match(wjxRegex)
if (!match) return // 如果没有匹配到链接则退出
const url = match[0]
// 配置浏览器启动参数
const launchOptions = {
headless: false,
// 使用无GPU模式提高性能
args: ['--disable-gpu', '--no-sandbox', '--disable-setuid-sandbox','--remote-debugging-port=0'],
// 增加启动超时时间
timeout: 30000
};
try {
const browser = await puppeteer.launch(launchOptions);
const page = await browser.newPage();
page.setDefaultNavigationTimeout(30000); // 设置为30秒
// 设置页面加载策略
await page.setJavaScriptEnabled(true);
await page.setRequestInterception(true);
page.on('request', (req) => {
if (['image','font'].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
//await page.setJavaScriptEnabled(true);
//await page.setImagesEnabled(false);
//await page.setVideosEnabled(false);
// await page.setRequestInterception(true);
// 打开新页面
// page.on('request', (req) => {
// if (['image', 'stylesheet', 'font', 'script'].includes(req.resourceType())) {
// req.abort(); // 阻止加载图片、样式表、字体和脚本
// } else {
// req.continue(); // 继续其他请求
// }
// });
// page.on('request', (req) => {
// if (['image', 'stylesheet','font'].includes(req.resourceType())) {
// req.abort(); // 阻止加载图片、样式表、字体和脚本
// } else {
// req.continue(); // 继续其他请求
// }
// });
// 使用重试机制的导航函数
async function navigateWithRetry(maxRetries = 3) {
let retryCount = 0;
while (retryCount < maxRetries) {
try {
await page.goto(url, { waitUntil: 'load' });
return true;
} catch (err) {
if (err instanceof puppeteer.errors.TimeoutError) {
console.log(`导航超时,第${retryCount + 1}次重试...`);
retryCount++;
} else {
throw err;
}
}
}
throw new Error(`无法在${maxRetries}次尝试内导航到页面`);
}
//设置视口和 User-Agent
page.setViewport({ width: 1920, height: 1080 })
page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36')
// 访问问卷页面
// await page.goto(url, { waitUntil: 'networkidle0' })
await navigateWithRetry();
console.log('重接进入网页功能处理完毕')
// 示例:填写第一个文本输入框
// await page.type('#q1', '')
await page.waitForSelector('#q1', { visible: true });
await page.type('#q1', '114514');
console.log('问题1解决完毕')
// await page.type('#q2', '')
await page.waitForSelector('#q2', { visible: true });
await page.type('#q2', '114514');
console.log('问题2解决完毕')
// 示例:选择第一个单选按钮
// await page.waitForSelector('#q3_3',{visible:true});
// await page.click('#q3_3')
// const isElementVisible = await page.evaluate((selector) => {
// const element = document.querySelector(selector);
// return element ? element.offsetWidth > 0 && element.offsetHeight > 0 : false;
// }, '#q3_3');
// if (isElementVisible) {
// await page.click('#q3_3');
// } else {
// console.log('Element #q3_3 is not visible');
// }
// await page.evaluate(() => {
// const radio = document.getElementById('q4_3');
// if (radio) {
// radio.click();
// }
// })
// await page.evaluate(() => {
// const radio = document.getElementById('q4_3');
// if (radio) {
// // 创建一个鼠标事件
// const event = new MouseEvent('click', {
// view: window,
// bubbles: true,
// cancelable: true
// });
// // 触发点击事件
// radio.dispatchEvent(event);
// }
// });
await page.waitForSelector('.ui-radio');
await page.click('.ui-radio:nth-of-type(3) .jqradio');
console.log('问题三解决完毕')
//await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
// 提交问卷(根据实际页面结构修改)
// await page.click('#ctlNext')
//await page.click('#ctlNext');
// 等待提交完成
// 同时等待页面导航和点击操作
// await page.click('#ctlNext')
// await page.waitForNavigation()
const [response] = await Promise.all([ // 点击操作
page.waitForNavigation(),
page.click('#ctlNext')// 等待网络空闲
]);
console.log('导航栏以及结束处理完毕')
//await page.waitForNavigation()
//await asyncio.wait([page.waitForNavigation(),page.click('#ctlNext')])
// 关闭页面
await page.close()
console.log('完毕页面处理完毕')
// 发送反馈
//await session.send('问卷已自动填写并提交!')
console.log('success')
} catch (err) {
console.log('mission fail')
console.error('问卷处理失败:', err)
//await session.send('问卷自动处理失败,请手动处理。')
}
})
// 关闭浏览器实例
ctx.on('dispose', async () => {
if (browser) await browser.close()
})
}