不积跬步,无以至千里;不积小流,无以成江海。
————————————————————————————————————————————————————
最近因为某些原因笔者需要频繁的填写表单,在多次错过时间被点名批评后终于决定借助技术的力量拜托重复劳动。笔者之前从未接触过HTML和Javascript,是真正的从零开始,等做完以后发现还是挺简单的。这篇文章记录了我在这个过程中踩过的坑,希望能给大家提供一个参考。
网页分析和程序设计
首先,这是我们实验用的WPS网页表单:
在当前网页上,单击鼠标右键——审查元素(或者“查看代码”/“开发人员工具”,浏览器不同选项不同),得到如下页面:
下方即为本页面的HTML代码。每一个选项和输入框,都对应其中一行,找到相应的代码对我们使用程序自动填表至关重要。在相应元素上点击“审查元素”,即可跳到对应代码上。
下面开始编程。我使用的是VS2017,编程语言为.net4.7.1下的C#。
新建项目,创建windows窗体,在窗体中拖拽一个webbrowser控件,默认命名为“webBrowser1”。这个控件用于模拟浏览器访问网页。
输入网址,跳转:webBrowser1.Navigate("https://f.wps.cn/form-write/KKPn2wCj/");
也可以直接赋值:webBrowser1.Url =new Uri("https://f.wps.cn/form-write/KKPn2wCj/") ;
接下来需要等待网页加载完毕。如果网页没有加载完,代码是读取不到页面内的元素的。C#提供了一些方式用于判断加载状态,比如
- webBrowser控件的ReadyState属性,我们可以在循环语句中使用:
while (webBrowser1.ReadyState==WebBrowserReadyState.Loading) ;
- webBrowser控件的webBrowser1_DocumentCompleted()事件,在加载完毕后自动触发。将之后的代码写在事件内就可以。
但这个网页在加载时会触发多次事件,以上两种方式都无法使用。我简单粗暴的使用了一个定时器,延时几秒后判断是否找得到页面的元素,如果能找的到就说明页面加载完毕了,否则就再等几秒再判断一次。给窗口拖入一个timer控件。
timer1.Enabled = true;
timer1.Interval = 1000;
然后在触发中断函数中继续写代码,开始填表。
首先对这个网页进行分析。从网页代码中可以看到,问题1的标签是input,id为“input_0”:
C#提供了两种方式用于找到你想要的网页元素:GetElementById()和GetElementsByTagName()。既然有id号,那用id是最方便的:
HtmlDocument doc = webBrowser1.Document;
doc.GetElementById("input_0").Focus();//姓名
SendKeys.SendWait("峡谷柔情");//使用模拟按键的方式
这里之所以不用直接修改value的方式doc.GetElementById("input_0").SetAttribute("value", “峡谷柔情”);
可能是因为这个网页的问题,点击其他地方后刚修改的值马上会为空。因此笔者通过模拟键盘输入的方式进行填写。
问题2/3/4,网页代码如下:
对于这种单选框,我们模拟鼠标的点击来完成操作:doc.GetElementById("select_label_wrap_1_1").InvokeMember("click");//单位
问题5比较复杂,笔者作为初学者花了不少时间。点击这一项后会弹出一个日期框,选择日期后自动给value赋值。
它的代码如下:
从代码中可以看到,这是一个picker类:“class=ant-calendar-picker”,它的组成比较复杂,有很多子元素,儿通过id的访问只能到某一个父元素。c#提供了一些访问子元素的方式,如Children[],FirstChildren等。具体问题具体分析,经过我的尝试,第一次模拟点击的代码如下:
//打开日期选择
doc.GetElementById("date-common-4").Children[1].Children[0].Children[0].InvokeMember("click");
点击之后会弹出一个日历选择框,在网页代码的后半段也会出现对应的变化:
找到“今天”按钮对应的代码,我们可以看到这是一个超链接式的按钮:role=“button”。c#对于此类按钮没有相应的方法,但我们知道它的tagname为a,我们可以曲线救国:
//选择今天
doc.GetElementsByTagName("a")[doc.GetElementsByTagName("a").Count-1].InvokeMember("click");
因为是弹出窗口,所以在相同的标签中,它一定是数组中的最后一个元素。模拟鼠标点击即可。
对于问题6,我们需要结合系统时间做出选择:
int hour = System.DateTime.Now.Hour;//填写时间
if (hour >= 6 && hour <= 8)
doc.GetElementById("select_label_wrap_5_0").InvokeMember("click");
else if (hour >= 10 && hour <= 12)
doc.GetElementById("select_label_wrap_5_1").InvokeMember("click");
else if (hour >= 18 && hour <= 20)
doc.GetElementById("select_label_wrap_5_2").InvokeMember("click");
之后找到提交按钮对应的代码,模拟点击后,对于弹出的确认窗口用相同手法处理。
对于一张WPS表单,所有可能的问题都在这里了,下面贴上源码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace FormFillingAssistant
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
webBrowser1.Navigate("https://f.wps.cn/form-write/KKPn2wCj/");//加载表单界面
Timer timer1 = new Timer();
timer1.Enabled = true;
timer1.Interval = 1000;
}
private void timer1_Tick(object sender, EventArgs e)
{
HtmlDocument doc = webBrowser1.Document;
try
{
doc.GetElementById("input_0").Focus();//姓名
SendKeys.SendWait("峡谷柔情");//使用模拟按键的方式,直接修改value会无法录入
//建议使用执行单击事件的方式来设置单选框,而不是修改属性
doc.GetElementById("select_label_wrap_1_1").InvokeMember("click");//单位
doc.GetElementById("select_label_wrap_2_0").InvokeMember("click");//居住地
doc.GetElementById("select_label_wrap_3_0").InvokeMember("click");//共居人员
int hour = System.DateTime.Now.Hour;//填写时间
if (hour >= 6 && hour <= 8)
doc.GetElementById("select_label_wrap_5_0").InvokeMember("click");
else if (hour >= 10 && hour <= 12)
doc.GetElementById("select_label_wrap_5_1").InvokeMember("click");
else if (hour >= 18 && hour <= 20)
doc.GetElementById("select_label_wrap_5_2").InvokeMember("click");
doc.GetElementById("input_6").Focus();//体温
SendKeys.SendWait("36.6");
doc.GetElementById("select_label_wrap_7_0").InvokeMember("click");//本人情况
doc.GetElementById("select_label_wrap_8_0").InvokeMember("click");//活动轨迹
doc.GetElementById("select_label_wrap_9_0").InvokeMember("click");//同居者状况
doc.GetElementById("select_label_wrap_10_1").InvokeMember("click");//居住地情况
doc.GetElementById("select_label_wrap_11_0").InvokeMember("click");//近十四天
doc.GetElementById("select_label_wrap_12_0").InvokeMember("click");//其他事
//打开日期选择
doc.GetElementById("date-common-4").Children[1].Children[0].Children[0].InvokeMember("click");
//选择今天
doc.GetElementsByTagName("a")[doc.GetElementsByTagName("a").Count-1].InvokeMember("click");
//doc.GetElementById("submit_button").InvokeMember("click"); //提交
timer1.Enabled = false;//如果网页成功加载则关闭定时器1
timer2.Enabled = true;//开启定时器2
}
catch
{
}
}
private void timer2_Tick(object sender, EventArgs e)
{//在页面上停留一定时间,确认无误后自动提交,并点击确认按钮
timer2.Enabled = false;
webBrowser1.Document.GetElementById("submit_button").InvokeMember("click"); //提交
webBrowser1.Document.GetElementById("bind_phone_modal_confirm_button").InvokeMember("click"); //确认提交
}
}
}
计划任务
在“我的电脑”上右击,选择“管理”——“任务计划程序”,“创建基本任务”,设置好调用小工具的时间即可。傻瓜操作,不再赘述。
感谢 @卢喵猫 同学的文章https://blog.youkuaiyun.com/JCFY1055176872/article/details/104267639?from=timeline,给了我很大的启发。他是用python实现的,经过我的对照,两种编程语言各有优劣,收益颇丰。
萌新写手,烦请各位大佬多多指教。
附上C#中WebBrowser常用的方法和属性汇总:
https://www.cnblogs.com/vaevvaev/p/6980318.html