NetworkX 1.11 旧项目兼容指南:图算法、离线安装与版本共存

发布时间:2026/10/30 3:20:17
NetworkX 1.11 旧项目兼容指南:图算法、离线安装与版本共存

简介:NetworkX 1.11 的源码分发包,面向需要研究经典图论与复杂网络实现的 Python 开发者、数据科学学习者及教学人员,可用于离线查阅图算法源码、复现网络分析案例,或作为旧版本兼容与二次开发的基础材料。包内共594个文件,以429个 Python 模块为主体,辅以117个 rst 文档、若干 txt 说明、bz2/gz 压缩数据以及 png、html、css 等配套资源,整体约1.47MB;目录涵盖核心算法、文档、测试与示例数据,结构较为完整。已有83人学习或下载,说明该版本在相关学习与工程场景中仍具参考价值。读者可从中获得 NetworkX 1.11 的核心图结构、算法实现与文档说明,也能借助随包图数据压缩文件和测试材料理解图的读写、序列化及典型分析流程,适合按模块研读源码、对照文档验证算法,或用于课程实验与项目原型中的图计算需求。

1. 上周救的一个老工程,让我重新翻出了这个包

上周协助一位开发者处理一个无法正常运行的旧项目,报错只有一行,顺着调用栈追下去发现是 networkx 版本不匹配——代码里使用的connected_component_subgraphs、G.node这些写法,在新版里已经被移除或改名。networkx-1.11.zip 就是这种局面下的应急方案:它是 NetworkX 1.x 系列后期的源码发行包,解开后就是一套完整的 Python 源码目录,带 setup.py、算法实现、测试用例和示例脚本,纯标准库就能建图跑算法,不需要任何 C 扩展。

它主要面向三类人:维护十几年前调度、关系分析、路径规划代码的从业者,照老教材复现实验的学生,以及内网离线机器上装不了新版包的人。新版功能更多、性能更好,但 1.x 的 API 边界更贴近老代码,强行换用新版通常会导致大面积报错。下面按“拆包、建图、跑算法、避坑、共存”的顺序,把我实际跑过的路径写清楚。

2. 拆包先看结构:目录清单、依赖边界与三种安装方式

2.1 zip 解开之后,这些目录各管什么事

把 zip 解压到一个干净目录,先用find . -maxdepth 2 -type d看一下,结构其实很规整。源码包的目录组织按功能切分,即便后续不修改源码,知道哪个目录对应哪块能力,排查问题时也能少走弯路。

路径内容什么时候会打开
setup.pydistutils 安装脚本、元信息、依赖声明安装、查版本、看 install_requires
networkx/classes/Graph、DiGraph、MultiGraph 及其视图改数据结构行为、看属性存储方式
networkx/algorithms/最短路、连通性、中心性、聚类、社区调算法、翻具体实现参数
networkx/generators/随机图、经典图、格点图造测试数据
networkx/readwrite/GML、GraphML、edgelist、adjlist、pajek数据落盘与跨工具交换
networkx/drawing/matplotlib 出图、graphviz 布局适配可视化
networkx/linalg/邻接矩阵、拉普拉斯、谱分析与 numpy/scipy 交互
doc/、examples/文档源与可运行示例脚本学习写法、验证环境
networkx/tests/与各子包内tests/单元测试装完体检

依赖边界是这份源码包最值得说的一点。核心建图和算法部分只依赖 Python 标准库,加上一个decorator包(1.x 系列在networkx/utils/decorators.py里直接from decorator import decorator,缺它连import networkx都会失败)。matplotlib只在networkx/drawing/里被用到,numpy和scipy只在networkx/linalg/以及少数需要矩阵运算的算法里被导入,pygraphviz或pydot只在需要 graphviz 布局时才要求。这意味着在只有标准库的离线环境里,装完decorator就能用八成功能,这是它比新版更容易在旧机器上运行的原因。

提示:如果只做图计算、不做可视化和谱分析,不必先安装 numpy/scipy/matplotlib,可以避免其他依赖问题干扰核心功能。

2.2 三种安装方式,按“是否污染环境”挑一种

我一般会先用head -n 30 setup.py快速看一眼元信息,确认包名和版本号字段没有被改过,然后再选择安装方式。下面三条路径分别对应“装进当前解释器”“pip 直接指向 zip”“装到独立目录”,风险依次降低。

