其中几个重要问题的研究
这个论坛(http://www.cgtsj.com/)的登录系统比较简陋,没有验证码,所以只要用WebBrowser控件的SetAttribute自动填写text就可以搞定。但还有另一个方法,可能适用性更广些——使用Cookies。这样即使是要验证码的论坛(比如小木虫)登录,用户也只需登录一次,记录下网站反馈的Cookies,下次把这些数据feed给WebBrowser就能绕过验证码了。
我建了个名为wb的WebBrowser控件,只要多次调用“Debug.Print(wb.Document.Cookie)”就能在调试窗口看到这个网页在不同阶段都往Cookies里写了什么。我们不妨来研究一下: 一开始的内容是“Hm_lvt_925…de6=146…137,146…175,…; Hm_lpvt_925…de6=146…632; PHPSESSID=b0b…qn6”,这里前两个主要记录访问信息,最后一个是当前会话ID。
然后在网页内输用户名密码登录,记得要勾上“记住”选项,不然Cookies不会有变化。可以发现多了两项:“ck_n01=s…o; ck_m01=f…2;”第一个是用户名;第二个记录的是密码,但论坛也不会傻到用明文记录,所以做了些许的encoding。可这不是我们关注的重点,只要你不换密码,这个ck_m01的值是固定的。为此,我还做了个小实验:把我另一个小号的密码调成和这个号一样,最后发现ck_m01的值相同。这进一步说明了这个假设的正确性。
最后应用这个Cookies并模拟Click对应的签到按钮即可。
01. wb.Navigate("http://www.cgtsj.com/user/index.php")
02. While wb.ReadyState <> 4 Or wb.IsBusy ’ 页面打开时等待
03. My.Application.DoEvents()
04. End While
05. wb.Document.Cookie = "ck_n01=***;" ’ 填用户名
06. wb.Document.Cookie = "ck_m01=***;" ’ 填密码代号
07. wb.Navigate("http://www.cgtsj.com/user/index.php")
08. While wb.ReadyState <> 4 Or wb.IsBusy
09. My.Application.DoEvents()
10.End While
11.wb.Document.All("qiandao").InvokeMember("click")
必须先打开某段Cookies对应的“Domain”网站,才能且只能对该站内的Cookies进行操作,所以这里Navigate了两次。这里再补充说一句,第七行的Navigate替换为Refresh()好似是会有问题的,因为程序会默认浏览器不在加载状态,所以立刻会执行下一条语句(直接跳过While段);但这时候含签到按钮的页面还没有加载出来,所以会跳错。
事情到此貌似已经结束了?假设你在运行这段程序访问这个网站,你会发现网页跳出了一段错误信息:“JSON未定义”。找到错误提示对应的行号,发现有一段“var sj=JSON.stringify(jj)”,而这个JSON在上下文从来没有定义过是什么(看来浏览器没骗人)。其实这个是高版本(从IE 8开始)浏览器一个自带模块的内容,而对比较古老的版本则需要额外引用一个“json2.js”文件。你也许会纳闷,自己用的是Win 10系统,应该是IE 11了才对,但别忘了,WebBrowser和IE并不是完全挂钩的,它其实采用的是IE的低版本(应该是IE 6)兼容模式。为了让它以正常模式运行,MSDN给出了一个解决方案。
“打开注册表
HKEY_LOCAL_MACHINE (or HKEY_CURRENT_USER)
SOFTWARE
Microsoft
Internet Explorer
Main
FeatureControl
FEATURE_BROWSER_EMULATION
contoso.exe = (DWORD) 00000000
其中的"contoso.exe"为您的程序名字,即嵌入了WebBrowser控件的可执行程序的名字。
后面的数值"00000000"代表WebBrowser控件使用的IE版本,值对应的IE版本如下表:”
Value | Description |
11001 (0x2AF9) | Internet Explorer 11. Webpages are displayed in IE11 edge mode, regardless of the declared !DOCTYPE directive. Failing to declare a !DOCTYPE directive causes the page to load in Quirks. |
11000 (0x2AF8) | IE11. Webpages containing standards-based !DOCTYPE directives are displayed in IE11 edge mode. Default value for IE11. |
10001 (0x2711) | Internet Explorer 10. Webpages are displayed in IE10 Standards mode, regardless of the !DOCTYPE directive. |
10000 (0x02710) | Internet Explorer 10. Webpages containing standards-based !DOCTYPE directives are displayed in IE10 Standards mode. Default value for Internet Explorer 10. |
9999 (0x270F) | Windows Internet Explorer 9. Webpages are displayed in IE9 Standards mode, regardless of the declared !DOCTYPE directive. Failing to declare a !DOCTYPE directive causes the page to load in Quirks. |
9000 (0x2328) | Internet Explorer 9. Webpages containing standards-based !DOCTYPE directives are displayed in IE9 mode. Default value for Internet Explorer 9. |
Important In Internet Explorer 10, Webpages containing standards-based !DOCTYPE directives are displayed in IE10 Standards mode. | |
8888 (0x22B8) | Webpages are displayed in IE8 Standards mode, regardless of the declared !DOCTYPE directive. Failing to declare a !DOCTYPE directive causes the page to load in Quirks. |
8000 (0x1F40) | Webpages containing standards-based !DOCTYPE directives are displayed in IE8 mode. Default value for Internet Explorer 8 |
Important In Internet Explorer 10, Webpages containing standards-based !DOCTYPE directives are displayed in IE10 Standards mode. | |
7000 (0x1B58) | Webpages containing standards-based !DOCTYPE directives are displayed in IE7 Standards mode. Default value for applications hosting the WebBrowser Control. |
所以只要在注册表中添加对应exe文件的名称,设置值为0x2AF8就可以了。但在VS里调试还是会有问题的,这里需要对VS做一些小的配置更改:把调试模式从Debug改为Release,并在工程设置的“调试”选项卡中,“启用调试器”栏目下禁用“启用Visual Studio宿主进程”一项。该调整在Visual Studio 2008及2010下通过。
这样所有的功能似乎已经完备了,但我们其实还可以走得更远。假设我有多个账号,都想自动签到呢?有人会说,清一下Cookies,打一段“wb.Document.Cookie.Delete(0, wb.Document.Cookie.Count - 1)”就可以了吧?但事实上不行,首先,仅更改Cookies内容并刷新貌似并不能登入另一个账号,此时需要清除本会话(Session)的内容(记起刚刚提到的PHPSESSID了吗?);可即便清除了Session,登上了另一个号,网页还会提醒“您的其他账号今天已经签过到了”。真是个良心网站!但对于我们的程序而言,当然不希望有这样的障碍产生。究其原因,是因为有些Cookies是设置了矫顽性(persistence)的,Cookie.Delete方法去除不掉;比如说这个网站在其中一个号签完到后强制增添一个“最后签到时间”Cookie(qd_time=146…091),就是如此。幸运的是,某个名为InternetSetOption的Win32API函数(都64位系统了不知道为啥还这么叫…)能实力解决这个问题。这次,MSDN又帮上了忙:
* INTERNET_OPTION_SUPPRESS_BEHAVIOR (Const = 81):
* A general purpose option that is used to suppress behaviors on a process-wide basis.
* The lpBuffer parameter of the function must be a pointer to a DWORD containing the specific behavior to suppress.
* This option cannot be queried with InternetQueryOption.
*
* INTERNET_SUPPRESS_COOKIE_PERSIST (Const = 3):
* Suppresses the persistence of cookies, even if the server has specified them as persistent.
* Version: Requires Internet Explorer 8.0 or later.
*
* INTERNET_OPTION_END_BROWSER_SESSION(Const = 42):
* Flushes entries not in use from the password cache on the hard disk drive. Also resets the cache time used when the synchronization mode is once-per-session. No buffer is required for this option. This is used by InternetSetOption.
第一个是清Cookies的flag,第三个对应清除Session。第二个常数设置了无视矫顽性的属性;注意说明里还提到:只有IE 8以上才支持!真是幸运我们刚才已经解决了这个问题。
在代码一开始声明引用API:
01.Private Declare Function InternetSetOption Lib "wininet.dll" Alias "InternetSetOptionW" (ByVal hInternet As IntPtr, ByVal dwOption As Long, ByRef lpBuffer As IntPtr, ByVal dwBufferLength As Long) As Long
调用的时候这么写:
01.InternetSetOption(IntPtr.Zero, 81, New IntPtr(3), Runtime.InteropServices.Marshal.SizeOf(0)) ’ 清Cookies(最后一长串是在获知储存一个Integer所需的比特数)
02. InternetSetOption(IntPtr.Zero, 42, IntPtr.Zero, 0) ’ 清Session
还有一件比较minor的事,那就是作为一个我希望开机自启自动签到的机器,我不希望它在加载页面的时候发出咔嗒咔嗒的怪声。这个也是好解决的,可以使用Win32API函数CoInternetSetFeatureEnabled。声明:
01.Private Declare Function CoInternetSetFeatureEnabled Lib "urlmon.dll"(ByVal FeatureEntry AsLong, ByVal dwFlags AsLong, ByVal fEnable AsLong) As Long
然后在窗体加载时调用(常量FEATURE_DISABLE_NAVIGATION_SOUNDS值为21,意为禁止跳转声音;常量SET_FEATURE_ON_PROCESS值为2,意为仅对当前进程有效;1代表True):
01.Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
02. CoInternetSetFeatureEnabled(21, 2, 1)
02.End Sub
最后说明一点,这个实例可以应用到大部分论坛的自动登录/签到。要改的部分仅仅是Cookies的内容和要模拟点击的按钮。前者可通过一次登录获得其值;或者,如果有“记住”选项,这一部分甚至不用写进代码里。后者可以通过在按钮上方右键“检查元素”,从网页源代码中得知其ID,NAME,或执行的javascript。第一种情况,可以通过“wb.Document.All("id here")”或“wb.Document.GetElementByID("id here")”找到控件。第二种情况,虽然javascript有GetElementsByName,但貌似wb.Document集合里没有?难道要遍历wb.Document?其实多打一个.All就行了。用wb.Document.All.GetElementsByName("name here")得到的是一个数组,但一般叫这个名字的也就一个控件,找到它只要在后面加“(0)”取得第一个元素就可以了。第三种情况,直接wb.Navigate("javascript:js here")即可。