一、cefpython3的结构和使用

cefpython3 :https://github.com/cztomczak/cefpython

1.1 cef进程结构

CEF3是多进程架构的,CEF3进程主要有一个Browser(浏览器)进程和多个Renderer(渲染)进程。Browser被定义为主进程,负责窗口管理,网络请求,网页管理 、网络交互。browser从服务器器请求到了响应,将html文本发送给Renderer 进程,render进程加载html,进行渲染,展示网页的内容;除此之外,Renderer进程还负责Js Binding和对Dom节点的访问。Browser和Renderer进程可以通过发送异步消息进行双向通信。

一个浏览器有很多个CefBrowser窗口,这些窗口都是在Browser进程中创建。browser进程用来管理和处理回调函数消息

Renderer进程用来实现网页的渲染,每个renderer进程包含有一个主网页mainframe和多个子网页subframe

1.2 Renderer进程实现结构

renderer程序继承CefAppCefRenderProcessHandler类,在main函数中初始化。通过CefSettings.browser_subprocess_path配置render可执行程序路径。browser进程就会去启动这个进程去渲染网页

CEF Python 版本 cefpython3开发_python

1.3 brower进程实现结构

browserapp要继承CefAppCefBrowserProcessHandler类。实现browserapp的定义。同时要新建clienthandler类实现图中的回调函数接口类,用来处理拦截响应请求、管理生命周期、下载、显示加载、右键菜单等。在mian函数中初始化、启动消息循环。调用CefBrowserHost的静态方法创建browser窗口对象,在render进程的Frame中加载渲染内容。

CEF Python 版本 cefpython3开发_chrome_02


Browser进程中包含如下主要的线程:

TID_UI 线程是浏览器的主线程。如果应用程序在调用调用CefInitialize()时,传递 CefSettings.multi_threaded_message_loop=false,这个线程也是应用程序的主线程。

TID_IO线程主要负责处理IPC消息以及网络通信

TID_IO线程负责与文件系统交互

1.4 基本使用

brower的创建:初始化创建、开启、消息循环、结束进程

from cefpython3 import cefpython as cef
import sys
# 替换python预定义异常处理逻辑,为保证异常发生时能够结束所有进程
sys.excepthook = cef.ExceptHook  
# 创建浏览器
cef.Initialize()
#开启浏览器
cef.CreateBrowserSync(url="https://www.cc.163.com")
# 消息循环:监听信号和处理事件
cef.MessageLoop()
# 结束进程
cef.Shutdown()

1.5 窗口嵌入Brower

在Qwidget嵌入brower时,基本步骤是:
1、获取窗口组件

windowInfo = cefpython.WindowInfo()

2、嵌入适配环境

windowInfo.SetAsChild(widget)

3、绑定窗口、初始化浏览器环境

cefpython.CreateBrowserSync(windowInfo, browserSettings, navigateUrl)

4、加上brower的基本创建过程,brower初始化、消息循环、结束进程等。
注意
其中 cef.Initialize()cef.Shutdown() 需要在UI线程中进行
cefpython3目前并没有实现cef的全部接口,仍在更新中,相对体积较大,但安装方便,可以提高开发效率。

1.6 cefpython3实例

example one :点击创建Brower,无需设置父控件

from cefpython3 import cefpython as cef
import platform
import sys
from PyQt5.QtWidgets import  *
import  os

class CefMainWindow(QWidget):
    def __init__(self):
        super(CefMainWindow, self).__init__()
        self.initUi()

    def initUi(self):
        self.setWindowTitle("test")
        self.resize(400,300)

        layout = QVBoxLayout(self)

        self.btn1 = QPushButton(self)
        self.btn1.setText('进入Cc网页')
        self.btn1.clicked.connect(self.jumpToCc)

        self.btn2 = QPushButton(self)
        self.btn2.setText('进入百度网页')
        self.btn2.clicked.connect(self.jumpToBaidu)

        layout.addWidget(self.btn1)
        layout.addWidget(self.btn2)

    def jumpToCc(self):
        cef.Initialize()
        cef.CreateBrowserSync(url= 'https://cc.163.com')
        cef.MessageLoop()

    def jumpToBaidu(self):
        cef.Initialize()
        cef.CreateBrowserSync(url='https://baidu.com')
        cef.MessageLoop()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    cefWindow = CefMainWindow()
    cefWindow.show()
    sys.exit(app.exec_())
    cef.shutdown()

example Two: 在Qwidget中创建brower,同时实现搜索功能

"""
将cefpython3嵌入到PyQt5中,往输入框中输入URL地址,点击查询,创建浏览器并加载HTML内容显示
"""
from PyQt5 import QtWidgets
from cefpython3 import cefpython as cef
import sys
import os
# 浏览器内容窗口
class CefBrowser(QtWidgets.QWidget):
    def __init__(self, parent=None):
        self.browser = None
        super().__init__(parent)

    def create_browser(self, window_info, url):
        self.browser = cef.CreateBrowserSync(window_info, url=url)

    def embedBrowser(self, url):
        window_info = cef.WindowInfo()
        # void window_info.SetAsChild(int parentWindowHandle, list windowRect), windowRect~[left,top,right,bottom];
        window_info.SetAsChild(int(self.winId()), [0, 0, self.width(), self.height()])
        cef.PostTask(cef.TID_UI, self.create_browser, window_info, url)

