天天看點

使用 CEFPython 打造自己的浏覽器視圖1. CEFPython是什麼東西2. 一個簡單的例子3. 擴充浏覽器視圖的API4. Python主動調用JS5. 事件模拟6. 請求的互動流程控制7. 請求流程的處理8. 自定義請求的處理9. 正确處理引用與釋放資源

cefpython 是 cef 的 python 綁定實作。

cef https://bitbucket.org/chromiumembedded/cef ,是 chromium 的一套嵌入式實作。

簡單來說, cef 實作了浏覽器外在的簡單功能,可以直接渲染一個全功能的頁面。它包含了頁面布局渲染的引擎,也包含了執行 js 的引擎(v8)。但是它不管一個完整浏覽器還需要的其它功能,比如标簽頁,比如下載下傳管理等等。

使用 cefpython ,就可以很容易地把一個浏覽器視圖做到 gui 的環境當中,比如 wxpython。這樣最直接的作用,就是可以使用标準的 web 技術,比如 html , js 來完成桌面用戶端的一些功能。

當然,僅僅這樣是不夠的,因為浏覽器環境中,它自己本身是缺少一些系統級的功能的,比如檔案系統的通路,比如資料的持久化存儲。用 cefpython ,你可以非常容易地添加這個浏覽器視圖的 api ,這樣通過 js 實作與 python 代碼的互相調用,你想幹什麼都可以了。

執行上面的代碼,你會看到一個 wx 建立的視窗中,顯示了一個 html 頁面。

一個最簡單的使用 cefpython 的流程大概要做的事有:

擷取元件, <code>windowinfo = cefpython.windowinfo()</code> 。

嵌入對應的适配環境中, <code>windowinfo.setaschild(mainpanel.getgtkwidget())</code> 。

初始化浏覽器環境, <code>cefpython.createbrowsersync(windowinfo, browsersettings, navigateurl)</code> 。

處理事件重新整理, <code>wx.evt_timer(self, 1, lambda e: cefpython.messageloopwork())</code> 。

上面的代碼,建立的浏覽器環境算是一個标準的環境,對于:

得到的這個 <code>br</code> ,我們可以添加額外的,可供 js 使用的其它 api 。比如:

其中 <code>demo.html</code> 的内容為:

上面 js 中的, <code>py_func()</code> 的實作是由 python 完成的(你能在終端中看到輸出的一段字元串),而 <code>other.a</code> 這個資料,也是在 cefpython 中人為添加的。這一切都非常簡單。

不過,這樣直接添加全局的函數或者資料,會讓前端變得混亂。 cefpython 也提供了添加對象的方法, <code>br.setobject()</code>:

<code>setobject</code> 可以把 python 的一個執行個體添加到 js 中作為一個對象(但是目前它有一個限制,就是隻處理執行個體對象中的方法,不處理屬性)。

對應的 <code>demo.html</code> 我們可以這樣:

雖然 cefpython 中還有其它的一些功能,文檔在https://code.google.com/p/cefpython/wiki/api ,但我覺得,一個 <code>setobject</code> 已經解決了 python 和 js 之間互相傳遞消息的所有問題:

js -&gt; python , 調用, <code>ext.action('我在js中')</code> 。

python -&gt; js , 回調, <code>ext.get_info(function(data){alert(data)})</code> 。

從這部分也可以看出,不同環境的互相傳遞消息,還是異步的方式。而且後面也可以看到,“主動控制”中執行的 js ,也是異步執行的。

前面講的是擴充,從 python 方面來看,這算是一個被動的形式。對于 cefpython ,python 這邊是可以主動控制,排程浏覽器視圖的。下面以兩個菜單按鈕執行 js 邏輯為例說明一下,先在frame 上添加菜單欄:

這裡把 <code>act_a</code> 和 <code>act_b</code> 兩個菜單按鈕的按鍵行為綁定到兩個對應的執行個體方法上了,這兩個方法:

上面代碼是兩種執行 js 邏輯的方法。第一種是直接以名字調用指定的 js 函數,可以帶參數。第二種是給定 js 語句直接執行。兩種對 js 的執行方法,都是異步的。(是以實踐中你會先看到<code>async</code> 的輸出。)

這裡的 <code>executefunction</code> 和 <code>executejavascript</code> 是在 frame 對象上的(新版本的 cefpython 好像在 browser 上也添加了兩個方法了)。

浏覽器中的各種事件,其實 js 也可以處理,不過我在 cefpython 的文檔中看到了比較“暴力”的一個 api,<code>sendmouseclickevent</code> ,它直接是指定坐标給一個 <code>click</code> 。