# 1) 解压并核对包内元信息 unzip networkx-1.11.zip -d /opt/src cd /opt/src/networkx-1.11 grep -nE "name=|version=|install_requires" setup.py | head # 2) 方式 A:走 distutils,直接装进当前解释器 python setup.py install # 3) 方式 B:pip 直接指向 zip,省掉手工解压 python -m pip install /opt/src/networkx-1.11.zip # 4) 方式 C:装到独立目录,完全不动全局 site-packages python -m pip install --no-deps --target=/opt/vendor/nx111 /opt/src/networkx-1.11.zip

方式 A 走的是setup.py install,它把文件写进当前解释器的site-packages,副作用最大,装之前最好确认python -c "import sys; print(sys.executable)"指向的是你要动的那一个。方式 B 让 pip 自己解压再构建,好处是会记录到pip list里,卸载干净;如果源上的包和本地 zip 同时存在,pip 会优先用你给的路径。方式 C 用--target指定落地目录,配合--no-deps跳过依赖解析,装完的目录需要手动加进sys.path才能用,适合“同一台机器上既要新版又要 1.11”的场景。

--prefix参数也常用,python setup.py install --prefix=/opt/pyenv/nx111会把包装到自定义前缀下的lib/pythonX.Y/site-packages,但对 distutils 来说,你得自己把那个路径塞进PYTHONPATH,不如--target省事。另外,--no-deps跳过了decorator,需要在目标环境里单独准备一份,否则导入阶段就会失败。

2.3 装完先跑冒烟脚本,别急着上真实数据

装完立刻验证,比之后在业务代码里调试报错更省时间。下面这段脚本同时覆盖版本、带权最短路、连通分量、PageRank 四条主链路,跑通说明核心和算法目录都可用。

# smoke_test.py —— 安装后的最小体检 import networkx as nx # 版本断言,防止误装新版:1.11 的 __version__ 是 '1.11' assert nx.__version__.startswith("1.11"), "版本不对:%s" % nx.__version__ G = nx.Graph() # 三元组写法在 1.x 和 3.x 下语义一致,适合做兼容性冒烟 G.add_weighted_edges_from([("a", "b", 2.0), ("b", "c", 3.0), ("a", "c", 10.0)]) print(nx.shortest_path(G, "a", "c", weight="weight")) # 期望 ['a', 'b', 'c'] print(list(nx.connected_components(G))) # 期望 [{...}],注意 list() 包一层 print(round(nx.pagerank(G)["b"], 4)) # 期望一个 0 到 1 之间的小数

第一个断言用startswith而不是==,因为打包时版本串可能带1.11之外的细节,前缀匹配更稳。add_weighted_edges_from收的是(u, v, w)三元组,内部会自动生成weight属性,与后面的shortest_path(weight="weight")对应上。connected_components在 1.x 返回的是生成器,直接print只能看到一个对象地址,所以外面套list()。PageRank 那行取单个节点的分值,主要是验证算法目录能正常导入并完成一次迭代。

如果环境里装了 nose,1.x 还提供import networkx; networkx.test()的测试入口,会跑一遍自带用例,耗时会比冒烟脚本长得多,我只在怀疑源码被改过时才走这条。日常体检跑上面十几行就够了。

3. 建图这一步就定生死:四种图类型怎么选、属性怎么挂

3.1 Graph、DiGraph、MultiGraph、MultiDiGraph 的选择边界

选型错误在后面做算法时会以报错或数值异常的形式冒出来,而且很难回溯到建图那一步。判断依据就三个问题:关系有没有方向、同一对节点之间会不会有多条边、要不要记录自环。

类型有向平行边自环典型场景单边内存开销
Graph否否允许无向好友关系、物理链路最低
DiGraph是否允许调用链、关注关系、单向路由约 1.3 倍
MultiGraph否是允许多次通话记录、多设备互连较高
MultiDiGraph是是允许带方向的多重交互流水最高
import networkx as nx G = nx.Graph() # 无向、单边 D = nx.DiGraph() # 有向 M = nx.MultiGraph() # 无向、允许平行边 G.add_edge("u1", "u2", weight=3) D.add_edge("u1", "u2", weight=3) D.add_edge("u2", "u1", weight=5) # 有向图里这是两条不同的边 M.add_edge("u1", "u2", weight=3) M.add_edge("u1", "u2", weight=7) # 同一对节点,两条边都留着 print(len(G.edges()), len(D.edges()), len(M.edges())) # 1 2 2

