如果你用一个人听得懂的语言和他说话,那就会进入他的大脑。如果你用他自己的语言与他交谈,那就会进入他的内心。
- 纳尔逊-曼德拉
WebAssembly(WASM)为许多语言在不同的环境中使用打开了大门--如浏览器、云、无服务器和区块链等,仅举几例--它们以前是无法使用的。例如,通过利用WASM的Pyodide,你可以在浏览器中运行Python。你还可以使用Wasmer等运行时,将Python编译成WebAssembly,将其容器化,然后在不同的目的地,如边缘、物联网和操作系统中运行容器。
在本教程中,你将使用Python和Pyodide构建一个单页应用程序,以操纵DOM和管理状态。
--
Python单页应用系列。
- 第一部分(本教程!)。学习Pyodide的基础知识并创建基础应用程序
- 第二部分(即将推出)。用Pandas分析和处理数据,并使用网络工作者来加快应用程序的速度
- 第三部分(即将推出)。创建一个Python包,添加额外的功能,并添加一个持久的数据层。
- 目标
- 浏览器中的Python
- 我们正在建造的东西
- 用Python进行DOM操作
- 类型翻译
- Netflix数据集
- 安装Pyodide和TailwindCSS
- 创建应用程序组件
- 添加样本数据
- Pandas数据操作
- 总结
目标
在本教程结束时,你应该能够。
- 与JavaScript一起使用Pyodide,在两者之间共享和访问对象
- 直接从Python代码中操纵DOM
- 在浏览器中运行Python的强大数据科学库
- 创建一个单页应用程序(SPA),从远程文件中获取数据,用Pandas操作数据,并在浏览器中显示出来。
浏览器中的 Python
这是什么意思?
在浏览器中运行Python意味着我们可以直接在客户端执行Python代码,而不需要在服务器上托管和执行代码。这是由WebAssembly实现的,它允许我们建立真正的 "无服务器 "应用程序。
为什么它很重要?为什么不直接使用JavaScript?
在浏览器中运行Python背后的主要目标不是要取代JavaScript,而是要把两种语言结合起来,让每个社区都能使用对方强大的工具和库。例如,在本教程中,我们将与 JavaScript 一起使用 Python 的Pandas库。
我们正在构建的东西
在本系列教程中,我们将构建一个无服务器的单页应用程序(SPA),该程序获取Netflix的电影和节目数据集,并使用Pandas来读取、净化、处理和分析数据。然后将结果显示在DOM上,供终端用户查看。
我们的最终项目是一个SPA,显示电影和电视节目的列表,推荐的电影和节目,以及有趣的事实。终端用户将能够删除和过滤电影和节目。数据在PouchDB中持久化。
在这一部分,我们将重点关注。
- 学习Pyodide和Pandas的基础知识
- 在Python和JavaScript之间共享对象和方法
- 从Python代码中操纵DOM
- 构建基本的应用结构
你可以在这里找到第一部分中你要创建的应用程序的现场演示。
用Python对DOM进行操作
在我们深入到构建应用程序之前,让我们快速地看看我们如何使用Python与DOM API交互来直接操作它。
首先,创建一个名为index.html的新的HTML文件。
<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script> </head> <body> <p id="title">My first Pyodide app</p> <script> async function main() { let pyodide = await loadPyodide(); pyodide.runPython(`print('Hello world, from the browser!')`); }; main(); </script> </body> </html>
在这里,我们从CDN上加载了Pyodide的主运行时间以及Pyodide的内置包,并使用runPython方法运行了一个简单的Python脚本。
在你的浏览器中打开该文件。然后,在浏览器的开发者工具的控制台内,你应该看到。
Loading distutils Loaded distutils Python initialization complete Hello world, from the browser!
最后一行显示,我们的Python代码在浏览器中被执行了。现在我们来看看如何访问DOM。要做到这一点,我们可以导入js库来访问JavaScript范围。
像这样更新HTML文件。
<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script> </head> <body> <p id="title">My first Pyodide app</p> <script> async function main() { let pyodide = await loadPyodide(); pyodide.runPython(` print('Hello world, from the browser!') import js js.document.title = "Hello from Python" `); }; main(); </script> </body> </html>
所以,js 代表全局对象window ,然后可以用来直接操作DOM和访问全局变量和函数。我们用它来改变浏览器/文档的标题为 "Hello from Python"。
刷新浏览器以确保它的工作。
接下来,让我们将该段的内部文本从 "我的第一个Pyodide应用程序 "更新为 "被Python取代"。
<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script> </head> <body> <p id="title">My first Pyodide app</p> <script> async function main() { let pyodide = await loadPyodide(); pyodide.runPython(` print('Hello world, from the browser!') import js js.document.title = "Hello from Python" js.document.getElementById("title").innerText = "Replaced by Python" `); }; main(); </script> </body> </html>
保存该文件,并再次刷新页面。
类型翻译
Pyodide的一个伟大功能是你可以在Python和JavaScript之间传递对象。有两种翻译方法。
隐式转换
如前所述,基本数据类型将在Python和JavaScript之间直接转换,而不需要创建特殊的对象。
| Python | JavaScript |
|---|---|
int | Number 或BigInt |
float | Number |
str | String |
bool | Boolean |
None | undefined |
| JavaScript | Python |
|---|---|
Number | int 或float |
BigInt | int |
String | str |
Boolean | bool |
undefined | None |
null | None |
让我们看一个例子。
首先,在script 标签上添加一个新的变量,叫做name 。
<script> var name = "John Doe"; // NEW! async function main() { let pyodide = await loadPyodide(); pyodide.runPython(` print('Hello world, from the browser!') import js js.document.title = "Hello from Python" js.document.getElementById("title").innerText = "Replaced by Python" `); }; main(); </script>
接下来,让我们用js看一下类型和值。
<script> var name = "John Doe"; async function main() { let pyodide = await loadPyodide(); pyodide.runPython(` print('Hello world, from the browser!') import js js.document.title = "Hello from Python" js.document.getElementById("title").innerText = "Replaced by Python" `); // NEW !! pyodide.runPython(` import js name = js.name print(name, type(name)) `); }; main(); </script>
刷新浏览器。在控制台内,你应该看到以下输出。
正如你所看到的,我们可以直接访问这个变量的值,而且它被从JavaScriptString 转换为Pythonstr 。
代理
正如你所看到的,基本类型可以直接转换为目标语言中的等价物。另一方面,"非基本 "类型需要被转换为代理对象。有两种类型的代理对象。
- JSProxy是一种代理,用于使 JavaScript 对象表现得像 Python 对象。换句话说,它允许你从 Python 代码中引用内存中的 JavaScript 对象。你可以使用to_py()方法将代理转换为一个本地的Python对象。
- PyProxy是一个让Python对象表现得像JavaScript对象的代理,允许你从JavaScript代码中引用内存中的Python对象。你可以使用toJs()方法将该对象转换为一个本地的JavaScript对象。
要将Python字典转换为JavaScript对象,使用
dict_converter参数,其值为Object.fromEntries。
dictionary_name.toJs({ dict_converter: Object.fromEntries })如果没有这个参数,
toJs()将会把字典转换成一个 JavaScriptMap对象。
JSProxy 示例
创建一个名为products 的新变量。
var products = [{ id: 1, name: "product 1", price: 100, }, { id: 2, name: "Product 2", price: 300, }];
把它导入到runPython ,并检查其类型。
pyodide.runPython(` import js print(type(js.products)) `);
Fullscript 标签。
<script> var name = "John Doe"; // NEW !! var products = [{ id: 1, name: "product 1", price: 100, }, { id: 2, name: "Product 2", price: 300, }]; async function main() { let pyodide = await loadPyodide(); pyodide.runPython(` print('Hello world, from the browser!') import js js.document.title = "Hello from Python" js.document.getElementById("title").innerText = "Replaced by Python" `); pyodide.runPython(` import js name = js.name print(name, type(name)) `); // NEW !! pyodide.runPython(` import js print(type(js.products)) `); }; main(); </script>
刷新页面后,你应该在控制台中看到,结果是<class 'pyodide.JsProxy'> 。通过这个代理,我们可以从我们的Python代码中访问内存中的JavaScript对象。
像这样更新新添加的pyodide.runPython 块。
pyodide.runPython(` import js products = js.products products.append({ "id": 3, "name": "Product 3", "price": 400, }) for p in products: print(p) `);
你应该在浏览器中看到一个AttributeError: append 错误,因为 JSProxy 对象没有一个append 方法。
如果你把
.append改为.push会发生什么?
为了操作这个对象,你可以使用to_py() 方法将其转换为一个 Python 对象。
pyodide.runPython(` import js products = js.products.to_py() products.append({ "id": 3, "name": "Product 3", "price": 400, }) for p in products: print(p) `);
你现在应该看到。
{'id': 1, 'name': 'product 1', 'price': 100} {'id': 2, 'name': 'Product 2', 'price': 300} {'id': 3, 'name': 'Product 3', 'price': 400}
PyProxy 示例
像这样更新script 标签。
<script> async function main() { let pyodide = await loadPyodide(); pyodide.runPython(` import js products = [ { "id": 1, "name": "new name", "price": 100, "votes": 2 }, { "id": 2, "name": "new name", "price": 300, "votes": 2 } ] `); let products = pyodide.globals.get("products"); console.log(products.toJs({ dict_converter: Object.fromEntries })); }; main(); </script>
在这里,我们访问了Python变量,然后通过.toJs({ dict_converter: Object.fromEntries }) 将其从Python dict转换为JavaScript对象。
刷新页面后,你应该在控制台看到以下输出。
{id: 1, name: 'new name', price: 100, votes: 2} {id: 2, name: 'new name', price: 300, votes: 2}
就这样,让我们把你新发现的Pyodide知识用于构建一个应用程序吧
Netflix数据集
我们将开发一个无服务器的SPA应用,获取Netflix数据集。然后我们将使用Pandas来读取、处理、操作和分析数据。最后,我们将把结果传递给DOM,将分析后的数据显示给终端用户。
数据集是一个CSV,其中包括以下几列。
| 名称 | 描述 |
|---|---|
| ID | JustWatch上的标题ID。 |
| 标题 | 标题的名称。 |
| 节目类型 | 电视节目或电影。 |
| 描述 | 简短的描述。 |
| 发行年份 | 发行年份。 |
| 年龄认证 | 年龄认证。 |
| 运行时间 | 剧集(节目)或电影的长度。 |
| 体裁 | 体裁的列表。 |
| 生产国 | 制作该标题的国家的列表。 |
| 季数 | 如果是一个节目,则是季节的数量。 |
| IMDB ID | 标题在IMDB上的ID。 |
| IMDB分数 | 在IMDB上的得分。 |
| IMDB 投票 | IMDB上的投票。 |
| TMDB人气 | TMDB上的受欢迎程度。 |
| TMDB得分 | TMDB上的分数。 |
安装Pyodide和TailwindCSS
像这样更新你的index.html文件的内容。
<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script> <script src="https://cdn.tailwindcss.com"></script> </head> <body class="bg-slate-900"> <div id="app" class="relative h-full max-w-7xl mx-auto my-16"></div> <script> </script> </body> </html>
正如你所看到的,我们从CDN加载了Pyodide以及用于造型的Tailwind CSS。id 我们还定义了一个<div> 元素,app ,以容纳我们接下来要建立的App组件。
创建应用程序组件
在script 标签上添加以下JavaScript代码。
class App { state = { titles:[], } view() { return `<p class="text-slate-100">Hello, World!</p>` } render() { app.innerHTML = this.view(); } }
在这里,我们定义了一个名为App 的对象。我们把它称为一个组件,因为它是一个独立的、可重复使用的代码片段。
App 组件有一个用于保存数据的状态对象,同时还有两个嵌套的函数,叫做view() 和render() 。render() 只是把从view() 输出的 HTML 代码附加到 DOM 中,把div 的id 的app 。
让我们创建一个新的App 的实例,称为appComponent ,并对其调用render() 。在App 的class 声明后添加以下代码。
var appComponent = new App(); appComponent.render();
在你的浏览器中打开该文件。你应该看到 "你好,世界!"。
添加样本数据
接下来,让我们把样本电影添加到state 。在script 标签中,就在调用appComponent.render(); 之前,用以下内容更新状态。
appComponent.state.titles = [ { "id": 1, "title": "The Shawshank Redemption", "release_year": 1994, "type": "MOVIE", "genres": [ "Crime", "Drama" ], "production_countries": [ "USA" ], "imdb_score": 9.3, "imdb_votes": 93650, "tmdb_score": 9.3, }, { "id": 2, "title": "The Godfather", "release_year": 1972, "type": "MOVIE", "genres": [ "Crime", "Drama" ], "production_countries": [ "USA" ], "imdb_score": 9.2, "imdb_votes": 93650, "tmdb_score": 9.3, } ];
现在,我们可以通过在我们的App 类中这样更新view() ,构造一个表格来显示数据。
view() { return (` <div class="px-4 sm:px-6 lg:px-8"> <div class="sm:flex sm:items-center"> <div class="sm:flex-auto"> <h1 class="text-4xl font-semibold text-gray-200">Netflix Movies and Shows</h1> </div> </div> <div class="mt-8 flex flex-col"> <div class="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8"> <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8"> <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg"> <table class="min-w-full divide-y divide-gray-300"> <thead class="bg-gray-50"> <tr> <th scope="col" class="whitespace-nowrap py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Title</th> <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Type</th> <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Release Year</th> <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Genere</th> <th scope="col" class="whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900">Production Country</th> </tr> </thead> <tbody class="divide-y divide-gray-200 bg-white"> ${this.state.titles.length > 0 ? this.state.titles.map(function (title) { return (` <tr id=${title.id}> <td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-500 sm:pl-6">${title.title}</td> <td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-gray-900">${title.type}</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">${title.release_year}</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">${title.genres}</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">${title.production_countries}</td> </tr> `) }).join('') : (` <tr> <td class="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-500 sm:pl-6">Titles are loading...</td> <td class="whitespace-nowrap px-2 py-2 text-sm font-medium text-gray-900">Titles are loading...</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">Titles are loading...</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">Titles are loading...</td> <td class="whitespace-nowrap px-2 py-2 text-sm text-gray-500">Titles are loading...</td> </tr> `) } </tbody> </table> <div> </div> </div> </div> </div> `) }
所以,我们-
- 添加了一个表元素,其中有标题、类型、发行年份、流派和生产国等列。
- 检查了
state.titles数组的长度,看它是否包含任何标题。如果有,我们就循环查看,并为每个标题创建一个表行。如果没有,我们就创建一个带有加载信息的表行。
在浏览器中刷新页面。
潘达斯数据操作
安装潘达斯
为了在Pyodide中加载Python包,你可以在初始化Pyodide后直接使用loadPackage函数。
比如说。
let pyodide = await loadPyodide(); await pyodide.loadPackage("requests");
你可以使用一个列表来加载多个包。
await pyodide.loadPackage(["requests", "pandas", "numpy"]);
回到你的HTML文件中,在appComponent.render(); 之后添加一个main 函数。
async function main() { let pyodide = await loadPyodide(); await pyodide.loadPackage("pandas"); }
不要忘记调用它。
在你的浏览器中刷新页面。你应该在控制台中看到以下内容。
Loading pandas, numpy, python-dateutil, six, pytz, setuptools, pyparsing Loaded python-dateutil, six, pytz, pyparsing, setuptools, numpy, pandas
所以,Pandas和相关的子依赖已经被加载到浏览器中了!
读取和操作数据
在这一节中,我们将从互联网上获取一个CSV文件,将其读入一个Pandas DataFrame,对其进行消毒和处理,最后将其传递给状态。
Python代码。
import js import pandas as pd from pyodide.http import pyfetch # 1. fetching CSV from and write it to memory response = await pyfetch("https://raw.githubusercontent.com/amirtds/kaggle-netflix-tv-shows-and-movies/main/titles.csv") if response.status == 200: with open("titles.csv", "wb") as f: f.write(await response.bytes()) # 2. load the csv file all_titles = pd.read_csv("titles.csv") # 3. sanitize the data # drop unnecessary columns all_titles = all_titles.drop( columns=[ "age_certification", "seasons", "imdb_id", ] ) # drop rows with null values for important columns sanitized_titles = all_titles.dropna( subset=[ "id", "title", "release_year", "genres", "production_countries", "imdb_score", "imdb_votes", "tmdb_score", ] ) # Convert the DataFrame to a JSON object. ('orient="records"' returns a list of objects) titles_list = sanitized_titles.head(10).to_json(orient="records") # 4. set titles to first 10 titles to the state js.window.appComponent.state.titles = titles_list js.window.appComponent.render()
请注意代码的注释。
将这段代码添加到main 中的runPythonAsync方法中。
async function main() { let pyodide = await loadPyodide(); await pyodide.loadPackage("pandas"); await pyodide.runPythonAsync(` // add the code here `); }
接下来,删除appComponent.state.titles 。同时,我们需要修改view 方法中的这一行。
${this.state.titles.length > 0 ? this.state.titles.map(function (title) {
到。
${this.state.titles.length > 0 ? JSON.parse(this.state.titles).map(function (title) {
为什么?
titles_list ( )是一个JSON字符串,所以为了迭代它,我们需要对它进行反序列化。titles_list = sanitized_titles.head(10).to_json(orient="records")
在你的浏览器中刷新该页面。你应该首先在表格中看到一个加载信息。在Pyodide加载后,Pandas导入,并且在脚本执行完毕后,你应该看到完整的电影列表。
总结
我们在本教程中涵盖了很多内容。我们看了Pyodide如何让你在浏览器中运行Python代码,让你拥有以下能力。
- 在浏览器中直接加载和使用Python包。(我们使用Pandas来读取和分析一个CSV文件)。
- 从Python代码中访问和操作DOM。(在我们的 Python 代码中导入 js 使我们能够访问 DOM。)
- 在Python和JavaScript之间共享和访问对象和命名空间。(在我们的Javascript代码中,我们创建了一个组件,我们能够在我们的Python代码中访问这个组件,以便管理它的状态和调用它的方法。)
你可以在这里找到本教程的源代码。
--
不过,我们仍然缺少一些东西,我们需要解决一些问题。
- 首先,我们还没有对导入的CSV文件做很多处理。Pandas给了我们很大的权力来轻松分析和处理数据。
- 第二,Pyodide可能需要一些时间来初始化和运行Python脚本。由于它目前在主线程中运行,它使应用程序瘫痪,直到它运行完毕。我们应该把Pyodide和Python脚本移到一个Web Worker中,以防止这种情况。
- 第三,我们还没有看到完整的类似SPA的行为。我们仍然需要更新该组件,以添加事件监听器来响应用户的操作。
- 最后,Python脚本部分在代码编辑器中没有语法强调。另外,它开始变得难以阅读了。我们应该把这段代码移到一个Python包中,并把它导入Pyodide。这将使它更容易维护和扩展。
我们将在接下来的教程中介绍这四件事!
--
Python单页应用系列。
- 第一部分(本教程!)。学习Pyodide的基础知识并创建基础应用程序
- 第二部分(即将开始)。用Pandas分析和处理数据,并使用网络工作者来加快应用程序的速度
- 第三部分(即将推出)。创建一个Python包,添加额外的功能,并添加一个持久的数据层