乐趣区

关于python:使用Py2neo查询Neo4j中的节点关系和路径

Neo4j 是一款开源图数据库,Py2neo 提供了应用 Python 语言拜访 Neo4j 的接口。本文介绍了应用 Py2neo 的 NodeMatcher 和 RelationshipMatcher 查问图中的节点和关系,以及通过执行 Cypher 语句的查问形式。

​本文应用的 Py2neo 是 2021.1 之后的版本,手册请戳这里:
The Py2neo Handbook

连贯 Neo4j 数据库

本文中会用到多种数据类型,在此一并援用

import numpy as np
import pandas as pd
from py2neo import Node,Relationship,Graph,Path,Subgraph
from py2neo import NodeMatcher,RelationshipMatcher

配置 Neo4j 数据库的拜访地址、用户名和明码

neo4j_url = 'http://localhost:7474/'
user = 'neo4j'
pwd = 'admin'

2021.1 之前拜访数据库的形式为:

graph = Graph(neo4j_url, username=user, password=pwd)

2021.1 之后的版本拜访数据库的形式为:

graph = Graph(neo4j_url,  auth=(user, pwd))

以下图为例:

  • 图中蕴含一些 Person 节点,每个 Person 节点有 name、age、work 属性;
  • 其中“赵赵”节点是多 label 的节点,除了有 Person 标签,它还有 Teacher 标签;
  • Person 和 Person 节点之间有共事、街坊、学生、老师等关系;
  • 图中还有一些 Location 节点,它们之间有蕴含关系;
  • Person 节点和 Location 节点之间有“到访”关系,“到访”关系具备 date 和 stay_hours 两个属性。

1. 通过 graph.schema 查问图中节点和关系有哪些类型

查看节点的类型用graph.schema.node_labels,查看关系的类型用graph.schema.relationship_types,它们的返回值类型都是 frozenset,是不能增删元素的汇合。

>>>graph.schema.node_labels 
frozenset({'Location', 'Person', 'Teacher'})
>>>graph.schema.relationship_types
frozenset({'到访', '蕴含', '共事', '学生', '老师', '街坊'})

2. 应用 NodeMatcher 查问节点

首先创立一个 NodeMatcher 对象,用 match 来指明要匹配哪种 label 的节点,用 where 来示意筛选条件(有两种办法)。须要留神的是,匹配胜利返回的是 NodeMatcher 的对象,要转化成 Node 对象,能够用 first 取出符合条件的第一个节点,或者转化成节点的 list。

>>>node_matcher = NodeMatcher(graph)
>>>node = node_matcher.match("Person").where(age=20).first()
>>>node
Node('Person', age=20, name='李李', work='宇宙电子厂')
>>>nodes = list(node_matcher.match("Person").where(age=35))
>>>nodes
[Node('Person', age=35, name='王王', work='宇宙电子厂')]

where 条件有两种写法,一种是把要匹配的属性和值写成 key=value 的模式,例如下面的 where(age=20),这种写法只能依照值是否完全一致来匹配,不能依照值的大小来筛选,如果写成上面这样是会报错的:

node = node_matcher.match("Person").where(age>20).first() # 谬误

想要依照值的大小筛选或者做一些字符串的含糊匹配,能够把条件表达式写成一个字符串,整体放在 where 语句中,在这个字符串中,能够用 _ 来代指匹配到的节点。上面两个例子,第一个是匹配 work 属性为“月亮 XX”模式的 Person 节点,另一个是匹配 age 大于 20 的 Person 节点。