关键差异在边数统计上:Graph里对同一对节点重复add_edge只会覆盖属性,节点对本身还是那一条边;MultiGraph每次add_edge都新增一条,访问时要按G[u][v][key]方式拿具体那条。如果业务数据是“同一对人之间有多笔交易”,用Graph就会静默丢数据——这是我在实际项目里见过次数最多的建图错误。

3.2 节点和边的属性:1.11 里是 dict-of-dict

1.x 的属性存储用的是嵌套字典,G.node和G.edge是两个直接暴露出来的字典属性。理解这一点,后面看代码时就不会被G.node[x]和G.nodes()的区别绕进去。

G = nx.Graph() G.add_node("u1", role="seed", score=0.9) G.add_edge("u1", "u2", weight=3, channel="sms") print(G.node["u1"]["role"]) # 1.x 写法:G.node 是 dict-of-dict print(G.edge["u1"]["u2"]["weight"]) # G.edge 同理 print(G["u1"]["u2"]["weight"]) # 下标写法在 1.x 和 2.x 下都能用 G.node["u1"]["score"] = 0.95 # 原地修改,不会触发任何校验 G["u1"]["u2"]["weight"] = 5

节点 ID 只要是 hashable 就行,字符串、整数、元组都可以,但不要在同一张图里混用类型,否则排序和输出时会出现TypeError: '<' not supported between instances of 'str' and 'int'。属性值建议保持简单类型,序列化到 GML/GraphML 时会方便很多,嵌套结构在 1.x 的写入器里支持有限。

3.3 从 CSV 和邻接矩阵批量数据导入

真实数据通常以边表形式存在,用csv.DictReader逐行读取边,比一次性读取整个文件更节省内存。

import csv import networkx as nx G = nx.Graph() with open("edges.csv", newline="", encoding="utf-8") as f: reader = csv.DictReader(f) # 表头至少包含 source, target, weight for row in reader: # 逐行加边:Graph 下重复节点对只更新属性,不会新增边 G.add_edge(row["source"], row["target"], weight=float(row["weight"])) print(G.number_of_nodes(), G.number_of_edges())

如果手头是邻接矩阵,用from_numpy_matrix一次性转换更省事,矩阵的行列下标会直接变成节点编号。

import numpy as np import networkx as nx A = np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]]) G = nx.from_numpy_matrix(A) # 节点 0/1/2,非零元素即边 print(sorted(G.nodes())) B = nx.to_numpy_matrix(G) # 反向转换,节点顺序按 G.nodes() 排 print(np.asarray(B))

to_numpy_matrix这个函数名在 1.x 和 2.x 早期都能用,后面版本改成了to_numpy_array,迁移时优先用前者,不然这套代码在新环境里又要改一遍。另外,反向转换出来的矩阵节点顺序是按G.nodes()的迭代顺序来的,不是按编号排序,做矩阵对比前务必先对节点做一次显式排序。

3.4 子图返回的是视图,不是副本

G.subgraph(nodes)在 1.x 里返回的是一个视图对象,它共享原图的属性字典。改视图里的属性,原图跟着变,这个行为在调试时经常让人误判“数据被谁改了”。

H = G.subgraph(["u1", "u2"]) # 视图,底层仍指向 G 的属性字典 H.node["u1"]["score"] = 0.1 # 原图 G 里也一起变 K = G.subgraph(["u1", "u2"]).copy() # 显式复制,彻底切开 K.node["u1"]["score"] = 0.99 # 不再影响 G

稳妥做法是:只要子图会被后续算法或落盘逻辑修改,就在subgraph后面补一个.copy()。代价是内存翻倍,所以对超大图做连通分量拆解时,我会用生成器逐个处理、处理完立刻释放,而不是一次性把所有子图都复制出来。

4. 四个常用算法跑通:最短路、连通分量、中心性、PageRank

4.1 最短路:weight 参数没传,结果按跳数算

