QWebEngineView修改请求并获取响应
2019-10-07 Programming在自己的Qt程序中想通过QQ邮箱的通讯录来获取好友列表,考虑到登录过程的不确定性,希望效果是在浏览器中打开QQ邮箱登录页面,用户手动登录后浏览器窗口自动关闭,然后程序再请求需要的内容。
好友列表获取过程
打开浏览器控制台,登录QQ邮箱后可以在网络请求中找到一个请求地址为https://mail.qq.com/cgi-bin/laddr_lastlist
的请求,响应内容中的有效信息包括好友邮箱地址、昵称、备注、分组。
测试发现url中t=addr_datanew&category=hot&sid=0123456789abcdef
和cookie中的sid
以及header中的Referer
是必需的,其他的内容都可以省略。
响应内容并不是常用的json格式,观察后发现更像是用js代码声明了一个对象放在括号里,双引号会被转义为\x26quot;
,其他的emoji表情,unicode字符也是被转义为HTML实体后用\x26
代替&
,\
会被转义为\x5c
。
为什么要用QWebEngineView?
Qt涉及到网络请求的话最先想到的应该是QNetworkAccessManager,现有程序中的下载队列用的就是这个。
但是概述中提到了”登录过程的不确定性“,具体来说就是我使用QQ帐号登录的时候,最理想的情况是点击登录后就登录成功,但实际情况下偶尔还会弹出滑块验证码,异地登录还有手机短信验证码,只是单次登录不需要记住密码的话直接扫二维码会更方便,因此使用QNetworkAccessManager就会有各种各样的情况要考虑,而且还不知道是否有情况遗漏。
于是就想到了之前在Qt中用过的WebEngine。Qt中的WebEngine是基于Chromium项目的,加上它感觉给项目内置了个浏览器进去,不过考虑到私有项目就自己一个人用,体积大点也不会有太大影响。
如何用QWebEngineView?
参考Qt WebEngine Widgets Module的结构图:
QWebEngineView处于最上面的一层,提供的API也比较少,功能比较丰富的API在QWebEnginePage这层。
根据前面的好友列表获取过程,理想的情况就是弹出浏览器窗口 -> 用户登录成功 -> 页面跳转 -> 浏览器截取目标网络响应 -> 关闭浏览器
,页面跳转可以通过QWebEngineView的urlChanged()
事件检测到,但没找到合适的方法准确地截取某个请求的响应,只能换一个思路:弹出浏览器窗口 -> 用户登录成功 -> 页面跳转 -> 隐藏浏览器窗口 -> 使浏览器主动发送目标请求 -> 获取浏览器当前内容 -> 关闭浏览器
,主动发送请求可以调用QWebEngineView的load()
,获取浏览器当前内容则是用QWebEnginePage的toHtml()
。
但在实现时还是遇到了坑。
QWebEngineHttpRequest无法设置Referer
主动发送目标请求时需要构造QWebEngineHttpRequest对象,url中的sid可以从登录后跳转url获取,同一个QWebEngineView对象的cookie会自动维护,不需要手动设置,因此就剩下了header中的Referer需要处理。通过QWebEngineHttpRequest提供的setHeader()
方法设置Referer,结果请求还是提示”禁止GET方法调用“。本地nc -l -p 8000
进行监听,代码中改为请求本地,发现设置的Referer无效,请求头中并没有Referer项,换成随意的其他项,如setHeader("abc", "123")
,却可以正常携带该项。感觉像是bug或者WebEngine自身限制,但未找到具体说明。
不过在找相关说明的过程中找到了一个解决办法Referrer HTTP Header no longer ignored when set via RequestInterceptor,通过QWebEnginePage的setUrlRequestInterceptor()
设置QWebEngineUrlRequestInterceptor拦截发出的目标请求,在这里加上Referer,可以成功发送并得到响应。
toHtml()返回空字符串
获取当前页面上的内容,QWebEnginePage提供了toHtml()
和toPlainText()
两个函数,toHtml()
读取原始html,toPlainText()
会忽略掉所有html格式。需要注意的是这两个函数的用法有点特殊,他们都是异步函数,返回值为空,需要传入一个回调函数,通过回调函数获得从当前页面读取到的内容,最方便的写法就是在这里使用lambda表达式,例如:
this->page()->toHtml([](const QString &content) {
qDebug() << content;
}
但是运行之后发现qDebug()
的输出一直为空,在stackoverflow看到一个例子是将QWebEnginePage的loadFinished()
事件绑定到了一个包含toHtml()
的函数上,尝试之后发现有效。也就是说toHtml()
并不会判断页面是否加载完毕,需要在合适的时间调用才可以成功读取。
修改完毕后,发现toHtml()
偶尔还是会返回空字符串,于是尝试在接收到loadFinished()
事件后使用QTimer设定一个定时器,在100ms后再读取内容,此时不再出现空字符串的情况。
示例代码
WebEngineView.h
#ifndef WEBENGINEVIEW_H
#define WEBENGINEVIEW_H
#include <QTimer>
#include <QWebEngineView>
#include <QWebEngineProfile>
#include <QWebEngineCookieStore>
#include <QWebEngineUrlRequestInterceptor>
class MyInterceptor : public QWebEngineUrlRequestInterceptor
{
Q_OBJECT
public:
MyInterceptor(QObject *p = nullptr):QWebEngineUrlRequestInterceptor(p){}
void interceptRequest(QWebEngineUrlRequestInfo &info) {
if(info.requestUrl().toString().contains("https://mail.qq.com/cgi-bin/laddr_lastlist"))
info.setHttpHeader("Referer", "https://mail.qq.com/");
}
};
class WebEngineView : public QWebEngineView
{
Q_OBJECT
public:
WebEngineView(QWidget *p = nullptr):QWebEngineView(p){}
void run(void) {
this->page()->profile()->cookieStore()->deleteAllCookies();
MyInterceptor *interceptor = new MyInterceptor(this);
this->page()->setUrlRequestInterceptor(interceptor);
connect(this, SIGNAL(urlChanged(const QUrl &)),
this, SLOT(urlChangedSlot(const QUrl &)));
connect(this, SIGNAL(loadFinished(bool)),
this, SLOT(loadFinishedSlot(bool)));
this->load(QUrl("https://mail.qq.com/"));
this->show();
}
private slots:
void urlChangedSlot(const QUrl &url) {
QString u = url.toString();
if(u.contains("https://mail.qq.com/cgi-bin/frame_html")) {
int from = u.indexOf("sid=") + 4;
QString sid = u.mid(from, u.indexOf("&", from) - from);
this->load(QUrl("https://mail.qq.com/cgi-bin/laddr_lastlist?sid="
+ sid + "&t=addr_datanew&category=hot"));
}
}
void loadFinishedSlot(bool ok) {
if(ok&&this->url().toString().contains("https://mail.qq.com/cgi-bin/laddr_lastlist"))
QTimer::singleShot(100, this, SLOT(getContentSlot()));
}
void getContentSlot() {
this->page()->toPlainText([](const QString &content) {
qDebug() << content;
});
}
};
#endif // WEBENGINEVIEW_H
main.cpp
#include <QApplication>
#include "WebEngineView.h"
int main(int argc, char* argv[]) {
QApplication a(argc, argv);
WebEngineView w;
w.run();
return a.exec();
}
test.pro
QT += core gui widgets webenginewidgets
CONFIG += c++11
SOURCES += main.cpp
HEADERS += WebEngineView.h