Chienomi

PyQt5でウェブ 【前編】 QWebEngine / QQmlApplicationEngineのキャッシュとプロファイル

またしても無知を晒すPython*Qt*WebEngineのお話。

Unsurfが「Incognitoブラウジングにいいよ」とか言ったけれど、実際プロファイルもキャッシュも残ってしまうので、なんとかならないかな、ということを考えた。

この記事、及びこのソフトウェアのコードは、PyQtに関する情報を調べる中で「いくら検索しても情報や実例が出てこない」という問題に悩まされたことを踏まえてコードを書き残すことを主眼としたものであり、内容はレベルの高いものではない。むしろ稚拙といっていい。私自身があまり理解していないのだから。

QWebEngineの場合

PyQt5を使うQWebEngineの場合の情報はこちらのドキュメントにあるQWebEngineProfileで、cachePath, persistentStoragePath, storageNameといった値でコントロールされることがわかる。

QWebEngineの場合はデフォルト名が<appname>/QWebEngineで固定であり、従来の方式だと~/.local/QWebEngineなんかになってしまってよろしくない。

そこでこのコード

#!/usr/bin/python
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import *
from PyQt5.QtGui import QIcon
import re
import os

argv = sys.argv
argc = len(argv)

if (argc != 2):
    print("Usage:\n unsurf.py addr")
    quit()

# For opening local file.
url = argv[1]
if re.match('^[a-z]+://', url):
    pass # do nothing.
elif re.match('^/', url):
    url = 'file://' + url
else:
    url = "file://" + os.getcwd() + "/" + url

app = QApplication([])

# Choice application icon. I don't know smart way for choice generic web browser icon.
for iconpath in ["Papirus/64x64/apps/redhat-web-browser.svg", "Vibrancy-Colors/apps/96/browser.png", "Papirus/64x64/apps/internet-web-browser.svg", "breeze/apps/48/plasma-browser-integration.svg", "breeze/apps/48/internet-web-browser.svg", "Adwaita/scalable/apps/web-browser-symbolic.svg", "gnome/256x256/apps/web-browser.png", "ePapirus/22x22/actions/web-browser.svg", "AwOken/clear/128x128/apps/browser.png", "andromeda/apps/48/internet-web-browser.svg"]:
    if os.path.exists("/usr/share/icons/" + iconpath):
        app.setWindowIcon(QIcon.fromTheme("web-browser", QIcon("/usr/share/icons/" + iconpath)))
        break

web = QWebEngineView()
web.setWindowTitle("Unsruf Quick WebBrowser")
web.load(QUrl(url))
web.show()

sys.exit(app.exec_())

にちょっと書き足しておく。まず単純にアプリ名を与えてあげれば固有にはなる。

- app = QApplication([])
+ app = QApplication(sys.argv)

プロファイル名のセットの仕方はQWebEngineProfileQWebEnginePageを使ってこんな感じ

web = QWebEngineView()
pf = QWebEngineProfile("unsurf", web)
page = QWebEnginePage(pf, web)
web.setPage(page)

これでプロファイル名が“unsurf”になる。 ただ、今回の場合はどちらかというと、「Unsurfは常にincognito」ということが重要になるので、キャッシュパスを知るには

dpf = QWebEngineProfile.defaultProfile()
print(dpf.cachePath())

プロファイルパスは

dpf = QWebEngineProfile.defaultProfile()
print(dpf.persistentStoragePath())

だから、

- sys.exit(app.exec_())
+ app.exec_()
+ 
+ shutil.rmtree(dpf.persistentStoragePath())
+ shutil.rmtree(dpf.cachePath())
+ 
+ sys.exit()

って感じ。おっと、importも忘れずに。

ところがそもそもの話としてQWebEngineにはoffTheRecordモードが入っていて、使い方がわからず調べ回った結果、QWebEngineProfileにからっぽのQObjectを与えるか、そもそも何も与えなければ良い、ということがわかった。

なので最終的には

--- a/unsurf.py
+++ b/unsurf.py
@@ -23,18 +23,23 @@ elif re.match('^/', url):
 else:
     url = "file://" + os.getcwd() + "/" + url
 
-app = QApplication([])
-
+app = QApplication(["unsurf"])
 
 # Choice application icon. I don't know smart way for choice generic web browser icon.
 for iconpath in ["Papirus/64x64/apps/redhat-web-browser.svg", "Vibrancy-Colors/apps/96/browser.png", "Papirus/64x64/apps/internet-web-browser.svg", "breeze/apps/48/plasma-browser-integration.svg", "breeze/apps/48/internet-web-browser.svg", "Adwaita/scalable/apps/web-browser-symbolic.svg", "gnome/256x256/apps/web-browser.png", "ePapirus/22x22/actions/web-browser.svg", "AwOken/clear/128x128/apps/browser.png", "andromeda/apps/48/internet-web-browser.svg"]:
     if os.path.exists("/usr/share/icons/" + iconpath):
         app.setWindowIcon(QIcon.fromTheme("web-browser", QIcon("/usr/share/icons/" + iconpath)))
         break
