当遇到展示小规模图谱时,使用 NextworkxGraphML 是一种性价比很高的解决方案。

GraphML 是通用的图数据交换格式,负责图数据的存储和跨工具共享。
NetworkX 是 Python 中一个强大的 网络(图)分析与操作库,它可以处理 GraphML ,还提供丰富的图算法。

本文使用 Nextworkx 实现查询 GraphML 图数据的功能。

  • 查询根节点,支持分页
  • 查询某个节点的子图结构:即它的子节点以及子节点对应的边和下一级节点

缓存数据文件

缓存图文件以后,在用户反复操作这张图的时候就不必每次从磁盘加载文件,响应速度会快得多。

from cachetools import LRUCache

file_cache = LRUCache(maxsize=100)

def get_graph(file_id:str):
    """获取图数据"""
    if file_id in file_cache:
        content = file_cache[file_id]
    else:
        fp = os.path.join(data_dir,f"{file_id}.graphml")
        if not os.path.isfile(fp):
            fp = os.path.join(data_dir,"demo.graphml")
        content = nx.read_graphml(fp)
        file_cache[file_id] = content
    return content

获取根节点

支持对根节点分页。

def build_echarts_data(nodes=[], edges=[],categories=set()):
    result = {
        "nodes": nodes,
        "links":edges,
        "categories": categories
    }

    return result

def get_node_info(G,node_id:str):
    node_data = G.nodes[node_id]
    return {
        "id": node_id,
        "name": node_data.get("id", node_id),
        "category": node_data.get("type", "Unknown")
    }

def get_root(file_id:str,page=1, limit=20):
    '''获取根节点和对应的边'''
    G = get_graph(file_id)

    # 确保是有向图
    if not G.is_directed():
        G = G.to_directed()

    # 找到所有无入边的顶层节点
    top_nodes = [n for n in G.nodes if G.in_degree(n) == 0]

    if not top_nodes:
        return {"data":build_echarts_data(),"total_page":0}
    
    # 分页
    total = len(top_nodes)
    total_page = math.ceil(total/limit)
    start_idx = (page - 1) * limit
    if start_idx >= total:
        return {"data":build_echarts_data(),"total_page":total_page}
    end_idx = start_idx + limit    
    if total >= end_idx:
        top_nodes_page = top_nodes[start_idx:end_idx]
    else:
        top_nodes_page = top_nodes[start_idx:]

    # print("顶层节点(无入边):", top_nodes_page)
        
    result_nodes = []
    result_links = []    
    categories_set = set()  # 类别集合

    for node_id in top_nodes_page:
        n = get_node_info(G,node_id)
        result_nodes.append(n)
        categories_set.add(n['category'])

    return {"data":build_echarts_data(result_nodes,result_links,categories_set),"total_page":total_page}

从上面代码可以看到:返回的数据结构适合 echart 展示。

获取子节点的图结构

下面是获取子节点的图结构,有了这个方法后,前端可以不断的点击节点,钻取到子图:

def get_child(file_id:str,node_id:str):
    """查询子节点"""
    G = get_graph(file_id)

    if node_id not in G.nodes:
        return build_echarts_data()     

    # 用于输出的结构
    result_nodes = []
    result_links = []    
    categories_set = set()  # 类别集合

    n = get_node_info(G,node_id)
    result_nodes.append(n)
    categories_set.add(n['category'])

    # 遍历一级边
    for neighbor in G.neighbors(node_id):
        edge_data = G.get_edge_data(node_id, neighbor)
        edge_label = edge_data.get("label", "") if edge_data else ""            
        result_links.append({
            "source": node_id,
            "target": neighbor,
            "label": edge_label
        })

        # 加入二级节点
        if neighbor not in result_nodes:
            n = get_node_info(G,neighbor)
            result_nodes.append(n)
            categories_set.add(n['category'])

    return build_echarts_data(result_nodes,result_links,categories_set)

见证结果

我们使用下面的代码查询样例 GraphML :

if __name__ == '__main__':

    r = get_root("123")
    print(r)
    r = get_child(file_id="123",node_id="1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795")
    print(r)
{'data': {'nodes': [{'id': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'name': '权利人', 'category': 'entity'}], 'links': [], 'categories': {'entity'}}, 'total_page': 1}
{'nodes': [{'id': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'name': '权利人', 'category': 'entity'}, {'id': 'e713a31ba70e5e1f1070bca95f410de7ea9c8b77066ed72f795c800ead454340', 'name': '同一主体', 'category': 'entity'}, {'id': 'c343f9fd7f6c38d7a5013150e80b222bb0f6dda67dfc1e5748427fa845caf940', 'name': '技术秘密披露行为', 'category': 'entity'}, {'id': '008bbf0c6b06cbcff0d6f31670ada377dc105ff571b5c520840e09f00a5a2d6b', 'name': '针对披露技术秘密行为的重复诉讼认定 技术秘密的披露是一过性行为,权利人已就同一主体向另一主体披露同一技术秘密信息的行为提起诉讼,又在诉讼过程中或者裁判生效后再次就此提起诉讼的,构成重复诉讼。', 'category': 'passage'}, {'id': 'ac7935720ad3024330245310779b877e50329951702f815f056336c4a1c47ea5', 'name': '权利拥有者', 'category': 'concept'}, {'id': 'e60ac25a5279de83d0a198ff8b64773c717f25cd4221188aff4c20cdf318bcef', 'name': '法律主体', 'category': 'concept'}, {'id': '883ae0b3ae1bbab0e8dec8fda36bfc4c2aafc8afd01db3441d6e86eb07e93309', 'name': '权益
持有者', 'category': 'concept'}], 'links': [{'source': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'target': 'e713a31ba70e5e1f1070bca95f410de7ea9c8b77066ed72f795c800ead454340', 'label': ''}, {'source': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'target': 'c343f9fd7f6c38d7a5013150e80b222bb0f6dda67dfc1e5748427fa845caf940', 'label': ''}, {'source': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'target': '008bbf0c6b06cbcff0d6f31670ada377dc105ff571b5c520840e09f00a5a2d6b', 'label': ''}, {'source': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'target': 'ac7935720ad3024330245310779b877e50329951702f815f056336c4a1c47ea5', 'label': ''}, {'source': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'target': 'e60ac25a5279de83d0a198ff8b64773c717f25cd4221188aff4c20cdf318bcef', 'label': ''}, {'source': '1822ba17b297d5f6a4e5e3497c08a639925cef62b4518c01c04d30b694030795', 'target': '883ae0b3ae1bbab0e8dec8fda36bfc4c2aafc8afd01db3441d6e86eb07e93309', 'label': ''}], 'categories': {'passage', 'concept', 'entity'}}

总结

从上述代码可以发现:使用 NextworkxGraphML 是一种非常渐变的处理图数据的轻量级解决方案。


代码

本文涉及的所有代码以及相关资源都已经共享,参见:

为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。

🪐祝好运🪐