作为本系列博文的开篇,有必要先做些声明,用于免责、以绝口水:
- 博文仅围绕已经弃用的、C/S结构的《上海市个人非营业性客车额度竞拍程序》客户端(NetBidClient)进行介绍,对于正在使用的系统不进行任何讨论。
- 作者从未向“代拍黄牛”提供过任何技术支持或外挂软件,也没有依赖相关技术从事任何营利活动。研究此类技术仅是个人兴趣使然。
- 请勿使用相关技术从事非法活动,“出来混,迟早要还的”。
言归正传,看完定场诗咱们开始。
`说书唱戏劝人方 三条大路走中央`
`善恶到头终有报 人间正道是沧桑`
"神器"做了些什么?
其实市面上的“神器”一点也不神秘,它所做的事无非就是本来你使用软件竞标时所做的那些事——根据策略掌握时机出价、识别验证码、完成出价。只是计算机在完成这一系列步骤的时候,不会紧张、不会犹豫、不会出错、速度还比我们快许多(只要几百毫秒),大概这就是它们“神”的理由吧。
根据“神器”的上述功能,本系列博文将分为以下几个方面,依次展开讨论:
- 如何实现计算机模拟键盘鼠标的操作。
- 验证码的识别。
- 竞拍程序(NetBidClient)分析。
本讲内容
“天下武功,无坚不摧,唯快不破”,神功第一重,内容如下:
- 调用SendInput()函数实现键鼠模拟。
- 为NetBidClient竞拍程序部署一个演示用服务器,用于以后测试。
模拟键盘鼠标输入
先来看看,计算机若要替代人类进行竞拍程序操作,需要完成那些招式:
- 首先获取窗口句柄,并激活窗口。
- 获取窗口的屏幕位置坐标。
- 根据窗口的屏幕坐标计算出控件的屏幕坐标。
- 向控件发送鼠标或者键盘的操作指令。
以上这些招,依赖WinAPI函数就能完成(当然还有其它的方法可选,如果你想了解其它“门派”的武功可以看看这里)。
好,我们来看分解动作:
第1招, 获取窗口句柄,并激活
这招,通过调用FindWindow和SetForegroundWindow两个函数实现,看看函数名就能猜到他们是干什么的,声明如下:
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);
FindWindow有两个String类型的传入参数——lpClassName指窗口的类名和lpWindowName指窗口的标题名,我们使用VS的工具Spy++来获得它们,打开Spy++的查找窗口,拖拽“查找程序工具”(那个十字准星)到目标窗口上就行,结果如下图。
FindWindow函数的返回值就是窗口句柄,把获得的窗口句柄作为参数传给SetForegroundWindow函数,就能让窗口激活。
第2招, 获得窗口的屏幕坐标
调用GetWindowRect函数,即可获得以像素为单位的窗口位置与宽高信息。
[DllImport("user32.dll", SetLastError=true)]
static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
第3招, 计算窗口控件的屏幕坐标
屏幕坐标指的是以屏幕左上角为原点的向下坐标系,窗口坐标指以窗口左上角为原点的坐标系。
我们点击一个控件,或者在控件中输入字符时,SendInput函数要求我们提供屏幕坐标。由于窗口在屏幕上的位置不固定,所以控件的屏幕坐标也不是固定的,还好我们可以通过控件的窗口坐标加上窗口的屏幕坐标获得控件的屏幕坐标。
//screenX, screenY 是控件的屏幕坐标(x,y)
//window.RECT是上面GetWindowRect获得的窗口位置信息
// dx,dy 是控件在窗口坐标
screenX = window.RECT.Left + dx
screenY = window.RECT.Top + dy
第4招, 发送鼠标或键盘的操作指令
通过调用API函数SendInput函数来模拟键盘鼠标输入,这个算本讲的大招,需重点说说,先看声明:
[DllImport("user32.dll")]
internal static extern uint SendInput(uint nInputs,
[MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs,
int cbSize);
SendInput函数有3个传入参数,先看第二个pInputs,它是INPUT结构的数组,每个INPUT结构中定义了一次键鼠操作,既然pInputs参数是个数组类型,说明调用一次SendInput函数可以完成多个键鼠操作,例如,把鼠标移动到TextBox控件上(MoveTo)、按下鼠标左键(LeftDown,LeftUp)、输入字符(KeyChrDownUp)这一列动作可以一次传给SendInput去执行。
nInputs参数是指pInputs[]中有多少个INPUT,cbSize参数指INPUT结构的尺寸。
再来看看INPUT及部分结构体的定义。
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
internal InputType type;
internal InputUnion U;
internal static int Size
{
get { return Marshal.SizeOf(typeof(INPUT)); }
}
}
internal enum InputType : uint
{
MOUSE = 0,
KEYBOARD = 1,
HARDWARE = 2
}
[StructLayout(LayoutKind.Explicit)]
internal struct InputUnion
{
[FieldOffset(0)]
internal MOUSEINPUT mi;
[FieldOffset(0)]
internal KEYBDINPUT ki;
[FieldOffset(0)]
internal HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
internal struct KEYBDINPUT
{
internal VirtualKeyShort wVk;
internal short wScan;
internal KEYEVENTF dwFlags;
internal int time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
internal struct MOUSEINPUT
{
internal int dx;
internal int dy;
internal int mouseData;
internal MOUSEEVENTF dwFlags;
internal uint time;
internal IntPtr dwExtraInfo;
}
看了上面的定义,有点明白怎么用了吧!先告诉INPUT.type是MOUSE还是KEYBOARD操作,然后再在INPUT.U中放个MOUSEINPUT或KEYBDINPUT就行了,MOUSEINPUT和KEYBDINPUT结构体分别用于说明你想怎么操作鼠标或键盘。下面我们用个代码片断来看看SendInput函数的调用。
至于完整的声明及定义可在本文例子中找到(在WinAPIHelper.cs里)。
POINT p = new Point();
int perWidth = (0xFFFF / (GetSystemMetrics(SystemMetric.SM_CXSCREEN) - 1));
int perHeight = (0xFFFF / (GetSystemMetrics(SystemMetric.SM_CYSCREEN) - 1));
GetCursorPos(out p);
//把鼠标从当前位置,向右移动200个像素,向下移动300个像素
p.X = p.X + 200;
p.Y = p.Y + 300;
var pInputs = new[]{
new INPUT() //第一个动作
{
type = InputType.MOUSE, //一个鼠标操作
U = new InputUnion()
{
mi = new MOUSEINPUT()
{
dx = p.X * perWidth, //移动鼠标
dy=p.Y * perHeight,
mouseData = 0,
time = GetTickCount(),
dwFlags = MOUSEEVENTF.MOVE| MOUSEEVENTF.ABSOLUTE, //移动鼠标,绝对坐标
dwExtraInfo = GetMessageExtraInfo()
}
}
},
new INPUT()
{
type = InputType.MOUSE, //一个鼠标操作
U = new InputUnion()
{
mi = new MOUSEINPUT()
{
dx = 0,
dy= 0,
mouseData = 0,
time = GetTickCount(),
dwFlags = MOUSEEVENTF.LEFTDOWN, //鼠标左键按下
dwExtraInfo = GetMessageExtraInfo()
}
}
},
new INPUT()
{
type = InputType.MOUSE, //一个鼠标操作
U = new InputUnion()
{
mi = new MOUSEINPUT()
{
dx = 0,
dy= 0,
mouseData = 0,
time = GetTickCount(),
dwFlags = MOUSEEVENTF.LEFTUP, //鼠标左键弹起
dwExtraInfo = GetMessageExtraInfo()
}
}
}
,
new INPUT()
{
type = InputType.KEYBOARD, //一个键盘操作
U = new InputUnion()
{
ki = new KEYBDINPUT()
{
wScan =ScanCodeShort.KEY_1, //按下1键
wVk = VirtualKeyShort.KEY_1,
dwFlags =KEYEVENTF.UNICODE
}
}
}
,
new INPUT()
{
type = InputType.KEYBOARD, //一个键盘操作
U = new InputUnion()
{
ki = new KEYBDINPUT()
{
wScan =ScanCodeShort.KEY_1, //1键弹起
wVk = VirtualKeyShort.KEY_1,
dwFlags =KEYEVENTF.KEYUP | KEYEVENTF.UNICODE
}
}
}
};
SendInput((uint)pInputs.Length, pInputs, INPUT.Size);
在这个例子中, 鼠标从当前位置向右移动200px,再向下移动300px,点击一下鼠标左键,再按一下数字1键,如果在鼠标移到的位置上有个TextBox控件,你会发现TextBox里被输入了一个“1”。
另外,需注意一下,MOUSEINPUT结构中dx,dy的值,并不是以像素为单位的坐标系,它定义屏幕的左上角为原点,右下角的坐标为(0xFFFF,0xFFFF),使用的时候记得把你的像素坐标转化一下下。
秘籍
C#中使用WinAPI函数时,声明函数、定义各种结构类型、枚举类型,实在是个繁琐且容易出错的工作。下面给大家推荐一个Visual Studio的扩展工具,它能让您调用WinAPI函数的工作更容易些:
-
首先在这里下载,双击下载完成的.vsix文件,就会为VS安装扩展工具。
-
完成安装后,在VS IDE环境中会增加如图菜单
- 选择"Insert PInvoke Signatures"菜单,即可在光标处插入您想使用的API函数声明或结构定义等。
试一试吧, 是不是So easy? “以后妈妈再也不用担心我调用WinAPI函数了”。
部署竞拍演示服务器
作者并不了解上海国拍行的竞拍服务器采用的是什么技术,下面给出的演示服务程序,仅仅是根据NetBidClient程序的需要,模仿了部分服务器返回值而已,目的是能让NetBidClient成功登录,并能显示验证码。
- 下载附件中DemoSvr.zip文件,展开DemoSvr目录下的内容,目录结构保持不变。
- 在IIS中新建站点(.NetFramework 4),绑定HTTP和HTTPS,内容目录指向DemoSvr。
- 修改本机的hosts文件中,添加如下内容:
`
127.0.0.1 toubiao.alltobid.com
127.0.0.1 toubiao2.alltobid.com
127.0.0.1 tblogin.alltobid.com
127.0.0.1 tblogin2.alltobid.com
127.0.0.1 tbquery.alltobid.com
127.0.0.1 tbquery2.alltobid.com
`
好了,启动你的Web站点,访问一下https://toubiao.alltobid.com/car/gui/login.aspx,如果有返回值就成功了,打开NetBidClient程序登录吧,投标号/密码随便输。
结束语
非常感谢您读到了这里, 希望您能明白我说了此什么,如果我没说清楚,附件里有些例子供您参考。
下一次我们将用更简单的方法来模拟键鼠输入。
附件:
DemoSvr.zip 旧版拍牌程序NetBidClient,演示服务程序和源码
SimuWAPI.zip 本文例子程序