-
 web = QWebEngineView()
+
+# Set profile name if you need.
+pf = QWebEngineProfile()
+page = QWebEnginePage(pf, web)
+web.setPage(page)
+
 web.setWindowTitle("Unsruf Quick WebBrowser")
 web.load(QUrl(url))
 web.show()
 
-sys.exit(app.exec_())
\ No newline at end of file
+sys.exit(app.exec_())

って感じ。

QQmlApplicationEngineの場合

QMLを使うQQmlApplicationEngineの場合は

app = QtGui.QGuiApplication(["Qt Web Viewer"])

だとプロファイルは~/.local/Qt Web Viewerに、キャッシュは~/.cache/Qt Web Viewerに置かれる形(Linux上において)。

公式ドキュメントはこちらで、QQuickWebEngineProfileに含まれているcachePathpersistentStoragePathというプロパティでコントロールされる。

QQuickWebEngineProfilePyQt5/QtWebEngineで提供されている。

で、QWebEngineProfileの情報は調べればちらほら出てくるのだけど、QQuickWebEngineProfileのほうは全くといっていいほど出てこない。 ワードを変えると出てくるものもなくはないので、Googleがだめなだけ、という気はするけど、DuckDuckGoにすればヒットするとかそんなことはない。Bingに至ってはクォートしたところでワードを無視するので、QtWebEngineの話ばかり出てきて役に立たない。

プロファイルのパスの取得方法は

profile = QtWebEngine.QQuickWebEngineProfile.defaultProfile()

でいけるから、QWebEngineProfileと似た感じではある。 でも、QQmlApplicationEnginesetPageがないので同じようにはいかない。

検索しても類似コードも見つからずどうしようもない展開に。 なので質問してみたんだけど、回答がこなかったのでがんばった。本当にがんばった。

結果分かったのは、「Qtの場合はコード上で書くけど、QtQuick(QML)の場合はあくまでQML上で書くんだよ」ということ。 QMLオブジェクトとつなげる方法があるのかはよくわからないけれど、QQuickWebEngineProfileはQML上で書けってことのようだ。

ということで直したんだけど、オリジナルのリポジトリは消してしまっていたので元がどうだったか説明するのが難しい。 diffはあるので、diffを見ると別物である。

diff --git a/qwview.py b/qwview.py
index 2f81beb..a4352a4 100755
--- a/qwview.py
+++ b/qwview.py
@@ -1,18 +1,37 @@
 #!/usr/bin/python
 import sys
 import os
-from PyQt5 import QtGui, QtQml
+from PyQt5 import QtGui, QtQml, QtWebEngine
 from OpenGL import GL  #Linux workaround.  See: http://goo.gl/s0SkFl
+from docopt import docopt
 
-argv = sys.argv
-argc = len(argv)
+__doc__ = """{f}
 
-if (argc != 2):
-    print("Usage:\n qwview.py addr")
-    quit()
+Usage:
+    {f} [-i] [-p <profile_name>] <URL>
+    {f} -h | --help
 
-app = QtGui.QGuiApplication(["Qt Web Viewer"])
+Options:
+    -i --incognito               Enable off the record mode.
+    -p --profile <profile_name>  Use profile (storage) name.
+    -h --help                    Show this help and exit.
+""".format(f=__file__)
+
+otr_mode = False
+pfname = "Default"
+
+args = docopt(__doc__)
+if args['--incognito']:
+    otr_mode = True
+if args['--profile']:
+    pfname = args['--profile']
+
+app = QtGui.QGuiApplication(["QtWebViewer"])
+app.setWindowIcon(QtGui.QIcon(":/browser"))
 engine = QtQml.QQmlApplicationEngine()
-engine.rootContext().setContextProperty("myUrl", argv[1])
-engine.load("{home}/lib/qappview.qml".format(home=os.environ["HOME"]))
+cx = engine.rootContext()
+cx.setContextProperty("myUrl", args["<URL>"])
+cx.setContextProperty("isOffTheRecord", otr_mode)
+cx.setContextProperty("viewStorageName", pfname)
+engine.load("{home}/lib/qwview.qml".format(home=os.environ["HOME"]))
 app.exec_()
diff --git a/qwview.qml b/qwview.qml
index aae4ab7..4f7fac1 100644
--- a/qwview.qml
+++ b/qwview.qml
@@ -1,13 +1,41 @@
-import QtQuick 2.0
+import QtQuick 2.9
 import QtQuick.Window 2.0
-import QtWebEngine 1.0
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import QtWebEngine 1.9
 
 Window {
+    id: root
     width: 1024
     height: 750
     visible: true
     WebEngineView {
+        id: webview
         anchors.fill: parent
         url: myUrl
+        profile {
+            offTheRecord: isOffTheRecord
+            storageName: viewStorageName
+        }
+        onLoadingChanged: {
+            switch (loadRequest.status) {
+            case WebEngineLoadRequest.LoadStartedStatus:
+                loadProgressBar.visible = true
+                break
+
+            default:
+                loadProgressBar.visible = false
+                break
+            }
+        }
+        onLoadProgressChanged: loadProgressBar.value = loadProgress
+    }
+    ProgressBar {
+        id: loadProgressBar
+        width: root.width
+        from: 0
+        to: 100
+        value: 0
+        visible: false
     }
 }