这是最容易出错的一个点。shortest_path不传weight时按跳数比较,传了才按权重比较,两者结果可能完全不同,而且不报错。

import networkx as nx G = nx.Graph() G.add_edge("a", "b", weight=2.0) G.add_edge("b", "c", weight=3.0) G.add_edge("a", "c", weight=10.0) print(nx.shortest_path(G, "a", "c")) # 不传 weight:['a', 'c'],1 跳 print(nx.shortest_path(G, "a", "c", weight="weight")) # 带权:['a', 'b', 'c'],总权 5 dist, path = nx.single_source_dijkstra(G, "a", target="c", weight="weight") print(dist, path) # 5.0 ['a', 'b', 'c']

weight的值是属性名,不是数值。如果边属性叫cost,就传weight="cost"。如果传了weight="weight"但某条边没有这个属性,Dijkstra 会把它当默认值 1 处理,不会报错——这个静默默认值很容易让结果偏离预期,建图阶段最好对每条边显式赋值。

函数返回适用场景复杂度
nx.shortest_path节点列表单对或单源路径恢复一次 Dijkstra
nx.single_source_dijkstra(距离 dict, 路径 dict)单源、需要距离O(m + n log n)
nx.all_pairs_shortest_path_length生成器(node, dict)小图全对距离O(n·m)
nx.negative_edge_cyclebool带权图前置检查O(n·m)

有向图上跑最短路前,先确认方向是从源指向汇。DiGraph里D.has_edge("u2", "u1")为 False 时,从u2出发根本走不到u1,但算法不会提示你方向反了,只会抛NetworkXNoPath。我一般会在异常分支里单独打一行日志,把源、汇和方向一起输出。

4.2 连通分量:1.x 返回生成器,1.x 还有个被删掉的便捷函数

无向图的连通分量用connected_components,注意返回的是生成器,要立刻转成列表或迭代消费,否则后续再遍历时已经是空的。

comps = list(nx.connected_components(G)) # 必须 list() 包一层 largest = max(comps, key=len) sub = G.subgraph(largest).copy() # 视图转副本,后面随便改 print(nx.number_connected_components(G), len(largest)) # 1.x 专有的便捷入口:直接返回子图对象列表 subs = list(nx.connected_component_subgraphs(G))

connected_component_subgraphs在 1.x 里很常用,因为它一步到位给了子图对象,省掉subgraph().copy()。但这个函数在后来的版本里被移除了,依赖它的旧代码升级时会直接报AttributeError。有向图不能用connected_components,否则抛NetworkXNotImplemented,该用strongly_connected_components或weakly_connected_components,两者的语义差别是“双向可达”和“忽略方向可达”,选错会让社区发现结果完全变样。

4.3 中心性:归一化和权重方向是两个独立开关

中心性函数大多有normalized和weight两个参数,默认值不同,混用会导致不同指标之间没法横向比较。

import networkx as nx G = nx.Graph() G.add_weighted_edges_from([("a","b",1.0), ("b","c",2.0), ("a","c",5.0), ("c","d",1.0)]) deg = nx.degree_centrality(G) # 已归一化 btw = nx.betweenness_centrality(G, normalized=True, weight=None) # 按跳数算 clo = nx.closeness_centrality(G, distance="weight") # 注意参数名是 distance print(sorted(deg.items(), key=lambda kv: -kv[1])) print(sorted(btw.items(), key=lambda kv: -kv[1]))

betweenness_centrality的weight默认None,意思是按跳数算;传属性名则按权重算,但要注意权重在这里被当作“距离”使用,值越大表示越远。closeness_centrality的参数名是distance而不是weight,这是 1.x 的一个历史设计,写错参数名不会报错,只是被忽略。覆盖全部节点对的介数中心性复杂度是 O(n·m),节点上万时我会把k设成几百做近似采样,误差在可接受范围内,耗时能降一个数量级。

4.4 PageRank:不收敛的报错长什么样

PageRank 在 1.x 里叫pagerank,四个参数决定结果:阻尼系数alpha、最大迭代max_iter、收敛阈值tol、权重属性weight。