先修改一個 html 檔案:

代碼中的 <code>ext.log()</code> 是我自己加的,因為這個環境中沒有 <code>console</code> 用嘛,就把資訊打到終端檢視就好了,實作上是在 <code>ext</code> 中加一個方法:

上面的 html 中,有一個連結,經過實際測試,它的位置在 <code>39, 93</code> ,我們把之前的菜單按鈕的處理改成:

之是以是兩次調用,是因為我們需要的其實是, 滑鼠按下,又松開 ,簡單說就是我們要的是release ,不是 down 也不是 up 。

好了,現在去選中菜單項點選,就可以看到終端中的坐标資訊,以及頁面加載新的 demo2.html了。

這部分是比較麻煩一點的地方了。

用一個嵌入的浏覽器視圖,前面說的擴充浏覽器 api 部分,隻是站在前端角度看到的“很有用”的一部分。而如果站在全局的系統角度,一個嵌入式的浏覽器視圖,它更多的威力,是來自對請求與響應的控制。

我之前在找, cefpython 有沒有辦法,直接往視圖中渲染一段指定的 html 内容,結果是,好像不能(也許是 cefpython 沒有封裝相應的“原生”層面的 api)。就是說,在流程上,這個環境始終是 web 式的,請求 + 響應。是以,實作“直接渲染指定的 html 内容”的方式,也就變成了如何自己創造一個“響應”的問題。考慮“請求 + 響應”的流程,這個問題就是,如何額外定義另一套,相容 web 方式的,“請求 + 響應”流程。

這樣,這個問題就很明了,也很簡單了。它的做法,我們都見過,比如 chrome 中的“配置”,其實就是内置的 web 頁面。 firefox 的“配置項”,也是内置的 web 頁面。這些 web 頁面的位址,都是使用的 自定義協定 的方式處理的。于是,我就想,我可以在 html 中加一個通路連結,它形如:

自定義的 <code>cef</code> 協定。不過實際操作中發現這樣不行,要做自定義協定,需要顯式地自己去定義擴充(否則标準的處理流程會中斷,你無法正常響應這個請求),而且不幸的是, cefpython 中并沒有封裝相應的 api 。後來發現 cef 自己預設的協定中: <code>http, https, file, ftp, about, data</code> ,有一個 <code>data</code> ,暫且就把它用來自己擴充使用吧。

那麼在 html 頁面,我可以寫一個位址:

這樣,整個 cef 的“請求 + 響應”流程完全不受影響。

這裡的流程,指的就是“請求 + 響應”。在 cefpython 中,你可以通過調用<code>br.setclienthandler()</code> 方法,來自己完全指定一套流程處理的實作。或者,僅僅使用<code>br.setclientcallback()</code> 來指定具體的事件回調函數。

在整個流程中, cef 在擴充上的實作,是類似于“事件注冊”的 hook 方式。如果需要自定義,那麼自己做一個對象,裡面實作相應的方法即可。

注意一下,這裡說的“事件點”,其實隻有一套,但是在 cef 的概念中,它又把這些事件點“分組”了,對應到文點中不同的 handler ,比如 requesthandler , loadhandler ,keyboarhandler 等,都是這些事件的分組。而總的你需要實作的 clienthandler ,是可選地,需要去實作這些不同的“分組”中的方法的。

比如:

