示例:储存图数据

在构建地图应用、设计电路图、进行任务调度、分析网络流量等多种任务中,都需要对图(graph)数据结构实施建模,并储存相关的图数据。对于不少数据库来说,想要高效直观地储存图数据并不是一件容易的事情,但是 Redis 却能够以多种不同的方式表示图数据结构,其中一种方法就是使用散列。


图 3-20 简单的带权重有向图_images/IMAGE_GRAPH_EXAMPLE.png


图 3-21 图对应的散列键_images/IMAGE_GRAPH_HASH.png


比如说,假设我们想要储存图 3-20 所示的带权重有向图,那么可以创建一个图 3-21 所示的散列键,这个散列键会以 start_vertex->end_vertex 的形式,将各个顶点之间的边储存到散列的字段里面,并将字段的值设置成边的权重。通过这种方法,我们可以将图的所有相关数据全部储存到散列里面,代码清单 3-5 展示了使用这种方法实现的图数据储存程序。


代码清单 3-5 使用散列实现的图数据储存程序:/hash/graph.py

  1. def make_edge_name_from_vertexs(start, end):
  2. """
  3. 使用边的起点和终点组建边的名字。
  4. 例子:对于 start 为 "a" 、 end 为 "b" 的输入,这个函数将返回 "a->b" 。
  5. """
  6. return str(start) + "->" + str(end)
  7.  
  8. def decompose_vertexs_from_edge_name(name):
  9. """
  10. 从边的名字中分解出边的起点和终点。
  11. 例子:对于输入 "a->b" ,这个函数将返回结果 ["a", "b"] 。
  12. """
  13. return name.split("->")
  14.  
  15.  
  16. class Graph:
  17.  
  18. def __init__(self, client, key):
  19. self.client = client
  20. self.key = key
  21.  
  22. def add_edge(self, start, end, weight):
  23. """
  24. 添加一条从顶点 start 连接至顶点 end 的边,并将边的权重设置为 weight 。
  25. """
  26. edge = make_edge_name_from_vertexs(start, end)
  27. self.client.hset(self.key, edge, weight)
  28.  
  29. def remove_edge(self, start, end):
  30. """
  31. 移除从顶点 start 连接至顶点 end 的一条边。
  32. 这个方法在成功删除边时返回 True ,
  33. 因为边不存在而导致删除失败时返回 False 。
  34. """
  35. edge = make_edge_name_from_vertexs(start, end)
  36. return self.client.hdel(self.key, edge)
  37.  
  38. def get_edge_weight(self, start, end):
  39. """
  40. 获取从顶点 start 连接至顶点 end 的边的权重,
  41. 如果给定的边不存在,那么返回 None 。
  42. """
  43. edge = make_edge_name_from_vertexs(start, end)
  44. return self.client.hget(self.key, edge)
  45.  
  46. def has_edge(self, start, end):
  47. """
  48. 检查顶点 start 和顶点 end 之间是否有边,
  49. 是的话返回 True ,否则返回 False 。
  50. """
  51. edge = make_edge_name_from_vertexs(start, end)
  52. return self.client.hexists(self.key, edge)
  53.  
  54. def add_multi_edges(self, *tuples):
  55. """
  56. 一次向图中添加多条边。
  57. 这个方法接受任意多个格式为 (start, end, weight) 的三元组作为参数。
  58. """
  59. # redis-py 客户端的 hmset() 方法接受一个字典作为参数
  60. # 格式为 {field1: value1, field2: value2, ...}
  61. # 为了一次对图中的多条边进行设置
  62. # 我们要将待设置的各条边以及它们的权重储存在以下字典
  63. nodes_and_weights = {}
  64.  
  65. # 遍历输入的每个三元组,从中取出边的起点、终点和权重
  66. for start, end, weight in tuples:
  67. # 根据边的起点和终点,创建出边的名字
  68. edge = make_edge_name_from_vertexs(start, end)
  69. # 使用边的名字作为字段,边的权重作为值,把边及其权重储存到字典里面
  70. nodes_and_weights[edge] = weight
  71.  
  72. # 根据字典中储存的字段和值,对散列进行设置
  73. self.client.hmset(self.key, nodes_and_weights)
  74.  
  75. def get_multi_edge_weights(self, *tuples):
  76. """
  77. 一次获取多条边的权重。
  78. 这个方法接受任意多个格式为 (start, end) 的二元组作为参数,
  79. 然后返回一个列表作为结果,列表中依次储存着每条输入边的权重。
  80. """
  81. # hmget() 方法接受一个格式为 [field1, field2, ...] 的列表作为参数
  82. # 为了一次获取图中多条边的权重
  83. # 我们需要把所有想要获取权重的边的名字依次放入到以下列表里面
  84. edge_list = []
  85.  
  86. # 遍历输入的每个二元组,从中获取边的起点和终点
  87. for start, end in tuples:
  88. # 根据边的起点和终点,创建出边的名字
  89. edge = make_edge_name_from_vertexs(start, end)
  90. # 把边的名字放入到列表中
  91. edge_list.append(edge)
  92.  
  93. # 根据列表中储存的每条边的名字,从散列里面获取它们的权重
  94. return self.client.hmget(self.key, edge_list)
  95.  
  96. def get_all_edges(self):
  97. """
  98. 以集合形式返回整个图包含的所有边,
  99. 集合包含的每个元素都是一个 (start, end) 格式的二元组。
  100. """
  101. # hkeys() 方法将返回一个列表,列表中包含多条边的名字
  102. # 例如 ["a->b", "b->c", "c->d"]
  103. edges = self.client.hkeys(self.key)
  104.  
  105. # 创建一个集合,用于储存二元组格式的边
  106. result = set()
  107. # 遍历每条边的名字
  108. for edge in edges:
  109. # 根据边的名字,分解出边的起点和终点
  110. start, end = decompose_vertexs_from_edge_name(edge)
  111. # 使用起点和终点组成一个二元组,然后把它放入到结果集合里面
  112. result.add((start, end))
  113.  
  114. return result
  115.  
  116. def get_all_edges_with_weight(self):
  117. """
  118. 以集合形式返回整个图包含的所有边,以及这些边的权重。
  119. 集合包含的每个元素都是一个 (start, end, weight) 格式的三元组。
  120. """
  121. # hgetall() 方法将返回一个包含边和权重的字典作为结果
  122. # 格式为 {edge1: weight1, edge2: weight2, ...}
  123. edges_and_weights = self.client.hgetall(self.key)
  124.  
  125. # 创建一个集合,用于储存三元组格式的边和权重
  126. result = set()
  127. # 遍历字典中的每个元素,获取边以及它的权重
  128. for edge, weight in edges_and_weights.items():
  129. # 根据边的名字,分解出边的起点和终点
  130. start, end = decompose_vertexs_from_edge_name(edge)
  131. # 使用起点、终点和权重构建一个三元组,然后把它添加到结果集合里面
  132. result.add((start, end, weight))
  133.  
  134. return result