>>>node = node_matcher.match("Person").where("_.work =~' 月亮.*'").first()
>>>node
Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学')
>>>nodes = list(node_matcher.match("Person").where("_.age > 20"))
>>>nodes
[Node('Person', age=35, name='王王', work='宇宙电子厂'),
 Node('Person', age=30, name='张张', work='宇宙电子厂'),
 Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学')]

将 NodeMatcher 返回的后果转化为 Node 数据类型或者 Node 的 list 之后,拜访其中的属性也就非常简略了,如下面最初一例的后果,拜访其中第一个节点的 name 属性:

>>>nodes[0]['name']
'王王'

3. 应用 RelationshipMatcher 查问关系

RelationshipMatcher 的 match 办法有三个及以上参数:

  • 第一个参数是节点的序列或者 set,能够为 None,为 None 示意任意节点均可;
  • 第二个参数是关系的类型,能够为 None,为 None 示意任意类型的关系均可;
  • 第三个参数开始是要匹配的属性,写成 key=value 的模式。

match 办法的返回值是 RelationshipMatcher 类型,须要通过 first 转化成 Relationship 数据结构,或者转化为 list。
举例说明,比方想要查问“李李”节点的所有关系。先查问出节点,再查问节点的关系,r_type=None 示意任意类型的关系均可。返回的关系包含到访、共事。

>>>node1 = node_matcher.match("Person").where(name='李李').first()
>>>relationship = list(relationship_matcher.match([node1], r_type=None))
>>>relationship
[到访(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Location', name='禄口机场'), date='2021/7/16', stay_hours=1),
 共事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=30, name='张张', work='宇宙电子厂')),
 共事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=35, name='王王', work='宇宙电子厂'))]

例 2,查问“李李”和“张张”的关系,两个节点的程序示意了要匹配的关系的方向。所以在整个图中“李李”和“张张”节点之间的共事关系是双向的,然而查问后果只给出了从“张张”节点到“李李”节点的一条关系。

>>>node1 = node_matcher.match("Person").where(name='李李').first()
>>>node2 = node_matcher.match("Person").where(name='张张').first()
>>>relationship = list(relationship_matcher.match((node2,node1), r_type=None))
>>>relationship
[共事(Node('Person', age=30, name='张张', work='宇宙电子厂'), Node('Person', age=20, name='李李', work='宇宙电子厂'))]

例 3,查问图中某一类关系,第一个参数为 None,第二个参数 r_type 指定关系类型,这里查问了图中所有的共事关系。

>>>relationship = list(relationship_matcher.match(None, r_type='共事'))
>>>relationship
[共事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=30, name='张张', work='宇宙电子厂')),
 共事(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Person', age=35, name='王王', work='宇宙电子厂')),
 共事(Node('Person', age=35, name='王王', work='宇宙电子厂'), Node('Person', age=20, name='李李', work='宇宙电子厂')),
 共事(Node('Person', age=30, name='张张', work='宇宙电子厂'), Node('Person', age=20, name='李李', work='宇宙电子厂'))]

例 4,在查问关系时依照属性的值筛选,能够将该属性写为 key=value 的模式作为 match 办法的第三个参数。这里,查问图中的到访关系,并且 stay_hours 属性为 1。

