用 Pycco 为 Python 代码快速创建双列注释网页

文摘   2025-01-04 22:54   新加坡  

最近,我在网上看到了一个专门介绍深度学习源代码的网站(https://nn.labml.ai/index.html ),各类流行网络架构一应俱全。这个网站最吸引我的,是它的双列式结构。如下所示,每个页面右边显示代码,左边显示每段代码对应的注释。

一直以来,我在博客中介绍代码时,都是先写描述文字,再贴一段代码。这种方式对作者和读者来说都十分低效。受到上面那个网站的启发,我决定以后也采用这种方式介绍源代码。很可惜,上面那个网站是由 labml.ai 这个组织维护的,似乎并没有提供开源的、可定制的注释网站搭建方式。为此,我找到了一个替代品:Pycco。它可以方便地为 Python 代码生成双列带注释的静态页面。

Pycco 安装与使用

Pycco 是页面生成工具 Docco 的 Python 实现版。它们都是「快而脏」的文档生成器:源代码只有几百行,没有多余封装,直观而暴力地生成 HTML 页面的所有内容。它们为初学者只提供了一个文档页面,这个文档页面讲的是它们的源代码,且文档页面就是由它们本身创建出来的。Pycco 的官方文档页面如下所示。一开始介绍完 Pycco 的背景信息和安装方式后,文档就会直接开始介绍源代码。

根据文档的指示,我们可以通过下面的命令在 pip 里安装 Pycco:

pip install pycco

在使用 Pycco 时,我们完全不用去理解其源代码的意思,只需要准备一个带注释的 Python 源文件就行。Pycco 提供了一键式命令,帮我们把源文件里的注释和代码分离,并生成左侧为注释,右侧为代码的静态文档页面。这里的注释既包括了 # 开头的单行注释,也包括了 """ """" 包裹的多行注释。注意,三个单引号构成的多行注释 ''' ''' 不会被该工具识别。

我们来看一个示例。假设我们有下面一个名为 hello.py 的 Python 源文件。

"""
Hello
World
"""

import torch
import pycco

# Hello
print("hello world")

"""
End of file
"""

我们用下面的命令为其生成文档页面。

pycco hello.py

页面会生成在目录下的 docs 子目录里,子目录包含一个 hello.html 文件和一个 pycco.css 文件。我们可以用浏览器打开 .html 网页,看到下图所示内容。

注释默认使用 Markdown 语法。如果平时就习惯用 Markdown 写博客的话可以无缝切换。但是,默认情况下网页无法渲染公式。

页面跳转

除了普通的 Markdown 语法外,Pycco 还支持一个特殊的功能:文档跳转。我们可以把文件名写在 [[ ]] 内,实现源文件内部或源文件间的跳转。特别地,我们还可以在页面某处打上标签,并跳转到某页面的标签处。以下是一个示例。为了给「注释」加上注释,我别出心裁地把注释写进了代码部分。

让页面渲染 LaTeX 公式

刚才我们发现,目前的 Pycco 页面并不支持公式渲染。而在解释深度学习代码时,很多时候不得不用到公式。因此,我决定给 Pycco 加上渲染公式的功能。

Pycco 这种直观暴力的实现方法让网页开发者能够快速地修改页面生成逻辑。然而,我已经把 HTML 的知识快忘光了,配不上「网页开发者」这个名号。因此,我让 ChatGPT o1 来帮我开发这一功能。

经指导,我认识了 MathJax 这个在网页上渲染公式的工具。只需要在 HTML 的 head 里导入一些包,网页就可以自动识别单行公式 $ $ 和多行公式 $$ $$。我不记得 head 是什么了,但大概能猜到这个是一个相当于声明全局变量的语句块。

我在 pycco_resources\__init__.py 文件里找到了设置 head 的地方。这个文件提供了生成网页的模板,包括了写死的 CSS 文件和部分 HTML 代码。打开这个文件的最快方式是在某处写 import pycco_resources,之后用 IDE 的代码跳转找到这个包在本地的位置。

我们在该文件下图所示的位置插入和 MathJax 相关的代码。

<script>
    MathJax = {
      tex: {
        inlineMath: [ ["$","$"] ]
      }
    };
  
</script>
  <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>

修改模板后,重新生成网页,可以发现现在公式能够正确渲染了。

高亮新增/删除代码

在以后讲解代码的时候,我想高亮新增的或者删去的代码,就像 GitHub 里显示代码的更改一样。由于 Pycco 非常容易做二次开发,我又去请教 ChatGPT o1 该如何实现这样的功能。

经指导,我了解到代码高亮可以通过设置背景颜色 style 来实现。为此,我需要做两件事:

  1. 新增有关背景颜色的 CSS style
  2. 想办法指定要高亮的代码块

第一件事很简单,只需要打开模板文件 pycco_resources\__init__.py,添加背景颜色 style 即可。我添加了两种背景颜色,分别表示删去的和新增的代码块。我是在 VSCode 里找到想要的颜色值的。随便打开一个 CSS 文件,输入一个大概的颜色值,VSCode 就会弹出一个选择颜色的小窗口。

.highlighted-line-1 {
  background-color: #fa53208c;
}
.highlighted-line-2 {
  background-color: #68fc5d;
}

第二件事就比较难了。我需要先看懂目前高亮源代码的逻辑,再在其基础之上添加背景高亮的功能。Pycco 的主函数在文件 pycco/main.py 里,我们可以用导入 Python 包的方式快速找到这份源文件。原来的高亮关键字的逻辑如下,我在其中加入了一些代码用于输出中间结果。

函数的主要输入是列表 sections,函数的输出存储在列表项 section 的 "docs_html""code_html" 字段。

sections 内容如下,可见其中是过了解析器的注释块和代码块。每一段注释都会对应一段代码。

[{'docs_text': '=== BeGin ===\r\n\n', 'code_text': 'import torch\r\nimport pycco\r\n\r'}, {'docs_text': '# Hello\r\n## World\r\nThis is a **Python** [code](https://www.python.org/).\r\nTry formula $\\epsilon$ .\r\n$$\r\n3 = 1 + 2\r\n$$\r\n\n\n', 'code_text': 'print("hello world")\r\n\r'}, {'docs_text': 'End of file\r\n\n', 'code_text': '\n'}]

函数的输出是完整 HTML 文件的一部分。

<h3><span id="begin" href="begin"> BeGin </span></h3>
<div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">torch</span>
<span class="kn">import</span> <span class="nn">pycco</span></pre></div>
<h1>Hello</h1>
<h2>World</h2>
<p>This is a <strong>Python</strong> <a href="https://www.python.org/">code</a>.
Try formula $\epsilon$ .
$$
3 = 1 + 2
$$</p>
<div class="highlight"><pre><span class="nb">print</span><span class="p">(</span><span class="s2">&quot;hello world&quot;</span><span class="p">)</span></pre></div>
<p>End of file</p>
<div class="highlight"><pre></pre></div>

那么,我的目标就是修改这个文件。我需要先根据输入的原始代码块,判断这段代码是否要高亮,再修改 HTML 代码块的内容。

首先,从用户的角度考虑,应该怎么指定要背景高亮的代码呢?既然现在代码被拆成了一块一块的,且每块代码对应一段注释,我决定用一些特殊注释来高亮一整块代码,就像前面的设置标签和跳转标签的特殊注释一样。我加入了以下判断:如果注释前几个字符是 '===ADD===' 或 '===DEL===',就用对应的颜色高亮这段代码。

判断了是否要高亮后,我还需要做对应的修改。我不仅要在 HTML 代码块里高亮代码,还需要把注释块里的特殊命令删掉。通过观察相关代码,我忽然回忆起了 HTML 的部分实现原理:背景高亮就是把一段 HTML 字符串封进一个带有背景高亮样式的标签 <div></div> 里。那剩下的删除注释也很简单,只需要对字符串做一点简单操作就行了。代码修改过程及使用示例如下所示。

# Add following code

delete_background_start = '<div class="highlighted-line-1">'
add_background_start = '<div class="highlighted-line-2">'
background_end = '</div>'

if section["docs_text"].startswith('===ADD==='):
    section["docs_text"] = section["docs_text"][9:]
    section["code_html"] = add_background_start + section["code_html"] + background_end
elif section["docs_text"].startswith('===DEL==='):
    section["docs_text"] = section["docs_text"][9:]
    section["code_html"] = delete_background_start + section["code_html"] + background_end

修正多行注释缩进

在写这篇博文的时候,我又发现 Pycco 的一个 bug:在多行注释带缩进时,HTML 页面上的注释也会缩进,导致页面布局完全乱套。

我找到了 pycco/main.py 里处理多行注释的代码。我把旧代码全部删掉,换了一种暴力的字符串处理方式:删除多行注释左侧所有空格及制表符。

docs_text += line.lstrip('\t ') + '\n'

改完之后 bug 就修好了。

总结

在这篇文章中,我介绍了 Pycco 这个为 Python 代码生成双列注释-代码静态页面的 Python 工具,并分享了我对其的三个修改:支持 MathJax 公式、支持背景高亮、修复多行注释缩进 bug。相比介绍开源项目的 readthedocs 等文档构建工具,Pycco 能够为每行 Python 代码写注释,尤其适合详细讲解深度学习代码。对于熟练的网页开发者,Pycco 的源代码很短,改起来很方便;但另一方面,Pycco 似乎比较小众,多年都没人维护,可能有各种 bug。主要推荐想在个人博客里讲解代码的朋友使用这个工具。我以后都会用它来讲解代码。

ChatGPT 小插曲

ChatGPT 在指导我添加 MathJax 时,先叫我下载 python-markdown-math 这个 pip 包,让 Python 脚本里的 Markdown 解析器能够解析公式。但我在实践中发现不解析 $$ 符号,在正确导入 MathJax 包后,网页依然能够正确渲染。

ChatGPT 一开始提供的 MathJax 导入代码是 MathJax V3 的。经过 python-markdown-math 解析后,双行注释被转换成了 <script type="math/tex; mode=display"></script> 包裹的内容。这些公式没法成功渲染。我把添加双行公式的详细过程提供给 ChatGPT 后,它分析出这种 <script> 的写法是 MathJax V2 的,让我把 head 里的包改成 MathJax V2。修改过后,双行公式果然渲染成功了。然而,如上一段所述,我发现哪怕不用 python-markdown-math 也行。于是我又用回了 MathJax V3。可见 ChatGPT 有功有过,给我制造了一些麻烦,也算是能够帮我解决。

在求助 ChatGPT 添加背景高亮功能时,它非常出色地分析了问题:先理解 Pycco 生成 HTML 的过程,再决定用户如何指定要高亮的内容,最后根据不同的方案做实现。但它给的三个信息输入方案都不尽如人意:(1) 在命令行里指定要高亮的行;(2) 在每行要高亮的代码前都加特殊注释;(3) 用另一个配置文件来指定要高亮的行。这些方法的用户体验都不好,且难于实现。当然,由于我没有把所有源代码给它,它或许没有发挥完整的实力。最后我还是靠自己理解了 Pycco 的页面生成逻辑,选了一种好实现且好用的方案。

我从来没有用 ChatGPT 做过特别复杂的编程任务,这次稍微体会了一下。感觉它的能力有时候会出乎意料地强(比如发现 HTML 的风格是 MathJax V2 的),但不见得次次都靠谱。它的能力主要还是基于大量数据的「搜索」,加上一点浅显的思考。只是因为看过的数据过多,所以很多时候能从历史经验中直接给出很好的答案。当然,作为语言模型,它对于语言的理解能力极强,哪怕描述不完全也能准确理解用户的意图。做为使用者,我们要在用之前就想好能从 ChatGPT 那里获取到哪些帮助,并提供充足的信息,而不能全盘接受它的输出,就和我们日常和别人交流一样。


天才程序员周弈帆
NTU MMLab 在读博士生,ACM金牌选手的个人博客。主要分享深度学习、算法教程。放眼全世界,几乎没有比我讲得更易懂、亲民的人,不信你去读读看。
 最新文章