概要
個人情報をサーバーに送らずにデータ分析したいといった要望はままあります。
また、分析するコードをPythonで開発後に、Swiftに移植するのはかなり手間です。
ソリューションとしてオフラインでPythonを実行できないか以前に調べた内容を紹介します。
これはPyodideをWebkitから実行する方法です。
将来的にはwasmer/pythonで直接実行できることでしょう。
ちなみに上記ディスカッションのotmbは私の個人アカウントです。
環境構築
適当にPyodideの最新版を引っ張ってきます。
$ curl -LO https://github.com/pyodide/pyodide/releases/download/0.26.0a2/pyodide-core-0.26.0a2.tar.bz2
$ tar xvjf pyodide-core-0.26.0a2.tar.bz2
解凍したpyodideフォルダをXcodeプロジェクト内に配置した上でReferenceになるように取り込みます。
- 実際の配置
PyodideExample
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── ContentView.swift
├── Preview Content
│ └── Preview Assets.xcassets
│ └── Contents.json
├── PyodideExampleApp.swift
├── index.html
└── pyodide
├── package.json
├── pyodide-lock.json
├── pyodide.asm.js
├── pyodide.asm.wasm
├── pyodide.js
├── pyodide.mjs
└── python_stdlib.zip
- 青いフォルダ(Reference)になるように取り込む
コード
ContentView.swift
import SwiftUI
import UIKit
import WebKit
struct ContentView: View {
var body: some View {
VStack {
WebView()
}
.padding()
}
}
struct WebView: UIViewControllerRepresentable {
typealias UIViewControllerType = WebViewController
func makeUIViewController(context: Context) -> WebViewController {
return .init()
}
func updateUIViewController(_ uiViewController: WebViewController, context: Context) {}
}
class WebViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {
var webView: WKWebView!
override func loadView() {
let config = WKWebViewConfiguration()
config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
let controller = WKUserContentController()
controller.add(self, name: "logging")
config.userContentController = controller
webView = WKWebView(frame: .zero, configuration: config)
webView.uiDelegate = self
webView.isInspectable = true
view = webView
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.name, message.body)
}
override func viewDidLoad() {
super.viewDidLoad()
let htmlFileName = "index"
do {
guard let filePath = Bundle.main.path(forResource: htmlFileName, ofType: "html")
else {
throw NSError(domain: "Error: The file path is invalid.", code: -1, userInfo: nil)
}
let htmlString = try String(contentsOfFile: filePath, encoding: .utf8)
webView.loadHTMLString(htmlString, baseURL: URL(fileURLWithPath: filePath))
} catch let error {
print(error.localizedDescription)
}
}
}
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello WKWebView</title>
<script type="text/javascript">
function jsLog (msg){
window.webkit.messageHandlers.logging.postMessage(msg);
}
const console = {
log: jsLog,
info: jsLog,
warn: jsLog,
error: jsLog,
}
async function fetch(filename) {
const promise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', filename);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', (e) => resolve(xhr.response));
xhr.send();
});
const response = new Response(await promise.then(), {headers: new Headers([['Content-Type', 'application/wasm']])});
return response;
}
</script>
<script src="pyodide/pyodide.js"></script>
</head>
<body>
Pyodide test page <br>
Open your browser console to see Pyodide output
<script type="text/javascript">
async function main(){
let pyodide = await loadPyodide({
stdin: jsLog,
stdout: jsLog,
stderr: jsLog,
});
console.log(pyodide.runPython(`
import sys
sys.version
`));
pyodide.runPython("print(1 + 2)");
}
main();
</script>
</body>
</html>
実行結果
logging 3.12.1 (main, Feb 15 2024, 01:00:57) [Clang 18.0.0git (https://github.com/llvm/llvm-project 0a3a0ea5914cb4633f4f4c14f
logging 3
コードの説明
PyodideはJSのfetch
を利用してファイルを取得します。
ところがWKWebView
はローカルファイルの取得にXMLHttpRequest
は機能しますがfetch
は機能しません。
なのでfetch
を上書きします。
async function fetch(filename) {
const promise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', filename);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', (e) => resolve(xhr.response));
xhr.send();
});
const response = new Response(await promise.then(), {headers: new Headers([['Content-Type', 'application/wasm']])});
return response;
}
以上。簡単なハックでオフラインで動作します。
おまけ
機械学習ライブラリを使う場合は、フルサイズのPyodideに置き換えると良い。
$ curl -LO https://github.com/pyodide/pyodide/releases/download/0.26.0a2/pyodide-0.26.0a2.tar.bz2
$ tar xvjf pyodide-0.26.0a2.tar.bz2
index.html
<script type="text/javascript">
async function main(){
...
await pyodide.loadPackage(["numpy", "scikit-learn"]);
await pyodide.runPython(`
import numpy as np
a = np.array([1.0, 2.0, 3.0])
b = np.array([a, (10, 20, 30)])
print(b)
`);
}
main();
</script>
`);
実行結果
指定したライブラリの依存関係含めて読み込みます。
logging Loading numpy, scikit-learn, scipy, openblas, joblib, threadpoolctl
logging Loaded joblib, numpy, openblas, scikit-learn, scipy, threadpoolctl
logging [[ 1. 2. 3.]
logging [10. 20. 30.]]