新しいリポジトリはこちら。コードも掲載するとこんな感じ。

Python.

#!/usr/bin/python
import sys
import os
from PyQt5 import QtGui, QtQml, QtWebEngine
from OpenGL import GL  #Linux workaround.  See: http://goo.gl/s0SkFl
from docopt import docopt

__doc__ = """{f}

Usage:
    {f} [-i] [-p <profile_name>] <URL>
    {f} -h | --help

Options:
    -i --incognito               Enable off the record mode.
    -p --profile <profile_name>  Use profile (storage) name.
    -h --help                    Show this help and exit.
""".format(f=__file__)

otr_mode = False
pfname = "Default"

args = docopt(__doc__)
if args['--incognito']:
    otr_mode = True
if args['--profile']:
    pfname = args['--profile']

app = QtGui.QGuiApplication(["QtWebViewer"])
app.setWindowIcon(QtGui.QIcon(":/browser"))
engine = QtQml.QQmlApplicationEngine()
cx = engine.rootContext()
cx.setContextProperty("myUrl", args["<URL>"])
cx.setContextProperty("isOffTheRecord", otr_mode)
cx.setContextProperty("viewStorageName", pfname)
engine.load("{home}/lib/qwview.qml".format(home=os.environ["HOME"]))
app.exec_()

QML

import QtQuick 2.9
import QtQuick.Window 2.0
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtWebEngine 1.9

Window {
    id: root
    width: 1024
    height: 750
    visible: true
    WebEngineView {
        id: webview
        anchors.fill: parent
        url: myUrl
        profile {
            offTheRecord: isOffTheRecord
            storageName: viewStorageName
        }
        onLoadingChanged: {
            switch (loadRequest.status) {
            case WebEngineLoadRequest.LoadStartedStatus:
                loadProgressBar.visible = true
                break

            default:
                loadProgressBar.visible = false
                break
            }
        }
        onLoadProgressChanged: loadProgressBar.value = loadProgress
    }
    ProgressBar {
        id: loadProgressBar
        width: root.width
        from: 0
        to: 100
        value: 0
        visible: false
    }
}

QMLで書かれているWebEngineView.profile.storageNameにセットする形でできた。

もともとはローカルファイルを見る用に考えていたのだけど、機能的にはUnsurfよりも多く乗る形になった。

で、作りました

そんなこんなでQtのWebEngine周りをいじってたので、もうちょっと実用的なのが欲しいなと思って作っちゃいました。

今までのものと比べるとだいぶコードも長くて、機能的には

  • タブ機能
  • アドレスバー (検索はしない。スキーマの補完はする)
  • ページタイトルのサポート
  • タブ切り替え (クリック, Ctrl+Pg{Up,Down})
  • タブ閉じ (クリック, Ctrl+W)
  • キーボード・ショートカット(リロード, 戻る・進む)
  • プロファイル切り替え

といったところ。ローカルファイルよりもウェブ優先になっていたりと、Unsurfとはちょっと違う。

すごい簡単に作れた感じだけれども、QtもGUIアプリケーションもPythonも全然慣れてないので、かなり時間もかかったし、一個ずつ調べながら形にしていくみたいな感じだった。

昔、IEコントロールを使ったブラウザ(架装ブラウザとか呼ばれてた)がすごく流行ってたくさんあった。私はmoon browserとkikiをすごく使っていたし、今でも残っているSlaipnirとかLunascapeとかも元々はそういうブラウザだった。Mozillaと「マルチエンジン実装!」なんてのも流行ったね。

今はブラウザが高度化していたり、拡張機能への依存度が上がったりして、そうしたブラウザはほとんどなくなってしまって、大手ですらも見なくなったのだけど、QtでWebEngineを使うとすごく簡単に実装できるから(経験に乏しい私でも手探りでできたくらいだから)、やってみても面白いかも知れない。

ちなみにこのブラウザ、WebEngine(Blink。Chromiumと同じもの)を使っている関係上、パフォーマンスや表現能力はGtkWebKitを使うSurfやMidoriよりもよかったりする。 なんだか今回、エンジン自作を期待されてしまったような気がするのだけれど、実用的にはエンジン自作はもはや一般レベルでできるものではないので、ポイントを抑えて自分で作るのが良いと思う。 拡張機能を作るのがベストっていうことも多いだろうけれども。

個人的にはよりシンプルで軽く、プロファイル機能があり、余計なことをしないブラウザが欲しかったのでかなり満足。 (off the recordなブラウザが欲しい場合はUnsurf.pyがよく機能する)

Wrote on: 2019-11-05 22:48:00 +09:00