一、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程序继承CefApp和CefRenderProcessHandler类,在main函数中初始化。通过CefSettings.browser_subprocess_path配置render可执行程序路径。browser进程就会去启动这个进程去渲染网页
1.3 brower进程实现结构
browserapp要继承CefApp和CefBrowserProcessHandler类。实现browserapp的定义。同时要新建clienthandler类实现图中的回调函数接口类,用来处理拦截响应请求、管理生命周期、下载、显示加载、右键菜单等。在mian函数中初始化、启动消息循环。调用CefBrowserHost的静态方法创建browser窗口对象,在render进程的Frame中加载渲染内容。
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)
可以通过重写不同的回调函数去实现需求,修改需求、修改响应、创建响应等,具体参考: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