>>>relationship = list(relationship_matcher.match(None, r_type='到访', stay_hours=1))
>>>relationship
[到访(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Location', name='禄口机场'), date='2021/7/16', stay_hours=1)]

尽管 Py2neo 的手册上没有写,但其实 RelationshipMatcher 也能够接上 where 办法,依照属性的值筛选关系。下面这个例子也能够写作上面这种模式,成果是一样的。

relationship = list(relationship_matcher.match(None, r_type='到访').where(stay_hours=1))

同样,在 where 办法中也能够写一个字符串表达式,实现按值大小来筛选关系。例如要筛选出所有到访关系,且 stay_hours>= 1 的关系时,能够这样写:

>>>relationship = list(relationship_matcher.match(None, r_type='到访').where("_.stay_hours>=1"))
>>>relationship
[到访(Node('Person', age=20, name='李李', work='宇宙电子厂'), Node('Location', name='禄口机场'), date='2021/7/16', stay_hours=1),
 到访(Node('Person', age=20, name='刘刘', work='地球电子商务公司'), Node('Location', name='禄口机场'), date='2021/7/19', stay_hours=4)]

如何拜访返回的后果中的各个属性呢,Relationship 其实是蕴含了一对起止节点:start_nodeend_node,蕴含了关系的类型,而关系的属性是以 字典 模式存在的,能够用 get 办法来获取属性的值。
获取关系的起止节点:

>>>print(relationship[0].start_node['name'])
>>>print(relationship[0].end_node['name'])
李李
禄口机场

获取关系的类型的文本字符串

>>>print(relationship[0])
>>>print(type(relationship[0]).__name__)
(李李)-[: 到访 {date: '2021/7/16', stay_hours: 1}]->(禄口机场)
到访

获取关系中的属性和值

>>>print(relationship[0].keys())
>>>print(relationship[0].values())
>>>print(relationship[0].get('date'))
dict_keys(['date', 'stay_hours'])
dict_values(['2021/7/16', 1])
2021/7/16

4. 通过执行 Cypher 语句查问

NodeMatcher 和 RelationshipMatcher 可能表白的匹配条件绝对简略,更加简单的查问还是须要用 Cypher 语句来表白。Py2neo 自身反对执行 Cypher 语句的执行,能够将简单的查问写成 Cypher 语句,通过 graph.run 办法查问,返回的后果能够转化为 pandas.DataFrame 或者 pandas.Series 对象,从而和其余数据分析工具无缝连接。

例如,要查问 Person 节点,并满足 work 属性为“宇宙电子厂”。Cypher 语句中能够应用 WHERE 接条件表达式,应用 AS 将返回的属性改名,返回多个属性时,用 xxx AS x, yyy AS y。graph.run 办法之后再接 to_data_frame() 能够将返回的数据变成 pandas 的 DataFrame 对象,并且 用 AS 改过的属性名即为 DataFrame 中的列名

cypher_ = "MATCH (n:Person) \
WHERE n.work='宇宙电子厂' \
RETURN n.name AS name, n.age AS age "

df = graph.run(cypher_).to_data_frame() # pd.DataFrame


例 2,查问一个已知节点和其余哪些节点有关系,有什么样的关系。Cypher 语言查问关系时用 < 或者 > 示意方向,这里须要返回type(r),间接返回 r 的话后果里是空值。

>>>cypher_ = "MATCH (n:Person)-[r]->(m:Person) \
WHERE n.name='李李' \
RETURN type(r) AS type,m.name AS name"
>>>df = graph.run(cypher_).to_data_frame() # pd.DataFrame


例 3,Cypher 语言还能够查问门路,因为不确定返回的门路数量,所以最好先将后果转化为 pandas.Series,再遍历拜访其中每条门路的节点和关系。
这里查问的是“赵赵”节点和“王王”节点之间的关系门路,关系指定为共事或街坊,关系不超过 4 层。

>>>cypher_ = "MATCH path=(m:Person)-[: 共事 | 街坊 *1..4]->(n:Person) \
WHERE m.name='赵赵' AND n.name='王王' \
RETURN path"
>>>s = graph.run(cypher_).to_series()
>>>print(len(s))
>>>s[0]
1
Path(Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学'),
街坊(Node('Person', 'Teacher', age=45, name='赵赵', work='月亮中学'), 
Node('Person', age=30, name='张张', work='宇宙电子厂')), 
共事(Node('Person', age=30, name='张张', work='宇宙电子厂'), 
Node('Person', age=20, name='李李', work='宇宙电子厂')), 
共事(Node('Person', age=20, name='李李', work='宇宙电子厂'), 
Node('Person', age=35, name='王王', work='宇宙电子厂')))

这里查问到的关系门路数量仅有 1 条。从上图的后果中也能够看进去,Path 是一个比较复杂的构造,Path 中的节点和关系别离用 nodesrelationships示意,并且是依照门路上节点和关系的 程序 别离寄存的。这里给出一段示例代码,对每一个门路都做了间接打印 path 数据结构和本人组织门路文本。

for path in s:
    # 间接打印 path
    print(path)
    # 获取门路中的节点和关系
    nodes = path.nodes
    relationshis = path.relationships   
    # 本人组织门路文本
    path_text = ""
    for n,r in zip(nodes, relationshis):
        # 每次退出一个节点和一个关系的类型
        path_text += "{} - {} -".format(n['name'], type(r).__name__)
    # 别忘了最初一个节点
    path_text += nodes[-1]['name'] + '\n'
    print(path_text)

运行这段代码得的后果如下所示,下面一行是间接打印门路的后果,上面一行是本人组织文本失去的后果。

(赵赵)-[: 街坊 {}]->(张张)-[: 共事 {}]->(李李)-[: 共事 {}]->(王王)
赵赵 - 街坊 - 张张 - 共事 - 李李 - 共事 - 王王

小结

应用 Py2neo 查问 Neo4j 中的节点、关系和门路时,条件简略的查问能够通 NodeMatcher 和 RelationshipMatcher 来实现。而较为简单的查问,能够写成 Cypher 语句来查问,查问后果能够转化为 pandas 的 DataFrame 或者 Series 数据类型,与其余数据分析工具联合。


我的 Python 版本

>>> import sys
>>> print(sys.version)
3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)]
退出移动版