# Qt主窗口
class BrowserWindow:
    def setUI(self, MainWindow):
        MainWindow.resize(800, 600)
        MainWindow.setWindowTitle("cefpython3-PyQt5")

        # URL输入框、查询按钮、浏览器控件
        self.le_search = QtWidgets.QLineEdit()
        self.le_search.setPlaceholderText("输入网址...")
        self.btn_search = QtWidgets.QPushButton()
        self.btn_search.setText("查询")
        self.browser_widget = CefBrowser()

        # 设置布局方式:栅栏式
        self.main_layout = QtWidgets.QGridLayout(MainWindow)
        self.main_layout.addWidget(self.le_search, 0, 0, 1, 1)
        self.main_layout.addWidget(self.btn_search, 0, 1, 1, 1)
        self.main_layout.addWidget(self.browser_widget, 1, 0, 8, 2)

        # 信号和槽函数
        self.signal_slots()

    def signal_slots(self):
        # 绑定`查询`按钮的点击事件
        self.btn_search.clicked.connect(self.slot_load_url)

    def slot_load_url(self):
        """获取输入框的URL,判断是否已存在browser对象,如果存在则LoadUrl否则开始创建浏览器"""
        if self.le_search.text():
            if self.browser_widget.browser:
                self.browser_widget.browser.LoadUrl(self.le_search.text())
            else:
                self.browser_widget.embedBrowser(self.le_search.text())

    def show(self):
        """创建和显示应用窗口,循环监听处理"""
        app = QtWidgets.QApplication([])
        widget = QtWidgets.QWidget()
        main_window = BrowserWindow()
        main_window.setUI(widget)
        widget.show()
        app.exec_()


if __name__ == "__main__":
    sys.excepthook = cef.ExceptHook
    # bool cef.Initialize(settings={...},switches={...})
    cef.Initialize(settings={"multi_threaded_message_loop": True})
    BrowserWindow().show()
    cef.Shutdown()

二、cefpython3与js交互

2.1 绑定JS

Brower实例和JS进行绑定,然后通过设置中间媒介进行信息交互,可以选择property、function或者object

python和html代码如下图:br在绑定js之后,通过setFunction、setProperty、setObject接口都可以设置媒介交互,在html的js函数中可以执行绑定的python函数,同时还能去回调js中的函数,实现信息交互。

class Ext(object):
    def get_info(self, callback):
        callback.Call('123');

    def action(self, msg):
        print msg, type(msg)
#绑定窗口和创建
windowInfo = cefpython.WindowInfo()
windowInfo.SetAsChild(widget)
br = cefpython.CreateBrowserSync(windowInfo,browserSettings={},
      navigateUrl='file:///home/zys/temp/cef/demo.html')
#js绑定
js = cefpython.JavascriptBindings()
js.SetFunction("py_func", self.py_func)
js.SetProperty("other", {"a": 1})
js.SetObject('Ext', Ext())
br.SetJavascriptBindings(js)

def py_func(self):
        print 'I am in Python'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>测试</title>
<script src="jquery-2.1.4.js" type="text/javascript"></script>
</head>
<body>
  <h1>哈哈哈</h1>
  <a href="demo2.html">另一页</a>
  <script type="text/javascript">
    $(function(){
      py_func();  #执行py函数
      alert(other.a); 
      Ext.action('我在JS中');
      Ext.get_info(function(data){  #回调
        alert(data);
      });
    });
  </script>
</body>
</html>

2.2 python直接调用 JS

主要是通过br的mainFrame对象去直接调用js的Function,包括两个接口:
1、 ExecuteFunction :以名字直接调用指定 的 JS 函数,可以带参

def a(self, event):
    self.br.GetMainFrame().ExecuteFunction('Ext.action', 'xx')
    print 'async'

2、ExecuteJavascript :给定JS语句直接执行

def b(self, event):
    self.br.GetMainFrame().ExecuteJavascript('alert("Python")')

2.3 事件请求和响应

