robotgo以及gohook
先安装mingw64 #https://sourceforge.net/projects/mingw-w64/files/mingw-w64/mingw-w64-release/
安装gcc
go env -w CGO_ENABLED=1 #启用cgo
一、gohook
gohook是在代码执行的特定地点,自定义行为;核心是基于GO的反射,
主要功能:
- 反射机制:使用
reflect
包,在运行时获取和操作函数信息,实现动态拦截的替换 - hook注册和解注册:动态插拔,支持多hook,顺序执行
场景:
- 日志跟踪和测试,在关键函数前后添加
hook
用于记录和调试 - 错误处理,对于可能出现错误的函数使用,避免崩溃
- 性能优化:测量函数执行时间,用于性能优化
1.1、基础用法
package main
import (
"fmt"
// "github.com/go-vgo/robotgo"
hook "github.com/robotn/gohook"
)
func main() {
//add()
//low()
//event()
test()
}
func add() {
//注册两个hook,捕获到"w"则打印,捕获到"ctrl+shift+q"就 hook.End
/*
hook.Register(arg1, arg2, func(e hook.Event) { //如果捕获到 ctrl+shift+q 就执行println动作
fmt.Println("ctrl-shift-q")
hook.End()
})
arg1:记录的事件类型,这里用的常量配置
HookEnabled = 1 // iota
HookDisabled = 2
KeyDown = 3 //键盘动作
KeyHold = 4
KeyUp = 5
MouseUp = 6 //鼠标动作
MouseHold = 7
MouseDown = 8
MouseMove = 9
MouseDrag = 10
MouseWheel = 11
FakeEvent = 12
arg2:事件的值,比如KeyDown为键盘按下,按了“w”键,则arg2为“w”
func(e hook.Event):为回调函数,捕获到事件以及事件对应的动作后,执行的操作,入参为hook.Event 类型,会把捕获的内容存放到e 值中
*/
fmt.Println("--- Please press ctrl + shift + q to stop hook ---")
hook.Register(hook.KeyDown, []string{"q", "ctrl", "shift"}, func(e hook.Event) { //如果捕获到 ctrl+shift+q 就执行println动作
fmt.Println("ctrl-shift-q")
hook.End()
})
fmt.Println("--- Please press w---")
hook.Register(hook.KeyDown, []string{"w"}, func(e hook.Event) { //如果捕获到
fmt.Println("w")
})
s := hook.Start() //返回一个chan Event类型
<-hook.Process(s) //hook.Process内置有一个循环,去捕获请求,并判断是否要执行hook
}
func low() {
//循环去打印捕获到的内容,这里没有注册 hook
/*
type Event struct {
Kind uint8 `json:"id"`
When time.Time
Mask uint16 `json:"mask"`
Reserved uint16 `json:"reserved"`
Keycode uint16 `json:"keycode"` //键盘动作
Rawcode uint16 `json:"rawcode"`
Keychar rune `json:"keychar"`
Button uint16 `json:"button"`
Clicks uint16 `json:"clicks"`
X int16 `json:"x"` //鼠标动作
Y int16 `json:"y"`
Amount uint16 `json:"amount"`
Rotation int32 `json:"rotation"`
Direction uint8 `json:"direction"`
}
按键一次有三个动作,RawCode和keychar有区别,比如M和m的rawcode都是77,但是"m"keychar为109,"M"eychar为77
hook: 2024-05-08 15:30:08.9114509 +0800 CST m=+6.910321501 - Event: {Kind: KeyHold, Rawcode: 81, Keychar: 65535}
hook: 2024-05-08 15:30:08.9114509 +0800 CST m=+6.910321501 - Event: {Kind: KeyDown, Rawcode: 81, Keychar: 113}
hook: 2024-05-08 15:30:08.9750345 +0800 CST m=+6.973905101 - Event: {Kind: KeyUp, Rawcode: 81, Keychar: 65535}
*/
evChan := hook.Start()
defer hook.End()
for ev := range evChan {
fmt.Println("hook: ", ev)
fmt.Println("hook: [KIND]", ev.Kind)
}
}
func event() {
//捕获event,依次操作才可以看到效果
ok := hook.AddEvents("q", "ctrl", "shift")
if ok {
fmt.Println("add events...")
}
keve := hook.AddEvent("k") //字母k
if keve {
fmt.Println("you press... ", "k")
}
mleft := hook.AddEvent("mleft") //鼠标左键 ,mouse arguments: mleft, center, mright, wheelDown, wheelUp, wheelLeft, wheelRight.
if mleft {
fmt.Println("you press... ", "mouse left button")
}
}
func test() {
fmt.Printf("%d\n", 'Q') //81
fmt.Printf("%c\n", 81) //Q
}
1.2、记录键盘动作
package main
import (
"fmt"
hook "github.com/robotn/gohook"
)
var Unomal_key = map[uint16]string{ //特殊字符需要单独处理
8: "[Back]",
9: "[Tab]",
10: "[Shift]",
13: "[Enter]",
17: "[Ctrl]",
18: "[Alt]",
20: "[CAPS LOCK]", //CAPS LOCK
27: "[Esc]",
32: "[SPACE]", //SPACE
33: "[PageUp]",
34: "[PageDown]",
35: "[End]",
36: "[Home]",
37: "[Left]",
38: "[Up]",
39: "[Right]",
40: "[Down]",
41: "[Select]",
42: "[Print]",
43: "[Execute]",
44: "[PrintScreen]",
45: "[Insert]",
46: "[Delete]",
47: "[Help]",
91: "[Windows]",
92: "[Windows]",
93: "[Applications]",
95: "[Sleep]",
108: "[Separator]",
111: "[Divide]",
112: "[F1]",
113: "[F2]",
114: "[F3]",
115: "[F4]",
116: "[F5]",
117: "[F6]",
118: "[F7]",
119: "[F8]",
120: "[F9]",
121: "[F10]",
122: "[F11]",
123: "[F12]",
144: "[NumLock]",
145: "[ScrollLock]",
160: "[LShift]", //LShift
161: "[RShift]", //RShift
162: "[Ctrl]",
163: "[Ctrl]",
164: "[Alt]", //LeftMenu
165: "[RightMenu]",
}
func ListenKeylogger() {
fmt.Println("--- Please press ctrl + shift + q to stop hook ---")
hook.Register(hook.KeyDown, []string{"q", "ctrl", "shift"}, func(e hook.Event) { //如果捕获到 ctrl+shift+q 就执行println动作
fmt.Println("ctrl-shift-q")
hook.End()
})
hook.Register(hook.KeyDown, []string{}, func(e hook.Event) { //如果捕获到,这样写部分字符捕获不到,比如F1-F12,windows等键
tmpstr := ""
if v, ok := Unomal_key[e.Rawcode]; ok {
tmpstr = v
} else {
tmpstr = fmt.Sprintf("%c", e.Keychar)
}
fmt.Printf("%v\t:%s\n", e, tmpstr)
})
hook.Register(hook.KeyHold, []string{}, func(e hook.Event) {
tmpstr := ""
if v, ok := Unomal_key[e.Rawcode]; ok {
tmpstr = v
fmt.Printf("%v\t:%s\n", e, tmpstr)
} else {
if e.Keychar < 65535 {
tmpstr = fmt.Sprintf("%c", e.Keychar)
fmt.Printf("%v\t:%s\n", e, tmpstr)
}
}
})
s := hook.Start() //返回一个chan Event类型
<-hook.Process(s) //hook.Process内置有一个循环,去捕获请求,并判断是否要执行hook
}
func ListenKeylogger2() {
evChan := hook.Start()
defer hook.End()
for e := range evChan {
tmpstr := ""
if v, ok := Unomal_key[e.Rawcode]; ok {
tmpstr = v
if e.Kind == hook.KeyHold {
fmt.Printf("%v\t:%s\n", e, tmpstr)
}
} else {
if e.Kind == hook.KeyDown {
tmpstr = fmt.Sprintf("%c", e.Keychar)
fmt.Printf("%v\t:%s\n", e, tmpstr)
continue
}
}
}
}
func main() {
ListenKeylogger()
//F*,insert,delete,win,ctrl,alt,shift,CAPS lock,
//ListenKeylogger2()
}
/*
F1按键一次会有如下动作,ListenKeylogger2,这里针对F1这种场景,只提取 KeyHold一个即可,对于1-9,a-z,A-Z使用keychar,对于F1-F12得用Rawcode
2024-05-08 18:25:22.8228022 +0800 CST m=+1.697774101 - Event: {Kind: KeyHold, Rawcode: 112, Keychar: 65535} :[F1]
2024-05-08 18:25:22.9468061 +0800 CST m=+1.821778001 - Event: {Kind: KeyUp, Rawcode: 112, Keychar: 65535} :[F1]
正常按一个英文字符的动作
hook: 2024-05-08 17:52:21.506948 +0800 CST m=+2.944139301 - Event: {Kind: KeyHold, Rawcode: 65, Keychar: 65535}
hook: 2024-05-08 17:52:21.506948 +0800 CST m=+2.944139301 - Event: {Kind: KeyDown, Rawcode: 65, Keychar: 97}
hook: 2024-05-08 17:52:21.5702199 +0800 CST m=+3.007411101 - Event: {Kind: KeyUp, Rawcode: 65, Keychar: 65535}
*/
1.3、优化后
package main
import (
"fmt"
hook "github.com/robotn/gohook"
"os"
"strings"
"time"
)
var Unomal_key = map[uint16]string{ //特殊字符需要单独处理
8: "[Back]",
9: "[Tab]",
10: "[Shift]",
13: "[Enter]",
17: "[Ctrl]",
18: "[Alt]",
20: "[CAPS LOCK]", //CAPS LOCK
27: "[Esc]",
32: "[SPACE]", //SPACE
33: "[PageUp]",
34: "[PageDown]",
35: "[End]",
36: "[Home]",
37: "[Left]",
38: "[Up]",
39: "[Right]",
40: "[Down]",
41: "[Select]",
42: "[Print]",
43: "[Execute]",
44: "[PrintScreen]",
45: "[Insert]",
46: "[Delete]",
47: "[Help]",
91: "[Windows]",
92: "[Windows]",
93: "[Applications]",
95: "[Sleep]",
108: "[Separator]",
111: "[Divide]",
112: "[F1]",
113: "[F2]",
114: "[F3]",
115: "[F4]",
116: "[F5]",
117: "[F6]",
118: "[F7]",
119: "[F8]",
120: "[F9]",
121: "[F10]",
122: "[F11]",
123: "[F12]",
144: "[NumLock]",
145: "[ScrollLock]",
160: "[LShift]", //LShift
161: "[RShift]", //RShift
162: "[Ctrl]",
163: "[Ctrl]",
164: "[Alt]", //LeftMenu
165: "[RightMenu]",
}
var printbuffer string //打印字符串
var mouseDrag bool //全局判断鼠标拖拽是否开始
func printstr(kind string, RKIND uint8, str string, print bool) {
//fmt.Println("GET:", printbuffer, str, print)
if print {
//printbuffer = fmt.Sprintf("%v%v", printbuffer, str)
fmt.Printf("%v [%v] %v\n", time.Now().Format("2006-01-02 15:04:05"), kind, printbuffer)
printbuffer = ""
} else {
if RKIND == hook.KeyHold {
//去重
printbuffer = strings.TrimSuffix(printbuffer, str)
printbuffer = fmt.Sprintf("%v%v", printbuffer, str)
//fmt.Printf("====%#v=%#v\n", printbuffer, str)
} else {
printbuffer = fmt.Sprintf("%v%v", printbuffer, str)
}
}
}
func printMouse(kind, str string, x, y int16, clicks uint16) {
if str == "鼠标滚轮" {
fmt.Printf("%v [%v] %v 方向[%v] \n", time.Now().Format("2006-01-02 15:04:05"), kind, str, y)
} else if str == "鼠标左键拖拽" {
if mouseDrag {
return
} else {
mouseDrag = true
fmt.Printf("%v [%v] %v 坐标[%v,%v] \n", time.Now().Format("2006-01-02 15:04:05"), kind, str, x, y)
}
} else {
if mouseDrag && str == "鼠标左键" {
mouseDrag = false
fmt.Printf("%v [%v] 鼠标拖拽结束 坐标[%v,%v] \n", time.Now().Format("2006-01-02 15:04:05"), kind, x, y)
} else {
fmt.Printf("%v [%v] %v 坐标[%v,%v] 点击次数:%v\n", time.Now().Format("2006-01-02 15:04:05"), kind, str, x, y, clicks)
}
}
}
func ListenKeylogger() {
fmt.Printf("%v [Started to KeyLogger....]\n", time.Now().Format("2006-01-02 15:04:05"))
evChan := hook.Start()
defer hook.End()
for e := range evChan {
tmpstr := ""
if v, ok := Unomal_key[e.Rawcode]; ok { //对于键盘做记录
tmpstr = v
if e.Rawcode == 27 {
os.Exit(0) //接收到 "ECS"就退出,这种退出方式有问题,会把输入的内容进行重放
/*
PS D:\golang\mt\src> .\keyboard.exe
2024-05-09 09:47:11 [Started to KeyLogger....]
2024-05-09 09:47:12 jifjsidf[Enter]
2024-05-09 09:47:14 qjiasd[Enter]
PS D:\golang\mt\src> jifjsidf
jifjsidf : 无法将“jifjsidf”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ jifjsidf
+ ~~~~~~~~
+ CategoryInfo : ObjectNotFound: (jifjsidf:String) [], CommandNotFound
*/
}
//针对特殊字符匹配两个
if e.Kind == hook.KeyHold {
printstr("KEYBOARD", e.Kind, tmpstr, false)
}
if e.Kind == hook.KeyUp {
printstr("KEYBOARD", e.Kind, tmpstr, true)
}
} else { //非特殊字符,只匹配 hook.KeyDown
if e.Kind == hook.KeyDown {
tmpstr = fmt.Sprintf("%c", e.Keychar)
printstr("KEYBOARD", e.Kind, tmpstr, false)
}
}
//鼠标记录
/*
记录的场景:其他场景,暂不记录
1)滚轮滚动:这里只记录上下方向
2)左键单击和右键单击
3)右键选择?
4)左键拖拽
*/
//fmt.Println(e)
if e.Kind == hook.MouseDown && e.Clicks >= 1 {
var mouse string
if e.Button == 2 {
mouse = "鼠标右键"
}
if e.Button == 1 {
mouse = "鼠标左键"
}
printMouse("MOUSE", mouse, e.X, e.Y, e.Clicks)
}
if e.Kind == hook.MouseWheel {
printMouse("MOUSE", "鼠标滚轮", 0, int16(e.Rotation), 0)
}
if e.Kind == hook.MouseDrag { //左键拖拽动作,只记录开始和结束坐标
printMouse("MOUSE", "鼠标左键拖拽", e.X, e.Y, e.Clicks)
}
}
}
func main() {
ListenKeylogger()
//test()
}
func test() {
ptime := time.Now().Format("2006-01-02 15:04:05")
fmt.Println(ptime)
}
/*
鼠标动作:
PS D:\golang\mt\src> .\keyboard.exe #右键一次,Button: 2
2024-05-09 10:11:05.3480506 +0800 CST m=+7.501229501 - Event: {Kind: MouseHold, Button: 2, X: 693, Y: 1195, Clicks: 1}
2024-05-09 10:11:05.4081911 +0800 CST m=+7.561370001 - Event: {Kind: MouseDown, Button: 2, X: 693, Y: 1195, Clicks: 1}
2024-05-09 10:11:05.4081911 +0800 CST m=+7.561370001 - Event: {Kind: MouseUp, Button: 2, X: 693, Y: 1195, Clicks: 1}
PS D:\golang\mt\src> .\keyboard.exe #左键一次,Button: 1,Clicks 在x,y同时的情况下,双击会变成2,三击变成3
2024-05-09 10:12:50.8009638 +0800 CST m=+4.221052001 - Event: {Kind: MouseHold, Button: 1, X: 1125, Y: 1175, Clicks: 1}
2024-05-09 10:12:50.9269416 +0800 CST m=+4.347029701 - Event: {Kind: MouseDown, Button: 1, X: 1125, Y: 1175, Clicks: 1}
2024-05-09 10:12:50.9269416 +0800 CST m=+4.347029701 - Event: {Kind: MouseUp, Button: 1, X: 1125, Y: 1175, Clicks: 1}
## 拖拽动作:Button: 0
2024-05-09 10:17:28.2115929 +0800 CST m=+15.968867701 - Event: {Kind: MouseMove, Button: 0, X: 1977, Y: 362, Clicks: 0}
2024-05-09 10:17:28.4002884 +0800 CST m=+16.157563101 - Event: {Kind: MouseHold, Button: 1, X: 1977, Y: 362, Clicks: 1}
2024-05-09 10:17:28.6504618 +0800 CST m=+16.407736401 - Event: {Kind: MouseDrag, Button: 0, X: 1977, Y: 363, Clicks: 1}
2024-05-09 10:17:28.6504618 +0800 CST m=+16.407736401 - Event: {Kind: MouseDrag, Button: 0, X: 1977, Y: 363, Clicks: 1}
2024-05-09 10:17:28.6504618 +0800 CST m=+16.407736401 - Event: {Kind: MouseDrag, Button: 0, X: 1977, Y: 365, Clicks: 1}
...中间都是 "MouseDrag"
2024-05-09 10:17:30.0938402 +0800 CST m=+17.851114101 - Event: {Kind: MouseDrag, Button: 0, X: 1988, Y: 409, Clicks: 0}
2024-05-09 10:17:30.2828984 +0800 CST m=+18.040172201 - Event: {Kind: MouseDrag, Button: 0, X: 1988, Y: 408, Clicks: 0}
2024-05-09 10:17:30.3464025 +0800 CST m=+18.103676201 - Event: {Kind: MouseDown, Button: 1, X: 1988, Y: 408, Clicks: 0}
2024-05-09 10:17:31.605366 +0800 CST m=+19.362639101 - Event: {Kind: MouseMove, Button: 0, X: 1988, Y: 408, Clicks: 0}
2024-05-09 10:17:31.605366 +0800 CST m=+19.362639101 - Event: {Kind: MouseMove, Button: 0, X: 1988, Y: 409, Clicks: 0}
##正常动作是 MouseMove
2024-05-09 10:34:40.3185 +0800 CST m=+2.200004001 - Event: {Kind: MouseMove, Button: 0, X: 1654, Y: 1366, Clicks: 0}
2024-05-09 10:34:40.3185 +0800 CST m=+2.200004001 - Event: {Kind: MouseMove, Button: 0, X: 1650, Y: 1366, Clicks: 0}
##滚轮动作,向上两下 Rotation:-1,向下两下, Rotation:1
2024-05-09 10:35:51.9729837 +0800 CST m=+2.062794701 - Event: {Kind: MouseWheel, Amount: 0, Rotation: -1, Direction: 3}
2024-05-09 10:35:52.7855524 +0800 CST m=+2.875363401 - Event: {Kind: MouseWheel, Amount: 0, Rotation: -1, Direction: 3}
2024-05-09 10:35:53.9752604 +0800 CST m=+4.065071201 - Event: {Kind: MouseWheel, Amount: 0, Rotation: 1, Direction: 3}
2024-05-09 10:35:54.2247366 +0800 CST m=+4.314547401 - Event: {Kind: MouseMove, Button: 0, X: 994, Y: 1211, Clicks: 0}
2024-05-09 10:35:54.4135664 +0800 CST m=+4.503377201 - Event: {Kind: MouseMove, Button: 0, X: 992, Y: 1211, Clicks: 0}
2024-05-09 10:35:54.7895778 +0800 CST m=+4.879388601 - Event: {Kind: MouseWheel, Amount: 0, Rotation: 1, Direction: 3}
2024-05-09 10:35:54.9158717 +0800 CST m=+5.005682401 - Event: {Kind: MouseWheel, Amount: 0, Rotation: 1, Direction: 3}
##滚轮按住一下
*/
测试日志输出如下:
PS D:\golang\mt\src> .\keyboard.exe
2024-05-09 16:07:21 [Started to KeyLogger....]
2024-05-09 16:07:22 [KEYBOARD]
2024-05-09 16:07:24 [KEYBOARD] [Ctrl][LShift]:T
2024-05-09 16:07:24 [KEYBOARD]
2024-05-09 16:07:27 [MOUSE] 鼠标左键拖拽 坐标[131,887]
2024-05-09 16:07:27 [MOUSE] 鼠标拖拽结束 坐标[131,887]
2024-05-09 16:07:28 [MOUSE] 鼠标左键 坐标[600,1112] 点击次数:1
2024-05-09 16:07:32 [KEYBOARD] jidsjif[Enter]
2024-05-09 16:07:34 [KEYBOARD] women[Enter]
2024-05-09 16:07:36 [KEYBOARD] hello[SPACE]
2024-05-09 16:07:37 [KEYBOARD] world[Enter]
2024-05-09 16:07:40 [KEYBOARD] heloo[SPACE]
2024-05-09 16:07:41 [KEYBOARD] world[Enter]
2024-05-09 16:07:43 [KEYBOARD] ceshi[Enter]
2024-05-09 16:07:45 [KEYBOARD] [Home]
2024-05-09 16:07:46 [KEYBOARD] [End]
二、robotgo
windows api:https://learn.microsoft.com/en-us/uwp/api/
2.1、按键对应
"A-Z a-z 0-9"
"backspace"
"delete"
"enter"
"tab"
"esc"
"escape"
"up" Up arrow key
"down" Down arrow key
"right" Right arrow key
"left" Left arrow key
"home"
"end"
"pageup"
"pagedown"
"f1"
"f2"
"f3"
"f4"
"f5"
"f6"
"f7"
"f8"
"f9"
"f10"
"f11"
"f12"
"f13"
"f14"
"f15"
"f16"
"f17"
"f18"
"f19"
"f20"
"f21"
"f22"
"f23"
"f24"
"cmd" is the "win" key for windows
"lcmd" left command
"rcmd" right command
// "command"
"alt"
"lalt" left alt
"ralt" right alt
"ctrl"
"lctrl" left ctrl
"rctrl" right ctrl
"control"
"shift"
"lshift" left shift
"rshift" right shift
// "right_shift"
"capslock"
"space"
"print"
"printscreen" // No Mac support
"insert"
"menu" Windows only
"audio_mute" Mute the volume
"audio_vol_down" Lower the volume
"audio_vol_up" Increase the volume
"audio_play"
"audio_stop"
"audio_pause"
"audio_prev" Previous Track
"audio_next" Next Track
"audio_rewind" Linux only
"audio_forward" Linux only
"audio_repeat" Linux only
"audio_random" Linux only
"num0"
"num1"
"num2"
"num3"
"num4"
"num5"
"num6"
"num7"
"num8"
"num9"
"num_lock"
"num."
"num+"
"num-"
"num*"
"num/"
"num_clear"
"num_enter"
"num_equal"
// // "numpad_0" No Linux support
// "numpad_0"
// "numpad_1"
// "numpad_2"
// "numpad_3"
// "numpad_4"
// "numpad_5"
// "numpad_6"
// "numpad_7"
// "numpad_8"
// "numpad_9"
// "numpad_lock"
"lights_mon_up" Turn up monitor brightness No Windows support
"lights_mon_down" Turn down monitor brightness No Windows support
"lights_kbd_toggle" Toggle keyboard backlight on/off No Windows support
"lights_kbd_up" Turn up keyboard backlight brightness No Windows support
"lights_kbd_down" Turn down keyboard backlight brightness No Windows support
2.2、mouse
func getScreenScale() (scroll int) { //获取屏幕缩放比例
//这里设置取值范围为50%,75%,100%,125%,150%,175%,200%,225%,250%,275%,300%
x, y := robotgo.GetScreenSize()
a, b := robotgo.Location()
id := robotgo.DisplayID
robotgo.MilliSleep(100)
robotgo.Move(x/2, y/2, id)
m, _ := robotgo.Location()
robotgo.Move(a, b, id) //返回原始位置
per := float32(m) * 2 / float32(x)
switch {
case per > 2.75:
scroll = 300
case per > 2.5:
scroll = 275
case per > 2.25:
scroll = 250
case per > 2.0:
scroll = 225
case per > 1.75:
scroll = 200
case per > 1.5:
scroll = 175
case per > 1.25:
scroll = 150
case per > 1.0:
scroll = 125
case per > 0.75:
scroll = 100
case per > 0.5:
scroll = 75
case per > 0.25:
scroll = 50
default:
fmt.Printf("[ERROR] 获取缩放比例值异常 %v \n", scroll)
}
return scroll
}
func getRealMove(m, n int, scroll int) (x, y int) {
x = (m * 100) / scroll
y = (n * 100) / scroll
return
}
func mouse() {
/*
注意:windows屏幕缩放会影响鼠标的位置,
*/
x, y := getRealMove(100, 100, getScreenScale())
robotgo.Move(x, y) //直接移动到(100,100) Location的结果为 125,125,使用 getRealMove获取移动到想要的坐标的真实坐标,即可
robotgo.MoveSmooth(100, 100) //鼠标在屏幕上平滑移动
robotgo.MouseSleep = 100
time.Sleep(1 * time.Second)
//鼠标中键
robotgo.ScrollDir(10, "up") //Scroll滚动,direct 指令 (x, "up") supported: "up", "down", "left", "right"
robotgo.ScrollDir(20, "right")
robotgo.ScrollSmooth(-20, 6, 500) //每次向下20个单位,执行6次,每次间隔500ms,
robotgo.DragSmooth(10, 10) //从当前位置,平滑拖拽(按住鼠标左键)到目的位置
robotgo.Click("wheelRight") //点击
/* 支持的参数:
"left": C.LEFT_BUTTON, //默认,
"center": C.CENTER_BUTTON,
"right": C.RIGHT_BUTTON,
"wheelDown": C.WheelDown,
"wheelUp": C.WheelUp,
"wheelLeft": C.WheelLeft, //可以用在某些(但并非所有)带第二个滚轮或支持左右滚动的鼠标热键。在某些情况下,必须通过鼠标的自带软件包控制这个功能
"wheelRight": C.WheelRight,
*/
robotgo.Click("left", true)
robotgo.MoveClick(10, 20, "left")
fmt.Println(robotgo.Location()) //屏幕缩放 125%,输出为12(10*1.25),25(20*1.25))
}
2.3、keyboard
func keyboard() {
//1、按住win + r键,输入cmd,打开cmd窗口
robotgo.KeySleep = 200 //设置默认键盘输入等待时间
robotgo.KeyTap("r", "cmd") //按键操作,写法1
//k1 := []string{"r", "cmd"} //按键操作,写法2
//robotgo.KeyTap(k1)
robotgo.TypeStr("cmd") //输入字符
robotgo.KeyTap("enter")
//2、使用微信截图alt +a ,注意这里按住alt键后要释放,不然alt键会一直处于down的状态,影响使用
robotgo.KeyToggle("alt", "down")
robotgo.KeyToggle("a")
fmt.Println("alt 键被按住了")
robotgo.KeyToggle("alt", "up") //截屏操作结束后,才会执行下一个动作
time.Sleep(5 * time.Second)
fmt.Println("alt 键被释放")
//3、修改粘贴板
robotgo.WriteAll("Test") //修改粘贴板
text, err := robotgo.ReadAll()
if err == nil {
fmt.Println(text)
}
}
2.4、screen
func screen() {
x, y := robotgo.Location()
fmt.Println("pos: ", x, y)
color := robotgo.GetPixelColor(100, 200)
fmt.Println("color---- ", color)
sx, sy := robotgo.GetScreenSize() //获取屏幕大小
fmt.Println("get screen size: ", sx, sy)
bit := robotgo.CaptureScreen(10, 10, 30, 30) //截图
defer robotgo.FreeBitmap(bit)
img := robotgo.ToImage(bit)
imgo.Save("test.png", img)
num := robotgo.DisplaysNum() //获取显示器个数
for i := 0; i < num; i++ {
robotgo.DisplayID = i
img1 := robotgo.CaptureImg()
path1 := "save_" + strconv.Itoa(i)
robotgo.Save(img1, path1+".png")
robotgo.SaveJpeg(img1, path1+".jpeg", 50)
img2 := robotgo.CaptureImg(10, 10, 20, 20)
robotgo.Save(img2, "test_"+strconv.Itoa(i)+".png")
}
}
2.5、event
这里用的gohook,不知道为啥用的gohook,而不是自己的
func main() {
add()
low()
event()
}
func add() {
fmt.Println("--- Please press ctrl + shift + q to stop hook ---")
hook.Register(hook.KeyDown, []string{"q", "ctrl", "shift"}, func(e hook.Event) {
fmt.Println("ctrl-shift-q")
hook.End()
})
fmt.Println("--- Please press w---")
hook.Register(hook.KeyDown, []string{"w"}, func(e hook.Event) {
fmt.Println("w")
})
s := hook.Start()
<-hook.Process(s)
}
func low() {
evChan := hook.Start()
defer hook.End()
for ev := range evChan {
fmt.Println("hook: ", ev)
}
}
func event() {
ok := hook.AddEvents("q", "ctrl", "shift")
if ok {
fmt.Println("add events...")
}
keve := hook.AddEvent("k")
if keve {
fmt.Println("you press... ", "k")
}
mleft := hook.AddEvent("mleft")
if mleft {
fmt.Println("you press... ", "mouse left button")
}
}
2.6、windows
func windows() {
//很多动作在windows上无效,linux暂未测试
//1、消息提示框
//abool := robotgo.Alert("test", "robotgo", "ok", "cancel") //弹窗提示
//if abool { //abool为弹窗选择的是 确定还是取消
// fmt.Println("ok@@@ ", "ok")
// title := robotgo.GetTitle()
// fmt.Println("title@@@ ", title)
//}
//2、切换窗口
currid := robotgo.GetActive() //获取notepadd进程,并切换到noteepad++,输入"hello world"然后切换回来
fpid, err := robotgo.FindIds("notepad++")
if err != nil {
fmt.Println("获取notepadd++ id失败")
} else {
fmt.Println("fpid:", fpid)
robotgo.MilliSleep(500) //这里需要等待一段时间,不然看不到效果
robotgo.ActivePid(fpid[0])
robotgo.MilliSleep(500)
t := robotgo.GetTitle(fpid[0]) //试了没用
fmt.Println("notepad++ [title]:", t)
robotgo.MouseDown("left")
robotgo.TypeStrDelay("hello world", 500)
}
robotgo.MilliSleep(500)
robotgo.SetActive(currid)
//3、其他
/*
更多的windows窗口操作,需要调用windows api去看
*/
}
三、调用windows
windows api: https://learn.microsoft.com/en-us/windows/win32/apiindex/api-index-portal
用syscall
包与系统直接通信,不用用到CGO
。 然而,也有不方便的地方,如大多数的API,需要依赖不安全 (unsafe)
的内存管理
四、小工具
4.1、截屏小程序
var capPos struct { //保存位置
x, y, m, n int
}
func capturePix() {
fmt.Println("--- Please press ctrl + shift + q to stop hook ---")
hook.Register(hook.KeyDown, []string{"q", "ctrl", "shift"}, func(e hook.Event) { //如果捕获到 ctrl+shift+q 就执行println动作
fmt.Println("ctrl-shift-q")
hook.End()
})
hook.Register(hook.KeyDown, []string{"r", "ctrl"}, func(e hook.Event) { //
x, y := robotgo.Location()
color := robotgo.GetPixelColor(x, y)
fmt.Printf("%v 位置:[%v,%v] 颜色%v\n", time.Now().Format("2006-01-02 15:04:05"), x, y, color)
})
s := hook.Start() //返回一个chan Event类型
<-hook.Process(s) //hook.Process内置有一个循环,去捕获请求,并判断是否要执行hook
}
func screenCut() {
/* 需求
1、功能一:根据鼠标位置获取颜色信息,根据ctrl + r 判断是否要执行动作
2、功能二:截屏
1)ecs键撤销,重新截屏
2)ctrl+shift+x开始,ctrl+shift+e结束截屏
3)默认保存为png格式
3、ctrl+shift +q 键,全局退出
*/
abool := robotgo.Alert("screen cust tool...", "Screenshot or capture color information")
if !abool { //检测颜色和像素鼠标位置
capturePix()
return
} else {
fmt.Println("--- Please press ctrl + shift + q to stop hook ---")
hook.Register(hook.KeyDown, []string{"q", "ctrl", "shift"}, func(e hook.Event) { //如果捕获到 ctrl+shift+q 就执行println动作
fmt.Println("使用截屏功能,按住ctrl-shift-q 退出,ecs键重新截取")
hook.End()
})
hook.Register(hook.KeyDown, []string{"x", "ctrl", "shift"}, func(e hook.Event) { //开始截屏
fmt.Printf("[截屏开始],按住ctrl-shift-q 退出,ecs键重新截取\n")
robotgo.MilliSleep(200)
capPos.x, capPos.y = robotgo.Location()
fmt.Printf("[截屏开始] 开始位置:[%v,%v] %#v\n", capPos.x, capPos.y, capPos)
})
hook.Register(hook.KeyDown, []string{}, func(e hook.Event) { //捕获到ecs就重新开始
if _, ok := Unomal_key[e.Rawcode]; ok {
fmt.Printf("[截屏ECS重新开始],按住ctrl-shift-q 退出,ecs键重新截取\n")
if e.Rawcode == 27 {
capPos.x, capPos.y, capPos.m, capPos.n = 0, 0, 0, 0
}
}
})
hook.Register(hook.KeyDown, []string{"e", "ctrl", "shift"}, func(e hook.Event) { //结束截屏
capPos.m, capPos.n = robotgo.Location()
fmt.Printf("[截屏结束] %v 位置:[%v] \n", time.Now().Format("2006-01-02 15:04:05"), capPos)
cmap := robotgo.CaptureScreen(capPos.x, capPos.y, capPos.m, capPos.n)
defer robotgo.FreeBitmap(cmap)
img := robotgo.ToImage(cmap)
err := imgo.SaveToPNG(fmt.Sprintf("%v.png", time.Now().Format("20060102_150405")), img)
if err != nil {
fmt.Println("截屏失败:", err)
}
})
s := hook.Start() //返回一个chan Event类型
<-hook.Process(s) //hook.Process内置有一个循环,去捕获请求,并判断是否要执行hook
}
}
4.2、腾讯ocr
腾讯的比较坏算,免费
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ocr "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr/v20181119"
"io"
"os"
"strings"
)
func PrettyString(str string) (string, error) {
var prettyJSON bytes.Buffer
if err := json.Indent(&prettyJSON, []byte(str), "", " "); err != nil {
return "", err
}
return prettyJSON.String(), nil
}
func test(str string) {
fmt.Printf("[%v] will removed ...", str)
err := os.Remove(str)
if err != nil {
fmt.Println(err)
}
}
func main() {
//1、配置client对象
credential := common.NewCredential(
"ak1",
"sk2", //注意替换
)
// 实例化一个client选项,可选的,没有特殊需求可以跳过
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ocr.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ocr.NewClient(credential, "ap-guangzhou", cpf)
//2、初始化请求
request := ocr.NewGeneralBasicOCRRequest()
request.ImageUrl = common.StringPtr("https://ocr-demo-1254418846.cos.ap-guangzhou.myqcloud.com/card/IDCardOCR/IDCardOCR1.jpg")
// 返回的resp是一个GeneralBasicOCRResponse的实例,与请求对象对应
response, err := client.GeneralBasicOCR(request)
if _, ok := err.(*errors.TencentCloudSDKError); ok {
fmt.Printf("An API error has returned: %s", err)
return
}
if err != nil {
panic(err)
}
// 输出1:输出json格式的字符串回包
str, err := PrettyString(response.ToJsonString())
if err != nil {
fmt.Printf("出错了:%v\n", err)
}
fmt.Printf("%s", str)
//输出2:提取关键内容,内容太多了,很多不需要(借助临时文件,创建fileobj对象,遍历fileobj内容)
fileobj, err := os.CreateTemp(".", "temp-")
if err != nil {
fmt.Println("临时文件创建失败", err)
} else {
fmt.Println("临时文件创建成功")
}
count, err := fileobj.WriteString(str)
defer os.RemoveAll(fileobj.Name()) //defer的执行顺序是先进后出,这个执行顺序要在关闭后执行
defer fileobj.Close()
fmt.Println("NAME:", fileobj.Name())
if err != nil {
fmt.Println("临时文件写入失败", err)
} else {
fmt.Println("文件写入:", count)
}
//reader := bufio.NewReader(fileobj)
fobj2, _ := os.Open(fileobj.Name())
reader := bufio.NewReader(fobj2)
defer fobj2.Close()
for {
line, err := reader.ReadString('\n')
if err == io.EOF {
fmt.Println("文件读取完毕")
break
}
if err != nil {
fmt.Println("文件读取异常", err)
}
if strings.Contains(line, "DetectedText") {
fmt.Println(strings.TrimSpace(line))
}
}
}
五、chromedp
Package chromedp
is a faster, simpler way to drive browsers supporting the Chrome DevTools Protocol in Go without external dependencies.
The Chrome DevTools Protocol allows for tools to instrument, inspect, debug and profile Chromium, Chrome and other Blink-based browsers. Many existing projects currently use the protocol. The Chrome DevTools uses this protocol and the team maintains its API.
chromedp是go写的(可以通过使用cdp协议驱动chrome)库,cdp协议是驱动浏览器的一种协议,我们打开浏览器的开发者模式其实也是通过cdp协议和浏览器建立了链接。cdp本质上是一种websocket协议
默认chromedp为headless模式,看不到界面输出
- 自学地址:
https://pkg.go.dev/github.com/chromedp/chromedp
(版本 0.9.1) - cdp介绍:
https://chromedevtools.github.io/devtools-protocol/
注意:chromedp比较依赖context包,chromedp 学习比较重要的两个方面一个是context,一个是html元素查找器。
golang查找html元素,可以使用goquery或者htmlquery,htmlquery使用xpath选择器,goquery使用css选择器,但是htmlquery在github上用的并不多,goquery用的比较活跃,xpath相对简单好学,chrome开发者模式,右键“copy selector”对应的是css选择器
xpath完整的语法:https://github.com/antchfx/xpath ;xpath的页面选择器
- htmlquery - an XPath query package for HTML document
- xmlquery - an XPath query package for XML document.
- jsonquery - an XPath query package for JSON document
- goquery: 基于net/html包以及 CSS Selector library cascadia,类jQuery的特性
5.1、xpath
xpath (摘录自w3cschool)
xpath使用路径表达式在XML文档中进行导航,XPath 使用路径表达式来选取 XML 文档中的节点或者节点集。这些路径表达式和我们在常规的电脑文件系统中看到的表达式非常相似。
1.1.1、xpath节点
在 XPath 中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释以及文档(根)节点。XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore> #文档节点
<book>
<title lang="en">Harry Potter</title> #lang="en" 为属性节点 ,"Harry Potter" 为基本值,基本值及无父或无子的节点
<author>J K. Rowling</author> #元素节点,"J K. Rowling" 为基本值,基本值及无父或无子的节点
<year>2005</year>
<price>29.99</price>
</book>
</bookstore>
//book 元素是 title、author、year 以及 price 元素的父
title、author、year 以及 price 元素都是 book 元素的子
title、author、year 以及 price 元素都是同胞
title 元素的先辈是 book 元素和 bookstore 元素,上一级和上两级都是父
bookstore 的后代是 book、title、author、year 以及 price 元素
1.1.2、xpath语法
XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的。 下面列出了最有用的路径表达式:
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:
路径表达式 | 结果 |
---|---|
bookstore | 选取 bookstore 元素的所有子节点。 |
/bookstore | 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素。 |
//book | 选取所有 book 子元素,而不管它们在文档中的位置。 |
bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。 |
//@lang | 选取名为 lang 的所有属性。 |
提示:如果 XPath 的开头是一个斜线(/)代表这是绝对路径。如果开头是两个斜线(//)表示文件中所有符合模式的元素都会被选出来,即使是处于树中不同的层级也会被选出来。
1.1.3、谓语(Predicates)
谓语用来查找某个特定的节点或者包含某个指定的值的节点。
谓语被嵌在方括号中。
在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:
路径表达式 | 结果 |
---|---|
/bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素。 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
/bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
/bookstore/book[position()❤️] | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
//title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素。 |
//title[@lang='eng'] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
/bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
/bookstore/book[price>35.00]/title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
1.1.4、选取未知节点
XPath 通配符可用来选取未知的 XML 元素。
通配符 | 描述 |
---|---|
* | 匹配任何元素节点。 |
@* | 匹配任何属性节点。 |
node() | 匹配任何类型的节点。 |
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
---|---|
/bookstore/* | 选取 bookstore 元素的所有子元素。 |
//* | 选取文档中的所有元素。 |
//title[@*] | 选取所有带有属性的 title 元素。 |
1.1.5、选取若干路径
通过在路径表达式中使用"|"运算符,可以选取若干个路径。
路径表达式 | 结果 |
---|---|
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素。 |
//title | //price | 选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
1.1.6、XPath轴(Axes)
轴可定义相对于当前节点的节点集。
轴名称 | 结果 |
---|---|
ancestor | 选取当前节点的所有先辈(父、祖父等)。 |
ancestor-or-self | 选取当前节点的所有先辈(父、祖父等)以及当前节点本身。 |
attribute | 选取当前节点的所有属性。 |
child | 选取当前节点的所有子元素。 |
descendant | 选取当前节点的所有后代元素(子、孙等)。 |
descendant-or-self | 选取当前节点的所有后代元素(子、孙等)以及当前节点本身。 |
following | 选取文档中当前节点的结束标签之后的所有节点。 |
namespace | 选取当前节点的所有命名空间节点。 |
parent | 选取当前节点的父节点。 |
preceding | 选取文档中当前节点的开始标签之前的所有节点。 |
preceding-sibling | 选取当前节点之前的所有同级节点。 |
self | 选取当前节点。 |
1.1.7、xpath运算符
运算符 | 描述 | 实例 | 返回值 |
---|---|---|---|
| | 计算两个节点集 | //book | //cd | 返回所有拥有 book 和 cd 元素的节点集 |
+ | 加法 | 6 + 4 | 10 |
- | 减法 | 6 - 4 | 2 |
* | 乘法 | 6 * 4 | 24 |
div | 除法 | 8 div 4 | 2 |
= | 等于 | price=9.80 | 如果 price 是 9.80,则返回 true。如果 price 是 9.90,则返回 false。 |
!= | 不等于 | price!=9.80 | 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。 |
< | 小于 | price<9.80 | 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。 |
<= | 小于或等于 | price<=9.80 | 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。 |
> | 大于 | price>9.80 | 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。 |
>= | 大于或等于 | price>=9.80 | 如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。 |
or | 或 | price=9.80 or price=9.70 | 如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。 |
and | 与 | price>9.00 and price<9.90 | 如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。 |
mod | 计算除法的余数 | 5 mod 2 | 1 |