开发一个自动在Notion中绘制图表的工具
2023 年 9 月 18 日

实际上,每个人都无法从Notion表格中可视化数据,这就是为什么我开发了一个可以自动在Notion中绘制图表的工具。

从我的角度来看,它是这样的:

第一部分 问题陈述 #

问题实际上是Notion中没有图表。因此,我们需要创建这样一个工具:

  1. 将接收一个可能包含图表描述的页面列表。
  2. 从页面中收集图表的描述。
  3. 在描述块后面添加一个图表,并删除原来的描述块。 ## 第2部分. 快速解释

我们将使用Django编写服务器,因为非官方的Notion API库是用Python编写的。然后我们将将我们的东西上传到Heroku。我们将使用IFTTT以一定的频率从Heroku拉取我们的东西。

那么我们应该实际上写些什么呢?

  1. 一个用于响应IFTTT请求的函数
  2. 一个用于搜索情节描述的函数
  3. 一个用于绘制情节并将其上传到Notion的函数
  4. 一个用于从描述中提取数据的函数

第3部分. 编写代码 #

我们去Notion,按下Ctrl + Shift + J,然后我们去应用程序-> Cookie并复制token_v2。让我们将其命名为TOKEN。

我们应该以某种方式存储一个页面数组,其中情节的描述可能存在。实际上很简单:

PAGES = [        "https://www.notion.so/mixedself/Dashboard-  40a3156030fd4d9cb1935993e1f2c7eb" ]
``` 为了某种程度上解析描述本身,我们需要以下关键词:

1. X轴上的数据
2. Y轴上的数据
3. 表格视图的URL

**代码如下所示:**

BASE_KEY = "基础:" X_AXIS_KEY = "X轴:" Y_AXIS_KEY = "Y轴:"


而一个空的图表描述如下所示:

def get_empty_object(): return { "数据库": "", "X轴": "", "Y轴": "" }


我们需要某种方式来检查描述是否为空。为此,我们将编写一个特殊函数:如果所有字段都不为空,则可以开始绘制图表。

def is_not_empty(thing): return thing != ""

def check_for_completeness(object): return is_not_empty(object["数据库"]) and is_not_empty(object["X轴"]) and is_not_empty(object["Y轴"])


需要以某种方式提取数据(实际上只是文本)以生成描述。让我们编写一些函数来实现这一点。

**一个小的解释** Notion将粗体字体(如图所示)存储为以下形式。

def br_text(text): return "" + text + "" def clear_text(text): return text.replace(br_text(BASE_KEY), "").replace(BASE_KEY, "") \ .replace(br_text(X_AXIS_KEY), "").replace(X_AXIS_KEY, "") \ .replace(br_text(Y_AXIS_KEY), "").replace(Y_AXIS_KEY, "").strip()


现在让我们来写一下,也许是我们的东西的主要函数。下面的代码解释了这里发生了什么:

def plot(): client = NotionClient(token_v2=TOKEN) for page in PAGES: blocks = client.get_block(page) thing = get_empty_object() for i in range(len(blocks.children)): block = blocks.children[i] print(block.type) if block.type != "image": title = block.title if BASE_KEY in title: thing["database"] = clear_text(title).split("](")[0].replace("[", "") elif X_AXIS_KEY in title: thing["x"] = 我们将我们的库连接到Notion。然后,我们遍历一个页面数组,这些页面可能需要绘图。我们检查页面的每一行,看看其中是否包含我们的键。如果在一行中找到键,则清除该文本并放入对象中。一旦对象完成,我们就检查生成的图表是否已经存在(如果是,则删除它),并绘制一个。 现在让我们编写一个从表格获取数据的函数。

def get_lines_array(thing, client):
    database = client.get_collection_view(thing["database"])
    rows = database.default_query().execute()
    lines_array = []
    for i in range(1, len(rows)):
        previous_row = rows[i - 1]
        current_row = rows[i]
        line = [(get_point_from_row(thing, previous_row)), (get_point_from_row(thing, current_row))]
        lines_array.append(line)
    return lines_array

在这里,我们获取表格内容并遍历所有行,形成一个从点到点的线段数组。

那么get_point_from_row实际上是什么呢?问题是,如果数据是日期,我们应该重新解析它,以便matplotlib能够正确处理:

def get_point_from_row(thing, row):
    x_property = row.get_property(thing["x"])
    y_property = row.get_property(thing["y"])
    if thing["x"] == "date":
        x_property = x_property.start
    if thing["y"] == "date":
        y_property = y_property.start
``` 现在我们准备绘制我们的图表。

```python
def draw_plot(client, thing, block, page):
    photo = page.children.add_new(ImageBlock)
    photo.move_to(block, "after")
    array = get_lines_array(thing, client)
    print(array)
    for i in range(1, len(array)):
        points = reparse_points(array[i - 1:i][0])
        plt.plot(points[0], points[1], color="red")
    if not path.exists("images"):
        os.mkdir("images")
    if thing["x"] == "date":
        x_axis_dates()
    filename = "images/" + random_string(15) + ".png"
    plt.savefig(filename)
    print("Uploading " + filename)
    photo.upload_file(filename)

在这里,我们添加一个新的块(带有照片),将其移到图表描述的下方。然后我们重新解析点(下面会详细介绍),使用matplotlib绘制线条,使用随机文件名保存生成的图像,并将其加载到图像块中。

我们需要重新解析这些点,因为matplotlib接受不同的数据输入表示形式,与当前实现方式不同。

def reparse_points(points):   return [      [points[0][0], points[1][0]],      [points[0][1], points[1][1]]   ]

如果仔细观察,该方法检查我们在x轴上的数据是否为日期。如果是,则需要正确显示:

def x_axis_dates(ax=None, fig=None):   if ax is None:      ax = plt.gca()   if fig is None:      fig = plt.gcf()   loc = mdates.AutoDateLocator()   fmt = mdates.AutoDateFormatter(loc)   ax.xaxis.set_major_locator(loc)   ax.xaxis.set_major_formatter(fmt)   fig.autofmt_xdate()

现在让我们编写一个函数,当我们收到POST请求时启动一个新线程。为什么是POST?因为如果需要POST请求来开始绘图,那么您不会因为错误(通过访问URL)而启动它。

为什么要新建线程?IFTTT, ## 第4部分. IFTTT

转到创建applets (opens new window)。选择触发器(在我们的情况下是日期和时间),勾选“每小时”。选择Webhook作为触发的事物,在测试目的下设置我们(到目前为止)的本地地址。实际上,就是这样。测试一下吧。

第5部分. Heroku #

你可能会问,为什么我们使用IFTTT作为触发器?因为我们可以使用Heroku,我们的导出程序将免费运行。Heroku有免费的计划来托管我们的事物。最重要的是,服务器每天至少要休眠6个小时。而且他肯定会休眠,因为我们每小时都会触发它,而不是每分钟触发。 然后我们按照以下步骤进行操作:前往Heroku 创建新产品 (opens new window)。然后通过Heroku客户端 (opens new window)安装新的操作系统。然后按照说明进行其他操作。

在将所有内容上传到Heroku后,前往IFTTT的小程序并将旧的URL更改为全新的URL。

现在一切应该正常工作。