pr = nx.pagerank(G, alpha=0.85, max_iter=100, tol=1e-06, weight="weight") print(sorted(pr.items(), key=lambda kv: -kv[1])[:3])
参数默认作用调整建议
alpha0.85阻尼系数,随机跳转概率一般不动,调到 0.9 以上收敛变慢
max_iter100幂迭代上限大图调到 500 到 1000
tol1e-06两次迭代的 L1 差阈值别小于 1e-10,否则容易不收敛
weightweight边权属性名无权图传None

不收敛时的报错是NetworkXError: pagerank: power iteration failed to converge in 100 iterations.,原因通常有三个:图里有大量悬挂节点(出度为 0)导致概率质量流失、tol设得过小、alpha接近 1。常见做法是先跑一遍nx.is_strongly_connected(G)(有向图)或者看最大连通分量占比,把不连通的部分单独处理,再调max_iter和tol。

4.5 出图先固定布局,再保存文件

服务器上没有显示器,matplotlib 默认后端可能卡住,所以运行前先切到 Agg 后端。

import random import matplotlib matplotlib.use("Agg") # 无显示器环境必须 import matplotlib.pyplot as plt import networkx as nx random.seed(42) # 1.x 的 layout 没有 seed 参数,靠全局随机种子 pos = nx.spring_layout(G, iterations=50) nx.draw(G, pos, with_labels=True, node_size=400, font_size=9) labels = {(u, v): d.get("weight") for u, v, d in G.edges(data=True)} nx.draw_networkx_edge_labels(G, pos, edge_labels=labels) plt.savefig("graph.png", dpi=150, bbox_inches="tight")

spring_layout的iterations越大布局越稳定,但耗时线性增长,50 到 100 是常用区间。edge_labels的键必须是(u, v)元组,用列表推导从G.edges(data=True)构造最省事。1.x 的布局函数没有seed参数,可复现性靠random.seed()在调用前设置,这一点和新版不同,从新版回退到 1.x 的代码经常在这处报TypeError: spring_layout() got an unexpected keyword argument 'seed'。

5. 避坑与排查:五个让人头疼的坑

5.1 坑一:报 AttributeError,装的是新版跑的是 1.x 代码

现象:AttributeError: module 'networkx' has no attribute 'connected_component_subgraphs',或者graphviz_layout找不到。原因:环境里的 networkx 是被其他依赖带上去的新版,代码却按 1.x 写。解决:在该项目的虚拟环境里锁死版本,并在程序入口加断言。

import networkx as nx assert nx.__version__.startswith("1."), "需要 networkx 1.x,当前 %s" % nx.__version__

这句断言放在__init__.py或启动脚本最前面,比让错误在业务深处抛出来好排查得多。多人协作时把版本号写进requirements.txt并附注释说明“此项目依赖 1.x 的 API 语义”,能省掉很多来回沟通。

5.2 坑二:set_node_attributes 参数顺序翻转,数据被静默写坏

现象:同一句nx.set_node_attributes(G, "score", {"a": 0.9})在 1.x 下正常,在新版下节点属性里出现一堆奇怪的键名,或者属性全空。原因:1.x 的签名是(G, name, values),新版改成了(G, values, name),两者都能执行,不报错,只是把字典当成了属性名。解决:不要跨版本复用这行代码,按版本分支封装。

def set_node_attrs(G, name, values): if nx.__version__.startswith("1."): nx.set_node_attributes(G, name, values) # 1.x:(G, name, values) else: nx.set_node_attributes(G, values, name) # 新版:(G, values, name)

这类“改了参数顺序但不改函数名”的变更比删函数更危险,因为删函数至少会立刻报错,顺序翻转是静默污染数据,往往要等下游指标异常才被发现。

5.3 坑三:G.node 和 G.nodes 的索引方式完全不同

现象:G.nodes["a"]报TypeError: list indices must be integers,或者G.node["a"]报AttributeError: 'Graph' object has no attribute 'node'。原因:1.x 里G.node是 dict-of-dict,G.nodes()是返回节点列表的方法;新版把G.nodes做成了视图对象,支持下标但不支持G.node这种单数写法。解决:跨版本代码统一用G["a"]拿邻接字典,或者统一走G.node并用版本断言保护。

def node_attrs(G, n): # 1.x 用 G.node;新版用 G.nodes。统一返回一个 dict store = G.node if nx.__version__.startswith("1.") else G.nodes return store[n]