主要是通过ClientHandler去指定各类事件的回调函数,具体包括以下:(https://github.com/cztomczak/cefpython)

CEF Python 版本 cefpython3开发_chrome_03


可以通过重写不同的回调函数去实现需求,修改需求、修改响应、创建响应等,具体参考:https://www.zouyesheng.com/cefpython.html 通过实例使用 cefpython 访问 qbit 专栏首页,并把网页保存到 source.html

import os
import sys
import threading
import time
from loguru import logger
from cefpython3 import cefpython as cef

# 在发生异常时关闭 CEF 进程
sys.excepthook = cef.ExceptHook

class ClientHandler(object):
    r""" 自定义客户端 Handler """
    def __init__(self, chromeObject):
        self.chrome = chromeObject

    def GetViewRect(self, rect_out, **kwargs):
        r""" 渲染接口 """
        # [x, y, width, height]
        rect_out.extend([0, 0, self.chrome.width, self.chrome.height])
        return True

    def OnConsoleMessage(self, browser, message, **kwargs):
        r""" 浏览器控制台接口 """
        self.chrome.console.append(message)

    def OnLoadError(self, browser, frame, error_code, failed_url, **_):
        self.chrome.ready = error_code
        self.chrome._getReadyLock.acquire()
        self.chrome._getReadyLock.notify()
        self.chrome._getReadyLock.release()

    def OnLoadingStateChange(self, browser, is_loading, **kwargs):
        r""" 加载接口,当浏览器加载状态变化时调用 """
        if is_loading:
            # 加载中
            self.chrome.ready = False
            logger.info('Loading ...')
        else:
            # 加载完成
            self.chrome.ready = True
            self.chrome._getReadyLock.acquire()
            self.chrome._getReadyLock.notify()
            self.chrome._getReadyLock.release()
            logger.info('Loaded.')


class Client(object):
    def __init__(self, width=1920, height=1080, headless=False):
        self.width = width
        self.height = height
        self.headless = headless

        self.console = []
        self.browser = None
        self.source = None
        self.domArray = None
        self.windowParams = None
        self.ready = True
        self._getSourceLock = threading.Condition()
        self._getDOMLock = threading.Condition()
        self._getReadyLock = threading.Condition()
        self._handler = ClientHandler(self)

        settings = {
            'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) ' \
                          'AppleWebKit/537.36 (KHTML, like Gecko) ' \
                          'Chrome/64.0.3282.140 Safari/537.36',
            # "debug": True,  # 调试模式
            # "log_severity": cef.LOGSEVERITY_INFO, # 日志的输出级别
            # "log_file": "debug.log",  # 设置日志文件
            # "user_agent": "from stonejianbu 0.0.1",
        }
        switches = {
            # 取消获取媒体流(如音频、视频数据),必须以空字符串代表否!~~~~
            # "enable-media-stream": "",
            # "proxy-server": "socks5://127.0.0.1:10808",  # 设置代理
            # "disable-gpu": "",  # 设置渲染方式CPU or GPU
        }
        if self.headless:
            settings['windowless_rendering_enabled'] = True
        cef.Initialize(settings=settings, switches=switches)

    def __getattr__(self, name):
        r""" 将所有未知的属性和方法传递给 CEF 浏览器 """
        return getattr(self.browser, name)

    def getBrowser(self):
        if self.browser:
            return self.browser
        # 创建浏览器实例
        if self.headless:
            parent_handle = 0
            wininfo = cef.WindowInfo()
            wininfo.SetAsOffscreen(parent_handle)
            self.browser = cef.CreateBrowserSync(window_info=wininfo)
        else:
            self.browser = cef.CreateBrowserSync()

        self.browser.SetClientHandler(self._handler)
        self.browser.SendFocusEvent(True)
        self.browser.WasResized()  # 在 headless 模式下应至少调用一次这个方法

        return self

    def LoadUrl(self, url, synchronous=False):
        r""" 将 URL 传递给浏览器 """
        logger.info('LoadUrl %s ...' % url)
        self.ready = False
        self.browser.LoadUrl(url)
        if synchronous:  # 同步方式
            self._getReadyLock.acquire()
            if not self.ready:
                self._getReadyLock.wait()
            self._getReadyLock.release()

    def getSource(self, synchronous=False):
        r""" 返回 main frame 的 html 源码,  """
        self.source = None
        self.browser.GetMainFrame().GetSource(self)

        if synchronous:
            self._getSourceLock.acquire()
            if not self.source:
                # 等待 Visit 函数准备好 source 的通知
                self._getSourceLock.wait()
            self._getSourceLock.release()
        return self.source

    def Visit(self, value):
        r"StringVisitor 接口"
        self.source = value
        self._getSourceLock.acquire()
        self._getSourceLock.notify()
        self._getSourceLock.release()

def BrowserThread(browser):
    r""" 线程入口函数 """
    browser.ready = False
    browser.LoadUrl(url, True) # True 为同步调用
    logger.info('Write source to source.html ...')
    with open('source.html', mode='w', encoding='utf8') as srcfp:
        source = browser.getSource(True) # 同步获取
        assert(source)
        srcfp.write(source)
    browser.CloseBrowser()

if __name__ == '__main__':
    url = r'http://sf.gg/blog/qbit'
    browser = Client(width=640, height=480).getBrowser()
    browserThread = threading.Thread(target=BrowserThread, args=(browser,))
    browserThread.start()
    cef.MessageLoop()
    browserThread.join()
    browser = None
    cef.Shutdown()

三、参考补充

cefpython3:https://github.com/cztomczak/cefpython cef源码:https://bitbucket.org/chromiumembedded/cef/src/master/ cef使用说明: cef3与JS交互: cef开发指南:https://dev.yunxin.163.com/docs/product/%E9%80%9A%E7%94%A8/Demo%E6%BA%90%E7%A0%81%E5%AF%BC%E8%AF%BB/PC%E9%80%9A%E7%94%A8/Demo%20CEF%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97