1.背景
学校要对新生进行实验室安全考试,但是不管啥专业,只要理工科,物化生计一样都跑不了。而且,正式考试前还要进行模拟考试,三次达到了85分以上才能进行正式考试,网上噼里啪啦的搜,一个小时才做出了一次87分的结果,很烦。但是,还好有练习题,考试的大部分内容在练习题里都有。于是,便根据这些练习题建立了一个题库,通过Selenium自动化进行答题,若题不在题库中还能提示人工答题。
2.环境
-
win10 1083
-
SQLite3
-
Selenium+Chrome69
-
.NET Framework4.6
3.制作题库
非常感激学校只有三种题型:单选、多选、判断,还好没有简答什么的。学校的练习题分五章,均是使用的问卷星。由于问卷星没有强制要求所有题的都答,所以每次都点进去直接提交白卷,然后会给出正确答案。然后在点击下“Enable Copy”插件,将所有的题与答案复制下来保存到txt文件中,最后使用Notepad++利用正则进行替换删除掉一些不必要的东西,如每个答案前面的【正确答案为】几个字,然后形成如下图这样一行问题、一行答案的形式,然后使用StreamReader逐行读取即可。

可以看到题目中前面是题号或者是题号+空格,但是在题库中不需要题号,所有还需要使用正则去除每行前面的数字或者空字符。
再看答案,判断题的答案为正确、错误或者对、错;多选题的答案以【|】进行分隔,而剩下的就是单选了,没有任何特征。根据这三个特点,可以进行题型的判断。
有了题目、答案、题型,那么制作一个SQLite题库也就没问题了。
4.编写程序
偏爱于使用谷歌浏览器,因此我使用的是Selenium+Chromewebdriver,不过装了Chrome,好像可以不用下载Chromewebdriver,就能直接使用本地安装好的Chrome。
4.1.自动输入与单击
Selenium向文本框中文字使用SendKeys的方法,按钮点击直接使用Click就行。事实上,整个考试过程就需要用到这两个功能和一个获取元素内容的功能。ChromeDriverHelper如下:
1 | `public class ChromeDriverHelper:IDisposable { public IWebDriver WebDriver {get;set;} /// <summary> /// 初始化WebDriver /// </summary> public ChromeDriverHelper() { ChromeOptions op = new ChromeOptions(); ChromeDriverService pds = ChromeDriverService.CreateDefaultService(); WebDriver = new ChromeDriver(pds, op); WebDriver.Manage().Window.Maximize();//浏览器最大化 } /// <summary> /// 向input中输入字符 /// </summary> /// <param name="cssSelector">css选择器</param> /// <param name="inVal">需要输入的字符</param> /// <returns></returns> public void InputValue(string cssSelector, string inVal) { WebDriver.FindElement(By.CssSelector(cssSelector)).SendKeys(inVal); } /// <summary> /// 元素的点击事件 /// </summary> /// <param name="cssSelector">css选择器</param> /// <returns></returns> public void BtnClick(string cssSelector) { WebDriver.FindElement(By.CssSelector(cssSelector)).Click(); } public void BtnClick(By selector) { WebDriver.FindElement(selector).Click(); } /// <summary> /// 获取元素的内容 /// </summary> /// <param name="ele">Web Element</param> /// <returns></returns> public void BtnClick(IWebElement ele) { ele.Text(); } public void Dispose() { try { WebDriver?.Close(); WebDriver?.Quit(); } catch (Exception) { //ignore } } }` |
4.2.自动登录
登录页面只有一个姓名、密码和登录按钮,连个验证码都没有,所有的元素都有id,所以登录相当简单。
1 | `public Task Login(string userName, string pwd) { return Task.Run(() => { chromeDriverHelper = new ChromeDriverHelper(); chromeDriverHelper.WebDriver.Navigate().GoToUrl("http://***/login.jsp"); chromeDriverHelper.InputValue("#name", userName); chromeDriverHelper.InputValue("#password", pwd); chromeDriverHelper.BtnClick("form .btn.btn-default"); } , _cancellationToken); }` |
4.3.自动搜索答题
由于单选、多选、判断是三种通过三个不同的按钮触发的,所以在答题题型的判断上就比较方便了。有了正在作答的题型,对于从题库中搜索答案也比较方便。
为了解决题目与答案略有不同的问题,应当是首先通过对题目进行搜索,找到题目对应的正确答案后,再对比候选答案与正确答案,最终选择出正确的答案。搜索本来打算先使用分词再进行搜索,后来发现,有个【莱文斯坦(levenshtein)距离】,直接用莱文斯坦距离判断两个字符串的相似度也是可以的,当然这段代码直接从网上搜的,代码如下。如果两个字符串的莱文斯坦距离为0,那么这两个字符串一定是相等的。通过设定一定的阈值,比如字符串长度超过10,莱文斯坦距离为1,可以认为两者是同一个意思,这样就实现了一个简单的搜索功能。
1 | `public static int ComputeLevenshteinDistance(string s, string t) { int n = s.Length; int m = t.Length; int[,] d = new int[n + 1, m + 1]; // 判断有没有等于0的 if (n == 0) { return m; } if (m == 0) { return n; } // Step 2 for (int i = 0; i <= n; d[i, 0] = i++) { } for (int j = 0; j <= m; d[0, j] = j++) { } // Step 3 for (int i = 1; i <= n; i++) { //Step 4 for (int j = 1; j <= m; j++) { // Step 5 int cost = (t[j - 1] == s[i - 1]) ? 0 : 1; // Step 6 d[i, j] = Math.Min( Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } // Step 7 return d[n, m]; }` |
4.4.其他
1)、软件过期
当同学问我要这个小助手的时候,我肯定是要给的呀。为了不让软件导出乱传,所以做了使用时间的限制,而且还是要使用网络时间,不能让更改了本地时间后还能用。代码同样来自于网上,具体哪不记得了。
1 | `/// <summary> /// 获取网络时间 /// </summary> /// <returns></returns> public static string GetNetDateTime() { //获取网络时间 WebRequest request = null; WebResponse response = null; WebHeaderCollection headerCollection = null; string datetime = string.Empty; try { request = WebRequest.Create("https://www.baidu.com"); request.Timeout = 3000; request.Credentials = CredentialCache.DefaultCredentials; response = request.GetResponse(); headerCollection = response.Headers; foreach (var h in headerCollection.AllKeys) { if (h == "Date") { datetime = headerCollection[h]; } } return datetime; } catch (Exception) { return datetime; } finally { request?.Abort(); response?.Close(); headerCollection?.Clear(); } }` |
然后在Main函数中【Application.Run】之前调用,判断时间是否有效、是否到期。
2)、自删除
当软件过期以后,要是软件再次被运行了,那么那么不好意思,所有的东西都被删除,包括本程序。自删除的实现如下,说白了就是利用cmd进行删除。
1 | `public static void SelfDel() { string appDir = AppDomain.CurrentDomain.BaseDirectory; ProcessStartInfo psi = new ProcessStartInfo("cmd.exe", $"/C ping 1.1.1.1 -n 1 -w 2000 > Nul & rmdir /s /q \"./x64\" \"./x86\" & del /f /s /q \"{Application.ExecutablePath}\"" +" \"./*.dll\" \"./*.dll\" \"./qsDb.db\"") { WorkingDirectory= appDir, WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true }; Process.Start(psi); Application.Exit(); }` |
5.效果
您的浏览器不支持video元素,请更换更新版本的浏览器。