這些内容雖然有點多( https://code.google.com/p/cefpython/wiki/api 中的 client handlers 部分),但是我們單純考慮“請求 + 響應”的話,隻需要處理兩個方法就可以了(其實是三個的)。因為我們面對的場景,隻需要考慮三點:

如何修改請求。

如何修改響應。

如何建立響應。

修改請求指的是,當你在流程中“截取”到請求時,在這個請求正式“發出”之前,修改它的相關屬性。

比如一個請求本來是到 <code>file:///home/zys/temp/cef/demo.html</code> ,你把它的 url 改到<code>http://www.zouyesheng.com</code> 了(mb,這個時候我的 www.zouyesheng.com 好像被牆了)。這部分的實作很容易。

先修改一個 <code>br</code> ,注冊一個 <code>clienthandler</code> 給它:

然後 <code>clienthandler</code> 這個類的實作,隻需要做一個方法, <code>requesthandler</code>https://code.google.com/p/cefpython/wiki/requesthandler 中的<code>onbeforeresourceload</code> 方法:

<code>onbeforeresourceload</code> 方法中,可以随意修改 <code>request</code> 的屬性,https://code.google.com/p/cefpython/wiki/request 。

注意一下, <code>onbeforeresourceload</code> 對 <code>request</code> 的修改,是全局影響的,像上面代碼中的這樣一改,相應的 js, css 請求就全失效了。

<code>onbeforeresourceload</code> 如果 <code>return true</code> ,則請求中斷。

修改響應是指,當請求完成,擷取到響應内容之後,在這些内容被渲染前,修改内容。比如你想把頁面上的廣告内容全部删除。

像前面的“修改請求”一樣,本來是實作一個方法就可以搞定的事,但不幸的是, cef 中還沒有實作這個唯一的 api <code>onresourceresponse</code> ,文檔中的說法是,這個 api 你可以自己搞定……

自己搞定的意思,就是後面講的,自己建立需要的,任意的響應。

這部分,算是一個完整的流程控制了。“建立”,即指的是無中生有,不像前面的,隻是“修改”層面的動作了(因為直接的“修改響應”的缺失,在這裡隻實作“修改響應”也是可以的)。

“請求 + 響應”中,最重要的一部分,對于 http 協定來說,就是送出請求,然後按 http 協定解析響應的封包。這部分, cef 中原本是按标準的 http 協定實作好了的,它提供了 api ,允許自定義這部分的實作(簡單說,你甚至可以自己用 <code>urllib</code> 來搞,不考慮線程與阻塞什麼的話)。

實作上,分兩部分:

<code>requesthandler</code> 中的 <code>getresourcehandler</code> 方法,要求傳回一個 <code>resourcehandler</code> 的執行個體。

<code>resourcehandler</code> 的實作,完成了送出請求,解析封包等操作https://code.google.com/p/cefpython/wiki/resourcehandler。

是以,我們要做的,就是做一個自己的 <code>resourcehandler</code> 。 它和 <code>clienthandler</code> 有些不同,雖然都不是繼承已有的類,但 <code>clienthandler</code> 隻需要實作用得到的方法即可,而 <code>resourcehandler</code> 中的所有被要求的方法,必須實作。

先處理 <code>clienthandler</code> (前面的 <code>br.setclienthandler()</code> 一樣的):

<code>getresourcehandler</code> 方法傳回一個 <code>resourcehandler</code> 執行個體。這裡,我們隻處理使用了 <code>data</code> 協定的請求。普通的 http 協定的請求,按預設處理就好了,不動它。

注意: self.red = resourcehandler() 這句的意義在于顯式地保持一個 resourcehandler 執行個體的引用(否則這個執行個體在真正想用它時已經被 python 給 gc 了?),這塊應該是從靜态的 c++ 綁定到動态的 python 時,對于引用與釋放的一個無奈之舉了。

<code>resourcehandler</code> 的實作:

一共有 6 個方法 https://code.google.com/p/cefpython/wiki/resourcehandler 。

雖然是自定義,但是形式上,卻按 http 封包解析的方式規定好了的,就是一般的先處理頭,再處理 body 的形式。

cefpython 因為是從 c++ 代碼綁定而來,是以,這部分的實作,有一點很特别,就是使用 python 中的 list 類型(因為 list 對象是“引用傳遞”),來處理 c++ 中的“位址”,比如上面的 <code>length</code> , <code>bytes_readed</code> 這些,雖然隻是一個數字,但都是用 list 來儲存,指派時也是為這個list 的第一個成員指派。(典型的“填坑”用法)

假設這裡,我們要響應一段自己設定的 html 文本(比如就是 <code>res = &lt;h1&gt;哈哈&lt;/h1&gt;</code> ),其實完全不會涉及封包的解析,我們就把對應的資料,填到相應的“坑”中去就可以了:

參照解析 http 響應封包的一般方法, <code>getresponseheaders</code> 方法最重要的一點,就是擷取接下來的body 部分的位元組長度(如果是未知長度,按 <code>-1</code> 處理)。把這個長度值填到 <code>length[0]</code> 中即可。

另外,處理頭的時候,可以處理 <code>response</code> 對象的相關屬性https://code.google.com/p/cefpython/wiki/response 。比如這裡,我們設定了mimetype ,這樣在渲染時就會按 html 頁面處理了。

下面是處理 body 部分:

同樣考慮 http 響應封包的一般解析方法,從原始的連接配接中讀取内容的行為與結果,是不一定的。是以通常我們會在這塊做一個 <code>while</code> ,然後從連接配接中不斷地嘗試讀取,直接已讀取的 <code>chunk</code> 長度等于我們期望的長度。這個過程的排程, cef 已經在内部封裝好了,我們隻需要在這裡給到的<code>readresponse</code> 方法實作上,控制它的幾個參數即可:

<code>data</code> ,響應的 body 部分的内容。

<code>bytes_to_read</code> 還需要讀取的位元組數。

<code>bytes_readed</code> 已讀取的位元組數。

<code>callback</code> 控制方法。

因為“讀取”行為是不斷嘗試的,是以這個 <code>readresponse</code> 方法會被不斷調用,直到 <code>bytes_to_read</code>減至 <code>0</code> ,說明需要的内容已經讀完。官網文檔中的幾句話,倒把這個“意會容易,說明難”的流程講清楚了 https://code.google.com/p/cefpython/wiki/resourcehandler :

我們這裡自己需要響應的内容是固定的 <code>&lt;h1&gt;ok&lt;/h1&gt;</code> ,是以幾個資料直接就可以寫死了:

最終,當通路一個 <code>data://xxxx</code> 這樣的位址時,你就可以在頁面上看到兩個大的 <code>ok</code> 字元,這個内容,就算是我們“直接建立的響應内容”了。

總結一下完整地控制請求與響應要做的事:

<code>br.setclienthandler()</code> 設定一個 <code>clienthandler</code> 執行個體。

<code>clienthandler</code> 執行個體的 <code>onbeforeresourceload</code> 方法可以修改請求。

<code>clienthandler</code> 執行個體的 <code>getresourcehandler</code> 方法可以根據 <code>request</code> 的屬性判斷,選擇性地傳回一個 <code>resourcehandler</code> 執行個體。

如果 <code>getresourcehandler</code> 傳回了一個 <code>resourcehandler</code> 執行個體,則這個執行個體在 <code>clienthandler</code> 中需要被顯示地保持引用。

<code>resourcehandler</code> 執行個體通過 <code>getresponseheaders</code> 與 <code>readresponse</code> 方法完成内容擷取。

<code>getresponseheaders</code> 中可以根據需要,設定 <code>resposne</code> 的各種屬性。

幾個概念梳理一下:

<code>br</code> 是一個 <code>browser</code> 執行個體。

<code>clienthandler</code> 執行個體是用于控制“請求 + 響應”的一套實作(它又是由各種 <code>xxxhandler</code> 組成的)。

<code>clienthandler</code> 中處理響應的流程部分,由專門的 <code>resourcehandler</code> 執行個體完成(它由<code>clienthandler</code> 執行個體的一個方法傳回得到)。

在過程當中,會用到 <code>request</code> 對象和 <code>response</code> 對象。

就 http 協定來說,從發送請求,到擷取響應,中間還有一些細節的東西。一個例子就是,當你擷取的響應頭中的 content-type 不是一個适合在浏覽器顯示的類型,那麼,應該彈出一個儲存檔案的提示框。是以,前面提到了 <code>request</code> 對象, <code>response</code> 對象,提到了就 cef 的流程來說,是如何處理請求與響應的。但是,更深一層的 http 協定的流程,如何去把握并沒有介紹。

當然,這部分的實作,可以說是跟 cef 這個東西沒有直接關系的,前面也說過了,按 cef 的基本流程,你完全可以通過 <code>urllib</code> 拿到響應之後再作後續處理。不過, cef 其實有提供相應的工具,來對 http 的請求與響應的解析過程作更細的控制的。

這裡涉及到的對象有:

<code>request</code> 和 <code>response</code> ,不多說。https://code.google.com/p/cefpython/wiki/request ,

<code>webrequest</code> 外部控制對象,接受 <code>request</code> 和 <code>webrequestclient</code> 的輸入。https://code.google.com/p/cefpython/wiki/webrequest

<code>webrequestclient</code> 流程實作對象(裡面實作一些回調需要用到的方法)。https://code.google.com/p/cefpython/wiki/webrequestclient

先講一下 <code>request</code> 對象。之前提它時,是在 cef 的自己的流程中,會遇到它。這裡,我們要憑空創造一個 <code>request</code> ,也是可以的:

通過對靜态方法 <code>cefpython.request.createrequest()</code> 的調用,我們可以得到一個 <code>request</code> 對象,然後,通過 <code>set*</code> 方法去設定它的各種屬性。之後,可以把它扔給 <code>webrequest</code> 進行實際的請求了。

注意,這個 wq 也像之前的 resourcehandler 執行個體一樣,需要顯式地保持一個引用。

這裡的 <code>webrequestclient()</code> 就是對一組請求狀态的方法實作,需要的方法有:

<code>onuploadprogress(wq, current, total)</code>

<code>ondownloadprogress(wq, current, total)</code>

<code>ondownloaddata(wq, data)</code>

<code>onrequestcomplete(wq)</code>

<code>getauthcredentials(is_proxy, host, port, realm, schema, callback)</code>

上面的幾個方法中,除了它的一個主要作用是可以監控狀态之外, <code>ondownloaddata</code> 方法,還是一個處理響應内容的手段。實際上,如果你使用 <code>webrequest</code> 來自己送出請求的話,那麼在<code>ondownloaddata</code> 中需要自己拼接多次響應的内容。

<code>webrequest</code> 的使用看一個最簡單的完整例子:

從上面的代碼可以看到, <code>webrequest</code> 是可以跟 browser 完全沒有關系的,處理好 <code>webreqest</code> 的顯式引用就好了。而 <code>webrequestclient</code> 的實作則可以看到請求在每個階段的狀态(具體方法參數的含義見官方文檔)。但話雖是這麼說,如果僅僅是為了完成一個 http 請求,那 <code>urllib</code> 也可以做到。 <code>webrequest</code> 作為 cef 提供的現成機制,目的應該還是跟 <code>clienthandler</code> 配合使用,達到對一個“請求響應”流程的完全控制 + 狀态監視。下一節專門考慮這個問題。

<code>webrequest</code> 是 cef 中提供的一套 http 請求處理的實作。把自定義請求的處理整個流程拿出來看的話,大概是這樣的:

gui 的初始化階段,建立 <code>browser</code> 。

<code>browser</code> 通過 <code>setclienthandler</code> 方法設定一個 <code>clienthandler</code> 的執行個體。

<code>clienthandler</code> 中的方法,可以更改請求。

<code>clienthandler</code> 的 <code>getresourcehandler</code> 方法傳回一個 <code>resourcehandler</code> 執行個體。

<code>resourcehandler</code> 負責處理請求,并得到響應。

<code>resourcehandler</code> 中使用 <code>webrequest</code> 處理請求。

<code>webrequest</code> 的使用,就是定義了一套 <code>webrequestclient</code> 接口,https://code.google.com/p/cefpython/wiki/webrequestclient

通過指定 <code>request</code> 和 <code>webrequestclient</code> 得到了 <code>webrequest</code> 。 <code>webrequestclient</code> 中是完成請求互動的細節,比如要如何去讀夠整個 http 響應的資料。

上面的概念可能有些多,一層套一層的。其實就是:

這個地方,不用 <code>webrequest</code> 直接用 <code>httplib</code> 拿資料是可行的,考慮效率,可能需要用多線程等并發方式處理請求。

但在我自己試的時候, cef 總是會挂,不知道為什麼。

不管是用 <code>webrequest</code> ,還是像 <code>httplib</code> 等其它方法,請求的處理與 cef 的對接點,都在<code>resourcehandler</code> 上。準确地說,一般是:

<code>__init__()</code> 時建立相應的執行個體。

<code>processrequest()</code> 送出請求。

<code>getresponseheaders()</code> 設定好響應頭。

<code>readresponse()</code> 完成響應資料讀取。

這裡說一句話,使用 <code>webrequestclient</code> 這東西,想要完善地處理通用的 http 請求是不可能的,它最大的問題是沒有作一個 <code>onheaderget()</code> 的回調,這樣,在流程上與 http 的互動流程是不比對的,會很麻煩。同時, <code>webrequestclient</code> 沒有提供關閉連接配接的方法,這意味着你無法中斷一個請求的處理。

cefpython 是對 c++ 的 cef 的綁定,所有在一些地方,資源的引用與釋放,沒有辦法自動地做到那麼徹底,這種情況下,就需要人為地處理掉。

這個問題之前就提過了。在 <code>clienthandler</code> 的 <code>getresourcehandler</code> 方法中,傳回的 <code>resourcehandler</code> 需要顯示地保留它的引用。比如之前的代碼:

把 <code>resourcehandler</code> 的執行個體放到 <code>self.res</code> 中。

跟上面的 <code>resourcehandler</code> 一樣, <code>webrequest</code> 的執行個體也需要顯示保留引用。

官方示例代碼中的做法,在結束掉 wx 的 application 執行個體之後,先清除 app 的引用,然後調用 cefpython 的關閉方法。

官方示例代碼中的做法,因為涉及多個 browser 執行個體的排程,在父視窗要關閉時,需要通知出去。 cefpython 在 browser 對象中提供了一個現成的方法, <code>br.parentwindowwillclose</code> 。

上面的代碼,處理在 <code>mainframe</code> 關閉時,總去處理 <code>parentwindowwillclose</code> 。