修复一个在Sphinx中曾经能正常显示的Graphviz dot图像,但现在无法显示

前端开发 2026-07-08

我接手了一门我已经教了好几年的Python课程。我不时重新整理这门课,以便引用最新的Python文档,当然由于课程文档是用Sphinx构建的,每次把构建环境更新到新版以便让外观在与现代Sphinx生成的文档上保持大致一致时,都会带来一些兼容性上的变动。

课程的一部分讲解Python的别名,以及对浅拷贝要小心的地方,我们用一个 digraph 输入来生成所需的图像。不幸的是,我无法找回原始图像(早前丢失),但基本结构大致是这样的:

     x → [ | | ]
         /  |  \
        /   |   \
       /    |    \
      ↙︎     ↓     ↘︎
[1|2|3]  [4|5|6]  [7|8|9]
      ↖︎     ↑     ↗︎
       \    |    /
        \   |   /
         \  |  /
     y → [ | | ]

其中竖线和对角线是来自 xy 外部 list 的定向箭头,展示了浅拷贝对通过以下方式生成的嵌套 list 的影响:

x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
y = list(x)
y = x.copy()  # equivalent
y = x[:]      # equivalent

用于生成该图像的原始代码,在较早版本的Sphinx中能够工作(我现在使用的是9.0.1版本)的原始代码是:

.. digraph:: alias_3

    node [colorscheme=pastel28, fillcolor=1, style=filled];
    { node[shape=box, fillcolor=2];
      x; y;
    }
    { node[shape=record, fillcolor=3];
      list123 [label="1|2|3"];
      list456 [label="4|5|6"];
      list789 [label="7|8|9"];
      listX [label="<1>|<2>|<3>"];
      listY [label="<1>|<2>|<3>"];
    }
    subgraph { rank=same; x -> listX; }
    subgraph { rank=same; y -> listY; }
    listX:1 -> list123;
    listX:2 -> list456;
    listX:3 -> list789;
    list123 -> listY:1 [dir=back];
    list456 -> listY:2 [dir=back];
    list789 -> listY:3 [dir=back];

但在Sphinx 9.0.1(如果有必要的话,还包括底层程序 dot 2.44.0)时,会报错,提示与那个 digraph 对应的 dot 代码执行出错,以及 stderr 的输出(它以 bytes 的repr显示,但我为了便于阅读而进行了解码):

警告:相邻节点之间存在一个具有记录形状的扁平边 - 将记录替换为HTML风格的标签

边x -> listX

错误:丢失x listX边

错误:丢失y listY边

我大概明白问题所在(原来,dot 指向同等级的记录时箭头是可以的,但现在它不喜欢了,因为它看起来像是指向整条记录中的单个元素,而不是整个记录,因此被禁止),是的,我大概可以通过把 x 放在最顶端并指向它的列表,而 y 指向底部并向上指向它的列表来实现,但我不想这么做,因为:

  1. 左侧绑定的实际名称对象看起来比较自然;实际名称应在一个可预测的位置被找到,然后从那里继续跟随引用的迷宫,且
  2. 从三行到五行会大幅增加纵向图像的尺寸,我在课堂上会把它放大显示在屏幕上;五行的显示就只剩下图像,没有空间来仅用于演示目的在图像和生成代码之间来回引用。

我也就此尝试了很久,甚至(真是)向gemma-4求助,结果gemma-4不断给出新的代码块(在我的默认提示中要求对正确性的置信度,它自称有90-100% 的信心),但都没有一个能真正工作。例如:

  • 把它改成 shape=none,并把标签做成类似 list123 [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"><TD>1</TD><TD>2</TD><TD>3</TD></TABLE>>]; 的形式,构建时没有错误,但显示的是一个框,只包含 list123,而不是三个框分别包含 123。尝试把它引用为 list123 [label="<TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'><TD>1</TD><TD>2</TD><TD>3</TD></TABLE>"]; 时,单个框成功包含原始文本 "<TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'><TD>1</TD><TD>2</TD><TD>3</TD></TABLE>
  • 保持记录形状,将 listXlistY 的元素重命名为 "<p1> | <p2> | <p3}"(不太清楚为何要改名以添加 p,但也就随它去),然后让 subgraph 语句变成 subgraph { rank=same; -> listX:p1; }(以明确表示我们可以指向整条记录的最左边的值,完全没问题我们不指向整个记录),再次失败,并出现我一开始遇到的同样的扁平边错误。
  • 再次尝试 shape=none 方案,但这次不是使用外层的 <>"",而是在HTML中直接放入内容且没有外部定界符(在第一个 '1' 处就因为语法错误而失败)。
  • subgraph 与边声明分离为:

```none subgraph { rank=same; x; listX; } subgraph { rank=same; y; listY; }

x -> listX [constraint=false]; y -> listY [constraint=false]; ```

这点令人困惑地,有时再次失败,仍会出现同样的“指向记录的扁平边”错误,另一些时候却能工作(似乎有一些奇怪的缓存机制在干扰行为;如果我改动一个完全不同的图,有时会让前一个图渲染,哪怕渲染不正确,在此之前根本不渲染),但不会在 x/y 与各自的 list 之间画出边(并且把 y 放在它的 list 的右侧而不是左侧)。

在现代的Sphinx/dot中,我现在所做的还能实现吗?如果能,该怎么做?是不是存在一个XY问题,其实有更简单的方式,用dot语言紧凑地生成一个有向图,显示名称与列表之间的引用关系?

解决方案

我对Sphinx一无所知,但你使用的Graphviz版本相当旧。可以在这里下载15.0:https://www.graphviz.org/download/

这是不是你要找的?

所有的 记录 节点都改写为HTML(https://www.graphviz.org/doc/info/shapes.html#html

(gemma-4 已接近,但缺少 <TR>)

//
//  from https://stackoverflow.com/questions/79951943/fixing-a-dot-graphviz-image-that-used-to-work-in-sphinx-but-no-longer-does
//
digraph X {
    node [colorscheme=pastel28, fillcolor=1, style=filled];
    { node[shape=box, fillcolor=2];
      x; y;
    }
    { node[shape=record, fillcolor=3];
      list123 [shape=none
         label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
     <TR><TD>1</TD><TD>2</TD><TD>3</TD></TR>
     </TABLE>>];
      list456 [shape=none
         label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
     <TR><TD>4</TD><TD>5</TD><TD>6</TD></TR>
     </TABLE>>];
      list789 [shape=none
         label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
     <TR><TD>7</TD><TD>8</TD><TD>9</TD></TR>
     </TABLE>>];
      listX [shape=none
         label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
     <TR><TD port="1"> </TD><TD port="2"> </TD><TD port="3"> </TD></TR>
     </TABLE>>];
      listY [shape=none
         label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
     <TR><TD port="1"> </TD><TD port="2"> </TD><TD port="3"> </TD></TR>
     </TABLE>>];
    }
    subgraph { rank=same; x -> listX; }
    subgraph { rank=same; y -> listY; }
    listX:1 -> list123;
    listX:2 -> list456;
    listX:3 -> list789;
    list123 -> listY:1 [dir=back];
    list456 -> listY:2 [dir=back];
    list789 -> listY:3 [dir=back];

}

给出:
enter image description here

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章