这个图数据储存程序的核心概念就是把边(edge)的起点和终点组合成一个字段名,并把边的权重(weight)用作字段的值,然后使用 HSET 命令或者 HMSET 命令把它们储存到散列里面。比如说,如果用户输入的边起点为 "a" ,终点为 "b" ,权重为 "30" ,那么程序将执行命令 HSET hash "a->b" 30 ,把 "a""b" 的这条边及其权重 30 储存到散列里面。

在此之后,程序就可以使用 HDEL 命令去删除图的某条边,使用 HGET 命令或者 HMGET 命令去获取边的权重,使用 HEXISTS 命令去检查边是否存在,又或者使用 HKEYS 命令和 HGETALL 命令去获取图的所有边以及权重。

比如说,我们可以通过执行以下代码,构建出前面展示过的带权重有向图 3-20 :

  1. >>> from redis import Redis
  2. >>> from graph import Graph
  3. >>>
  4. >>> client = Redis(decode_responses=True)
  5. >>> graph = Graph(client, "test-graph")
  6. >>>
  7. >>> graph.add_edge("a", "b", 30) # 添加边
  8. >>> graph.add_edge("c", "b", 25)
  9. >>> graph.add_multi_edges(("b", "d", 70), ("d", "e", 19)) # 添加多条边

然后通过执行程序提供的方法,获取边的权重,又或者检查给定的边是否存在:

  1. >>> graph.get_edge_weight("a", "b") # 获取边 a->b 的权重
  2. '30'
  3. >>> graph.has_edge("a", "b") # 边 a->b 存在
  4. True
  5. >>> graph.has_edge("b", "a") # 边 b->a 不存在
  6. False

最后,我们还可以获取图的所有边以及它们的权重:

  1. >>> graph.get_all_edges() # 获取所有边
  2. {('b', 'd'), ('d', 'e'), ('a', 'b'), ('c', 'b')}
  3. >>>
  4. >>> graph.get_all_edges_with_weight() # 获取所有边以及它们的权重
  5. {('c', 'b', '25'), ('a', 'b', '30'), ('d', 'e', '19'), ('b', 'd', '70')}

这里展示的图数据储存程序提供了针对边和权重的功能,因为它能够非常方便地向图中添加边和移除边,并且还可以快速地检查某条边是否存在,所以它非常适合用来储存节点较多但边较少的稀疏图(sparse graph)。在后续的章节中,我们还会继续看到更多使用 Redis 储存图数据的例子。