排查时先print(type(G))和print(nx.__version__),两条信息一起看,基本能立刻定位。

5.4 坑四:read_gml 读中文标签直接报编码错

现象:UnicodeDecodeError,或者读出来的标签是一串乱码。原因:1.x 的read_gml默认用 latin-1 解码,而实际文件是 UTF-8 存的。解决:读写两侧都显式指定encoding="utf-8",并且对非 ASCII 标签做一次映射。

G = nx.read_gml("graph.gml", encoding="utf-8") nx.write_gml(G, "graph_out.gml", encoding="utf-8")

如果仍然乱码,问题多半出在写入端:有些第三方工具生成的 GML 会把中文塞进label字段但不声明编码。稳妥做法是给节点用一个纯 ASCII 的 id,中文名放在单独的属性里,落盘时同时输出一份 id 到中文名的映射表,之后无论换哪个版本读写都不会出问题。

5.5 坑五:邻接矩阵 todense() 把内存打爆

现象:对十万节点的图执行nx.adjacency_matrix(G).todense(),进程直接被系统杀掉。原因:adjacency_matrix返回的是 scipy 稀疏矩阵,.todense()会转成 n×n 的稠密 float64 数组,十万节点就是 10^10 个元素、约 80 GB。解决:全程保持稀疏,需要乘向量时直接做稀疏点积。

A = nx.adjacency_matrix(G) # scipy sparse,内存与边数成正比 x = np.ones(A.shape[0]) y = A.dot(x) # 稀疏矩阵乘向量,不展开稠密 # 只有在节点数几千以内、确实需要稠密运算时才展开 if G.number_of_nodes() < 5000: dense = A.todense()

判断标准很简单:节点数乘节点数乘 8 字节,超过物理内存三分之一就不要todense()。这条经验同样适用于拉普拉斯矩阵和任何to_*_matrix系列的返回值。

6. 让 1.11 和新版在一台机器上共存:薄封装加落盘校验

生产环境里往往不能“只留一个版本”,因为其他服务还在用新版。我通常的做法是:把 1.11 装到独立目录,只在需要它的进程里把路径插到sys.path最前面,再用一层薄封装抹平 API 差异。

python -m pip install --no-deps --target=/opt/vendor/nx111 /opt/src/networkx-1.11.zip python -m pip install --target=/opt/vendor/nx111 decorator
# compat_nx.py —— 1.x 与新版的兼容层 import sys VENDOR = "/opt/vendor/nx111" if VENDOR not in sys.path: sys.path.insert(0, VENDOR) # 插到最前面,保证优先命中 1.11 import networkx as nx IS_1X = nx.__version__.startswith("1.") def set_node_attrs(G, name, values): if IS_1X: nx.set_node_attributes(G, name, values) else: nx.set_node_attributes(G, values, name) def component_subgraphs(G): if IS_1X: return list(nx.connected_component_subgraphs(G)) return [G.subgraph(c).copy() for c in nx.connected_components(G)] def roundtrip_check(G, path): nx.write_edgelist(G, path, data=True) H = nx.read_edgelist(path, data=True) assert set(G.nodes()) == set(H.nodes()), "节点集合读写不一致" assert G.number_of_edges() == H.number_of_edges(), "边数读写不一致" return True

sys.path.insert(0, ...)的顺序很关键,用append的话,如果系统里已经装了新版,先命中的还是新版,封装层就直接失效了。roundtrip_check是我加在每次落盘后的固定动作:写完边表立刻读回来,比对节点集合和边数。字符串类型的节点经过write_edgelist再读回来一般不会变,但如果混用了整数和字符串 ID,读写后类型可能统一成字符串,这个断言能立刻发现。

注意:边表格式不保存节点属性和孤立节点,如果图里有大量无边的节点,roundtrip_check会报节点集合不一致。这种场景改用 GML 或 GraphML 落盘,并在断言前把孤立节点单独记录一份。

从那以后我每次碰老版本依赖,都强制走一遍“版本断言、冒烟脚本、落盘回读”这三步,先在环境上花十分钟把问题暴露出来,而不是等业务跑了一半再回头查数据被谁改了。这套流程也帮我把几个跨版本项目从“只能在一台机器上跑”变成可复现的部署。希望帮到你。

本文还有配套的精品资源,点击获取