0%

PyQt - QWebEngineView知识点

最近有个个人需求,需要在网页上频繁下单,期间还要有一些网页操作行为,于是想把操作自动化。一个简单的方式是启动一个自定义的浏览器,通过hook一些网页响应,来实现自动化。

于是,把N多年没碰的Qt又捡了起来,Qt是一个跨平台的C++图形用户界面应用程序框架,它提供给开发者建立图形用户界面所需的功能,广泛用于开发GUI程序。在最新的Qt5中,有一个QWebEngineView类,基于Chromium的引擎,可以利用这个来实现自己的浏览器。

而PyQt是一个创建GUI应用程序的工具包。它是Python编程语言和Qt库的成功融合,鉴于已经好几年没碰C++了,加上工程量也不大,于是PyQt就是首选了。

本文不是PyQt的基础教程,本文只会将QWebEngineView中一些常用的技术点罗列出来。基础教程可以看这里

浏览器窗口

首先要把浏览器窗口跑起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtNetwork import *
from PyQt5.QtWidgets import *
from PyQt5.QtWebEngineWidgets import *


class Window(QWidget):
def __init__(self):
super(QWidget, self).__init__()

# 新建一个浏览器对象
self.browser = QWebEngineView()

# 放一个“刷新”按钮,点击可以刷新页面
self.refresh_btn = QPushButton('刷新')
self.refresh_btn.clicked.connect(self.reload)

# 创建一个横向布局
h_box = QHBoxLayout()
h_box.addStretch(0)
h_box.addWidget(self.refresh_btn)
# 创建一个纵向布局
v_box = QVBoxLayout()
v_box.addWidget(self.browser)
v_box.addLayout(h_box)

# 设置窗口属性
self.setWindowTitle('PyQt Example')
self.setGeometry(5, 30, 1400, 700)
self.setLayout(v_box)
self.show()

# 加载页面
self.reload()

def reload(self):
self.browser.load(QUrl('https://www.baidu.com'))


if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
app.exit(app.exec_())

代码比较简单,就不过多解释了,效果如下:

1

页面有了,但是如果要做到自动化的话,期望在网页加载完成之后,就做一些事情,那么可以监听QWebEngineView.loadFinished信号:

1
2
3
4
5
6
7
8
class Window(QWidget):
def __init__(self):
# ...
self.browser = QWebEngineView()
self.browser.loadFinished.connect(self.load_finished)

def load_finished(self):
QMessageBox.information(self, "Information", "网页加载完成了!")

再次运行,会在网页加载之后弹出一个提示框。不过有些网页内容是延迟加载的,用loadFinished信号并不能解决所有问题,下面还有其他方式。

执行自定义JS

执行自定义JS,是个自动化过程中,极有用的方式,比如网页加载之后,我需要在输入框中自动输入“hello word”:

1
2
3
4
5
6
7
class Window(QWidget):
def load_finished(self):
# query by id
value = 'document.getElementById("kw").value = "hello word"'
# query by css
# document.getElementsByClassName("s_ipt")[0].value = 'hello word'
self.browser.page().runJavaScript(value)

其中,“kw”是网页中输入框的id,如果没有id,也可以用getElementsByClassName,通过class来查询元素。

runJavaScript函数还提供了一个带回调的版本,执行的JS的返回值可以通过回调的方式来实现:

1
2
3
4
5
6
7
class Window(QWidget):
def load_finished(self):
value = "document.getElementsByClassName('s_ipt').length"
self.browser.page().runJavaScript(value, self.callback)

def callback(self, count):
# get length here

注意,执行input.value=xxx是无法触发对应元素的事件的,可以用如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
window.inputValue = function (dom, st) {
var evt = new InputEvent('input', {
inputType: 'insertText',
data: st,
dataTransfer: null,
isComposing: false
});
dom.value = st;
dom.dispatchEvent(evt);
}

window.inputValue(document.querySelector('input'),'输入要赋值的内容')

先用runJavaScript执行上面一段,定义函数,然后在需要的地方执行下面这句话。

引用自:js模拟实现输入框input事件

元素监控

网页中有些内容可能是延时加载出来的,在loadFinished信号发出来时,并没有加载出来,有个偏方:定义一个线程,不断执行JS去查询元素是否已经加载了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Window(QWidget):
def __init__(self):
# ...
thread = threading.Thread(target=self.auto_detect)
thread.start()

def auto_detect(self):
value = "document.getElementsByClassName('s_ipt').length"
self.browser.page().runJavaScript(value, self.callback)

def callback(self, count):
if count is None:
return

if int(count) > 0:
# do something

不知道有不有啥正规方法监听响应。

Cookie操作

网页登陆之后,一般都会将鉴权信息保存到Cookie中。

获取Cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyWebEngineView(QWebEngineView):
def __init__(self, *args, **kwargs):
super(MyWebEngineView, self).__init__(*args, **kwargs)
QWebEngineProfile.defaultProfile().cookieStore().cookieAdded.connect(self.on_cookie_add)

def on_cookie_add(self, cookie):
name = cookie.name().data().decode('utf-8')
value = cookie.value().data().decode('utf-8')
print(name + ' : ' + value)


class Window(QWidget):
def __init__(self):
# ...
self.browser = MyWebEngineView()

实现一个自定义的QWebEngineView,将cookieAdded信号绑定起来,就可以获取网页加载中的Cookie。

设置Cookie

1
2
3
4
5
class Window(QWidget):
def set_cookie(self, k, v, domain):
cookie = QNetworkCookie(QByteArray(k.encode()), QByteArray(v.encode()))
cookie.setDomain(domain)
self.browser.page().profile().cookieStore().setCookie(cookie)

删除Cookie

删除可以使用QWebEngineCookieStore::deleteAllCookies删除所有Cookie,或者使用QWebEngineCookieStore::deleteCookie删除指定Cookie。

子iFrame交互

网页中经常会嵌入一些iFrame,如果是同源的话,一般通过document.getElementById('${iframe_id}').contentWindow.document.getElementsByClassName("${css}").length来获取信息,但是如果不同源的话,这样执行会报错:

Blocked a frame with origin from accessing a cross-origin frame…

可以利用QWebEngineScript,在子iFrame中来执行JS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def auth(self):
value = """
function wechatAuthFunc() {
if (document.getElementsByClassName("${css}").length == 1) {
alert('Get it')
}
}
setInterval(wechatAuthFunc, 500);
"""
script = QWebEngineScript()
script.setSourceCode(value)
script.setInjectionPoint(QWebEngineScript.DocumentReady)
script.setRunsOnSubFrames(True)
self.browser.page().scripts().insert(script)

最后

  • 除了本文的内容之外,这里也有一些PyQt处理QWebEngineView的知识点。
  • Qt的设计水准在业界很有口碑,一致、易于掌握和强大的API是Qt最著名的优点之一,可以阅读API设计原则 – QT官网的设计实践总结了解详情。



-=全文完=-