跳至主要内容

QtWebkit开发爬虫

由来

本文主要介绍以下动态网页爬取方法中的一种,在web2.0时代,很多都使用异步加载技术。导致有用信息获取不到,那怎么办了?不要慌,肯定是有办法的。 别废话了。QtWebKit是安装了Qt后内置封装好的浏览器内核。需要详细了解的可以到Qt官网查看,有了浏览器内核后,就可以通过它实现分析ajax的连接,或者js事件。可能会有人会说,不是可以使用现在google出的HeadlessChrome。或者selenium操作浏览器或phantom.js。我觉得你还是太年轻,比如下面这种情况,你怎么解决? (逃~,如果你有什么好的方法一定记得告诉我)
$(document).ready(function(){
  // 真假浏览器检查
  var detect = false;// 默认大家都是好学生,没有被老师发现上课时间在完爬虫……
  if (navigator.webdriver || window.webdriver) {
    detect = true;// 你竟然是用webdriver驱动的!Headless Chrome 同学,你被老师发现啦……
  }
  if (window.outerWidth === 0 || window.outerHeight === 0){ // 窗口的外部宽度和高度为零的同学,你很值得怀疑哦……
    try {
      var canvas = document.createElement('canvas');
      var ctx = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
      var exts = ctx.getSupportedExtensions();
    }
    catch (e) {
      detect = true; // 你竟然不支持WebGL!PhantomJS 同学,你被老师发现啦……
    }
  }
  // 好啦,Headless Chrome 和 PhantomJS 两位同学已经被老师罚站去了。其他同学要好好听课哦!
  if (detect) {
    var a = '4c59b57a97001c48';var b = 'b1522652fa3c5d1e';var c = 'e32b6f4cad197580';
  } else {
    var a = 'b1522652fa3c5d1e';var b = 'e32b6f4cad197580';var c = '4c59b57a97001c48';
  }
  // 使用一次性密码进行AES加密解密。
  // a同学的作业是一串密钥,b同学的作业是一串初始向量,c同学的作业是了一串验证码。
  var x = CryptoJS.enc.Latin1.parse(a);
  var y = CryptoJS.enc.Latin1.parse(b);
  // abc三位同学一起去老师那里提交作业。
  $.post("data.php",{a:a,b:b,c:c},function(data){
    // 老师检查了他们的作业,如果大家都写对了,就奖励他们一个包装好的奖品。
    var dec = CryptoJS.AES.decrypt(data,x,{iv:y,padding:CryptoJS.pad.Pkcs7,mode:CryptoJS.mode.CBC});
    // 拿到奖品后,用a同学的密钥和b同学的初始向量就可以打开包装啦。原来奖品就是正常可用的账号啊!
    var tbdt = $.parseJSON(dec.toString(CryptoJS.enc.Utf8));
      .......
还来看看知乎大神们是怎么解决的,selenium爬虫被检测到 该如何破?
是不是发现没有几个答到重点,其实如果不需要效率,我可以针对上面这个问题提供几个方案:
  • 使用qtwebkit
  • 油猴脚本
  • Electron 的爬虫框架 Nightmare
  • c# webbrowser
  • 修改被检测特征或设置chrome
就例举上面这几种吧,其实还有很多种方案,就不一一列举了。

实现

看了上面的介绍,差不多就是要我们实现一个简单版的浏览器才可以解决。qtwebkit可以胜任,但是这里需要注意qtwebkitHTML5支持不好。后续我会使用Qt新出的qtwebengine组件。因为qtwebengine组件的资料比较少,差不多只有看官方文档,最主要还是c++的。所以我就来说说QtWebkit

QtWebkit开发爬虫准备

使用QtWebkit开发爬虫。主要使用里面的两个类:
QWebView 主要是一个浏览器的容器,操作浏览器的各种交互等
QNetworkAccessManager
是一个网络请求管理类,将我们的浏览器和其绑定后,我们的所有浏览器执行的请求都会被这里类管理,譬如我们需要改造请求头或者获取相应头等操作都需要用到这个类,代理设置和cookie之类。

Qwebview 类

上面描述的这个类是关于浏览器的交互,其实主要是关于QtWebkit进行js解析。有一个问题需要注意:这个类没有自带网页访问超时函数,还有如果有重定向跳转,那么不会获取第一次得到重定向的网页,就需要使用qtimer进行延时。这是因为访问网页返回状态码为 200 的链接就会触发一个函数-->_finished函数代表完成。那么问题就来了。我们一般只要获取返回码为200代表成功。但是重定向时,我们需要得到重定向的网页,那我们就不知道重定向后的网页后又要调用一次**_finished**函数。处理的时候需要注意
def download(self, url, timeout=60):
    '''等待下载完成并返回'''
        body = ""
        loop = QEventLoop()
        timer = QTimer()
        timer.setSingleShot(True)
        timer.timeout.connect(loop.quit)
        self.load(QNetworkRequest(QUrl(url)),
                  QNetworkAccessManager.GetOperation, body)
        timer.start(timeout * 1000)
        # 等待加载完成
        loop.exec_()
        if timer.isActive():
            # 下载成功
            timer.stop()
            # return self.page().mainFrame().toHtml()
            return self.page().mainFrame().toPlainText()
        else:
            print ("Request time out:"+ str(url.url().toString()))
以下是对webview的属性设置
def setSettings(self):
        # 以下是对webview的属性设置,
        # 第一个是设置访问远程url,当使用ajax请求的时候,这个就派上用上
        self.page().settings().setAttribute(
            QWebSettings.LocalContentCanAccessRemoteUrls, True)
        # 下面两句是利用html5的本地存储功能来帮助我们存储程序运行中要保存的数据
        self.page().settings().setAttribute(
            QWebSettings.LocalStorageDatabaseEnabled, True)  # 开始本地存储
        self.page().settings().setLocalStoragePath("html/")  # 存储的路径
        # 是否加载js
        self.page().settings().setAttribute(QWebSettings.JavascriptCanOpenWindows, True)
        # 自动禁用图像下载
        self.page().settings().setAttribute(QWebSettings.AutoLoadImages, False)

        # 如何存储cookie呢,并且利用cookiejar
        self.page().networkAccessManager().setCookieJar(QNetworkCookieJar(self))
        # for cookie in app.mcookiejar.allCookies():
        #     print cookie.name()
        # name是setCooke(name, val)中的name,在请求时候,可以将cookie放在header中发出请求

        # mOBJ = AdamOBJ()
     ##python调用js
        # self.page().mainFrame().evaluateJavaScript(QString('alert("hello
        # wolrd")')) 
  
        # js调用python
        # self.page().mainFrame().javaScriptWindowObjectCleared.connect(lambda: self.page(
        # ).mainFrame().addToJavaScriptWindowObject(QString('mOBJ'), mOBJ))  

        # 加入js代码
     self.page().mainFrame().evaluateJavaScript(open('jquery.js').read())
        self.page().mainFrame().evaluateJavaScript(jsstr)
        # 设置默认编码
        self.settings().setDefaultTextEncoding("utf-8")
有需要在发起请求时执行某些操作,或者在执行完了执行某些操作。那么需要加入下面函数。
# load成功时候执行。self._loadStarted 是开始的回调函数
self.loadStarted.connect(self._loadStarted)
# 成功渲染时候执行。self._loadFinished 是开始的回调函数
self.loadFinished.connect(self._loadFinished)
我们一般在load成功后,需要上面操作了?一般都是让程序阻塞js代码去掉
def _loadStarted(self):  
    '''load完成'''
    frame = self.page().mainFrame()    
    frame.evaluateJavaScript("window.alert=function(){}")    
    frame.evaluateJavaScript("window.confirm=function(){returntrue}")    
    frame.evaluateJavaScript("window.prompt =function(){return 0}")    
    frame.evaluateJavaScript("window.open =function(){}")
渲染后_loadFinshed函数处理的事件就比较多了,爬虫一般就在这时候抽取数据。绝大多数网页都会使用javascript处理页面,还有可能使用Ajax技术异步加载。触发事件后才发起或者生成的链接和数据。一般有下面几种:
  • js动态解析
    这个在self._loadStarted函数执行完后,webkit内核执行javascript代码生成出html代码。也静态方式获取动态生成的链接及数据
  • 自动交互
    模仿人的行为来加载数据,比如我们点击鼠标或者鼠标往下拉动加载数据。
  • 自动分析表单
    自动识别出action中的值为所提交的地址,提取input标签中的name和value作为参数,生成带有参数的链接。
  • hook网络请求
    这是一个ajax请求,有别于以上3种基于dom树解析的分析技术,要捉到其请求的url只能通过hook请求,hook住每一个由webkit发送出去的请求,从而拿到了 请求链接
# 给<a>标签设置onclick事件
'''
selectdom=document.querySelectorAll("a");
for(var i= 0; i< selectdom.length; i ++){
 if(!selectdom[i].getAttribute("onclick")){
 selectdom[i].setAttribute("onclick",selectdom[i].getAttribute("href"))
 }
}
'''
# onchange 事件在用户改变输入域的内容时执行 JavaScript 代码
'''
selectdom=document.querySelectorAll("[onchange]");
for(var i= 0; i< selectdom.length; i ++){
 try{
  selectdom[i].onchange();
  }catch(err){
  continue;
 }
}
'''
# onclick 事件在用户点击时执行 JavaScript 代码
'''
selectdom=document.querySelectorAll("[onclick]");
for(var i= 0; i< selectdom.length; i ++){
 try{
  selectdom[i].onclick();
  }catch(err){
  continue;
 }
}
'''
# onfocus 事件在对象获得焦点时执行 JavaScript 代码
'''
selectdom=document.querySelectorAll("[onfocus]");
for(var i= 0; i< selectdom.length; i ++){
 try{
  selectdom[i].onfocus();
  }catch(err){
  continue;
 }
}
'''
# onmouseout 事件在用户鼠标指针移出指定的对象时执行 JavaScript 代码
'''
selectdom=document.querySelectorAll("[onmouseout]");
for(var i= 0; i< selectdom.length; i ++){
 try{
  selectdom[i].onmouseout();
  }catch(err){
  continue;
 }
}
'''
# onmouseover 事件在用户鼠标指针移动到指定的对象时执行 JavaScript 代码
'''
selectdom=document.querySelectorAll("[onmouseover]");
for(var i= 0; i< selectdom.length; i ++){
 try{
  selectdom[i].onmouseover();
  }catch(err){
  continue;
 }
}
'''
# 还有其他事件,以后在添加。。。。。。

def _loadFinished(self):
    '''渲染完成'''
    self.loop.quit()
    frame = self.page().mainFrame()
    # 事件先按一遍 然后到parse处理
    frame.evaluateJavaScript('selectdom=document.querySelectorAll("a");for(var i= 0; i< selectdom.length; i ++){if(!selectdom[i].getAttribute("onclick")){selectdom[i].setAttribute("onclick",selectdom[i].getAttribute("href"))}}')

    frame.evaluateJavaScript('selectdom=document.querySelectorAll("[onerror]");for(var i= 0; i< selectdom.length; i ++){try{selectdom[i].onerror();}catch(err){continue;}}')

    frame.evaluateJavaScript('selectdom=document.querySelectorAll("[onchange]");for(var i= 0; i< selectdom.length; i ++){try{selectdom[i].onchange();}catch(err){continue;}}')

    frame.evaluateJavaScript('selectdom=document.querySelectorAll("[onclick]");for(var i= 0; i< selectdom.length; i ++){try{selectdom[i].onclick();}catch(err){continue;}}')

    frame.evaluateJavaScript('selectdom=document.querySelectorAll("[onfocus]");for(var i= 0; i< selectdom.length; i ++){try{selectdom[i].onfocus();}catch(err){continue;}}')

    frame.evaluateJavaScript('selectdom=document.querySelectorAll("[onmouseout]");for(var i= 0; i< selectdom.length; i ++){try{selectdom[i].onmouseout();}catch(err){continue;}}')

    frame.evaluateJavaScript('selectdom=document.querySelectorAll("[onmouseover]");for(var i= 0; i< selectdom.length; i ++){try{selectdom[i].onmouseover();}catch(err){continue;}}')

QNetworkAccessManager 类

这个类是关于网络方面的,需要使用这个类时候,我们需要把他和webkit内核绑定到一起。这样才能对网络进行监控拦截和修改,一般使用setNetworkAccessManager(manager)函数进行设置绑定,QNetworkAccessManager 类可以满足爬虫中的获取headerproxy等设置。进行网络拦截控制主要重写QNetworkAccessManager类中的createRequest函数,顾名思义就知道这函数是用来创建网络请求,_finished函数后获取状态。
def setProxy(self):
        # 可以使用https 代理
        QSslSocket.supportsSsl()
        proxy = QNetworkProxy()
        # Http访问代理
        proxy.setType(QNetworkProxy.HttpProxy)
        # proxy.setType(QtNetwork.QNetworkProxy.DefaultProxy)
        # proxy.setType(QNetworkProxy.Socks5Proxy)
        proxy.setHostName('localhost')
        proxy.setPort(1337)
        proxy.setUser("wushuang5112")
        proxy.setPassword("123456")
        QNetworkProxy.setApplicationProxy(proxy)
def requests(self, url, headers):
        req = QNetworkRequest(QUrl(url))
        req.setRawHeader(
            "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:59.0) Gecko/20100101 Firefox/59.0")
        # for header in headers:
        #     val = headers[header]
        #     req.setRawHeader(header, val)
        # req.setUrl(QUrl("http://qt.nokia.com"))
        # req.setRawHeader("User-Agent", "MyOwnBrowser 1.0")
        return req
可以用来进行检测URL 重定向等。
url = str(reply.url().toString())
# 获得返回状态
status =reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)    
status, ok = status.toInt()
在爬虫中我们是不是有什么还没有说,那就是Cookie的设置和获取,那怎么实现了
设置Cookie
def setCookie(self, my_cookie_dict):
        # 将字典转化成QNetworkCookieJar的格式
        self.cookie_jar = QNetworkCookieJar()
        cookies = []
        for key, values in my_cookie_dict.items():
            my_cookie = QNetworkCookie(QByteArray(key), QByteArray(values))
            my_cookie.setDomain('.baidu.com')
            cookies.append(my_cookie)
        self.cookie_jar.setAllCookies(cookies)
        # 如果没有在前面设置domain,那么可以在这里指定一个url作为domain
        # self.cookie_jar.setCookiesFromUrl(cookies,
        # QUrl('https://www.baidu.com/'))

        # 最后cookiejar替换完成
        self.page().networkAccessManager().setCookieJar(self.cookie_jar)
获取Cookie
# 在QWebView中使用下面代码  
cookies = []  
for citem in self.page().networkAccessManager().cookieJar().cookiesForUrl(QUrl('http://www.baidu.com')):  
    cookies.append('%s=%s' % (citem.name(), citem.value()))  
    cookies = common.to_unicode('; '.join(cookies))  
print (cookies) 

# 删除cookie
def deleteCookie(self,cookieList):
    cookie = []
    self.mainWindow.settings.value(cookie)

QtWebkit开发爬虫实现

上面提过,Webkit对现在的Html5支持不是很好。还有如果在使用服务器运行爬虫,那么需要一个视窗系统才可以正常使用。 那么怎么解决这些问题,对Html5的支持可以使用Qt高版本的qtwebengine组件。使用xvfb虚拟出一个视窗系统,只需把alert, confirm, prompt的代码注释掉(因为会让浏览器阻塞),简单快捷稳定。
其实还有很多方法解决Selenium + Webdriver能被识别的特征。后续继续介绍。

评论