Neo4j是一款开源的图数据库,简单易学,而py2neo是python中可以与Neo4j对接的库,两者的介绍参见:Neo4j简介及Py2Neo的用法
这篇博客文章对py2neo的使用介绍也很详尽,但是是v3版本,v4版本还是有些更新,所以在这里记录一下py2neo v4的用法,主要参考py2neo v4 handbook
安装
pip install py2neo
或者从github源码安装
pip install git+https://github.com/technige/py2neo.git#egg=py2neo
1.数据类型-py2neo.data
1.1节点和关系
创建节点和关系
from py2neo.data import Node, Relationship a = Node(“Person”, name=”Alice”) b = Node(“Person”, name=”Bob”) ab = Relationship(a, “KNOWS”, b) ab (Alice)-[:KNOWS]->(Bob)
上述创建的节点是未绑定的(unbound),也就是只存在于python这个客户端(client)中,并不存在于neo4j的服务器端(server),确实,我们neo4j都还没有安装启动呢。
在neo4j中创建的节点才是绑定的(bound)节点,在neo4j中创建的节点又叫做远程节点(remote node)。
下面这两个语句返回True or Flase,用来判断两个节点是否相等,判断的标准是节点的ID,而不是节点的标签、属性之类的。只有两个节点绑定到同一个远程节点,才会被判定为相等。
nodeA == nodeB nodeA != nodeB
关于节点属性的一些操作如下:
node[key] = value #给节点属性赋值 del node[key] #删除节点属性 len(node) #节点属性的个数 dict(node) #返回字典,包括了该节点的所有属性
关于节点标签的一些操作如下:
node.labels #返回节点的所有标签 labelA in node.labels #如果节点具有标签labelA,返回True node.labels.add(labelB) #给节点增加标签labelB node.labels.discard(label_C) #删除节点标签labelC node.labels.remove(_labelC) #同上,但是如果labelC不存在的话会返回ValueError node.labels.clear() #清除节点所有标签 node.labels.update(manylabels) #从可迭代对象manylabels中给节点增加多个标签
文档中没有提到的一点是,node.identity
可以返回节点的id,这点和v3的操作也是不一样的。
节点终于讲完了,还记得最开始我们还创建了一个关系吧?
ab = Relationship(a, “KNOWS”, b)
实际上,关系的创建有多种方式,上面是一种常见的方式,a、b分别是起始节点和终止节点,“KNOWS”是关系的类型(type),如果不写默认是“TO”;关系也可以从某个节点指向它自己,也就是下面的第三、第四种方式:
Relationship(start_node,type,end_node,**properties) Relationship(start_node,end_node,**properties) Relationship(node,type,**properties) Relationship(node,**properties)
此外,还可以给关系赋予属性(properties)
ab[‘time’] = ‘2019/04/01’
关系的一些其他操作如下
relationshipA == relationshipB relationshipA != relationshipB #判断两个关系是否相等,但是和节点不同,这里只要起始、终止节点和关系类型相同,就判定两个关系相等 del relationship[key] #删除属性 len(relationship) #返回属性个数 dict(relationship) #返回字典,包括所有属性 type(relationship) #返回关系的类型
1.2子图(subgraph)
子图,就是一个集合,可以包括节点、关系和walkable对象(后面会介绍)。
下面是一个创建子图的例子
s = ab | ac s {(alice:Person {name:”Alice”}), (bob:Person {name:”Bob”}), (carol:Person {name:”Carol”}), (Alice)-[:KNOWS]->(Bob), (Alice)-[:WORKS_WITH]->(Carol)} s.nodes() frozenset({(alice:Person {name:”Alice”}), (bob:Person {name:”Bob”}), (carol:Person {name:”Carol”})}) s.relationships() frozenset({(Alice)-[:KNOWS]->(Bob), (Alice)-[:WORKS_WITH]->(Carol)})
子图作为一个集合,就可以进行python的set对象的一些操作:
subgraphA | subgraphB | … #并集(Union) subgraphA & subgraphB & … #交集(Intersection) subgraphA - subgraphB - … #差集(Difference) subgraphA ^ subgraphB ^ … #对等差分(Symmetric difference),只在A或B中存在的元素集合,不包括同时存在于A和B中的元素;也就是并集和交集的差集。。。
另外还可以进行一些图数据库相关的操作
subgraph.keys() subgraph.labels subgraph.nodes subgraph.relationships subgraph.types()
注意:subgraph对象查询节点和关系的操作,在v3中是s.nodes()和s.relationships()
结果会返回subgraph中所有的节点或关系,是一个frozenset对象,用print()函数可打印出所有内容
而v4中是s.nodes和s.relationships(对,没有括号,不是callable对象)
用print结果只返回
把setview对象转换为list就可以自由调用了,也就是list(s.nodes)或list(s.relationships)
1.3 walkable对象
Walkable对象是一种Subgraph对象,但是附加了遍历信息,用walk() 函数就可以遍历walkable对象,返回节点-关系-节点-关系…-节点的信息流,总是从节点出发,到节点结束,中间是节点关系交替出现。
下面是代码示例,用“+”可以连接起图对象,构建walkable对象
w = ab + Relationship(b, “LIKES”, c) + ac w (Alice)-[:KNOWS]->(Bob)-[:LIKES]->(Carol)<-[:WORKS_WITH]-(Alice)
用walk()遍历上面构建的walkable对象w
walk(w)
一些其他操作如下
w.start_node w.end_node w.nodes w.relationships
还有一个path对象,也是一种walkable对象,它接受cypher查询语句,返回walkable的查询结果
1.4 Record对象
record对象是一个有序的键值对,有些类似namedtuple(根据廖雪峰的python教程:namedtuple具有tuple的不变性,又可以通过属性来引用),它同时扩展了tuple和mapping对象(python中mapping对象也就是dict)。
record[index] #根据序号引用 record[key] #根据属性引用 len(record) #返回属性数量 dict(record) #返回record对象的字典形式 data(*keys/indexes) #输入多个属性或序号,返回record中相应的键值对,返回结果为dict get(key/index, default=None) #输入一个属性或序号,返回record中相应的值 index(key) #返回给定属性对应的序号 items(*keys/indexes) #输入多个属性或序号,返回一个list,其中的元素是(key,value)形式的元组(tuple) keys() #返回keys的list values(*keys/indexes) #输入多个属性或序号,返回对应的值的list to_subgraph() #返回subgraph对象,它是record中所有图结构(节点、关系等)的并集
1.5 Table对象
table对象是一系列record对象的列表(list),通常来自cypher语句的查询结果,它提供了一些方法将结果转换为不同的输出格式。
repr(table) #将table转换为string(ASCII形式) repr_html() #将table转换为string(HTML形式),这个方法由Jupyter notebooks用来在浏览器中显示table keys() #返回keys的list field(key) #返回dict,包括key对应value的type,numeric(是否数值型),optional(是否有None)
最后这个field(key)暂时不太理解有什么用。。。
下面这些都是以特定格式写入文件
write(file=None, header=None, skip=None, limit=None, auto_align=True, padding=1, separator=’|’, newline=’\r\n’) writehtml(_file=None, header=None, skip=None, limit=None, auto_align=True) writeseparated_values(_separator, file=None, header=None, skip=None, limit=None, newline=’\r\n’, quote=’”‘) writecsv(_file=None, header=None, skip=None, limit=None) writetsv(_file=None, header=None, skip=None, limit=None)
2. Graph Databases-py2neo.database
接下来终于要正式进入正题了,之前我们还只是在python里面敲敲打打,现在需要在Neo4j里面创建数据库了
首先当然需要安装Neo4j,社区版就已经足够使用了,安装过程不再赘述
运行Neo4j之后,我们就可以在python里和它建立连接,连接方式有三种,也就是Neo4j支持的三种网络协议,Bolt,HTTP和HTTPS,分别使用端口7687,7474和7473(有的可能在windows防火墙中阻止了这几个端口,需要允许通过这几个端口)
文档中给出的代码示例都是通过Bolt连接,我一般习惯用HTTP连接
from py2neo import Graph graph = Graph(“http://localhost:7474/db/data/“) graph.run(“一串Cypher语句”).to_table()
上述代码中创建了graph对象,然后用graph.run就可以运行Cypher语句,就可以鼓捣很多事情了
我们还是来仔细看看文档中都说了些啥:
py2neo.database中包括的class和function都是用来和Neo4j交互的,其中最重要的就是Graph class,它表示一个Neo4j的图数据库实例,并提供到大多数常用的py2neo API的连接。下面先从Database开始:
2.1 Database
Database通过Bolt或HTTP与整个的Noe4j图数据库连接。Database中包括Graph,目前Neo4j只支持一个Database中有一个Graph
突然想到那么Neo4j是否可以有多个Database呢?答案是可以的,但是在Database之间进行切换比较麻烦,具体参见:Neo4j多数库切换
Database的创建:
from py2neo import Database db=Database() #使用默认参数bolt://localhost:7687,同下面这行代码 db=Database(“bolt://localhost:7687”)
Database的一些方法就暂时略过了,基本都是一些配置层面的参数和设置,目前应该还接触不到
2.2 Graph
Graph的创建(我理解为和Neo4j中的graph建立连接),以下三种方式都是等同的:
from py2neo import Graph graph_1=Graph() graph_2=Graph(host=”localhost”) graph_3=Graph(“bolt://localhost:7687”)
创建时还可以设置一些其他参数,如user/password等
graph=Graph(user=”admin”, password=”123456”)
接下来可以将之前创建的一些节点、关系导入到Neo4j中
graph.create(node) graph.create(relationship) graph.create(subgraph)
注意,如果我们在python中对节点、关系等做了一些改动,Neo4j中的远程节点并不会相应地变化,需要用push方法更新一下
graph.push(node)
还有一种pull方法作用相反,就是在Neo4j中对节点等做了改动,python中相应节点也不会有变化,需要用pull将远程节点及相关属性“拉”回来
graph.pull(node)
2.3 Transaction
下面还是接着谈Graph,但是引入一个新的类Transaction。
Graph表示的是Neo4j中的图数据库,Transaction则是对Graph进行一系列操作的Cypher语句的容器。
跨专业不恰当类比一下,Graph就好比表示年终状态的资产负债表,是某个时间点的状态,是静态的;Transaction就是期间进行的一系列操作,比如现金流量、所有者权益变动,是动态操作。
所以,Transaction是对Graph的一系列操作的集合,Graph的很多操作也就是transaction的操作,例如以下这些:
graph.begin(autocommit=False) #开始一个新的transaction,默认是不自动执行的(操作先暂存在transaction里),如果autocommit=True则自动执行 graph.create(subgraph) graph.delete(subgraph) #该语句自动执行 graph.separate(subgraph) #删除子图中的关系 graph.evaluate(“cypher语句”)#返回第一条记录的第一个值,自动执行 graph.exists(subgraph) #判断子图是否存在,自动执行 graph.merge(subgraph, primary_label=None, primary_key=None) #这个merge和cypher的merge有所区别,它首先是在Neo4j中寻找匹配的远程node,如果没有就创建节点,如果有就更新节点;然后是匹配关系,同样创建或更新 graph.run(“cypher语句”)#自动执行cypher语句
相应地,Transaction类中也就有create()
delete() separate() evaluate() exists() merge() run()这些方法,以及之前提到的pull()
push()方法,不过它们都不是自动执行的,而是将操作先暂存在transaction中,然后用transaction.commit()执行
此外,transcation还有以下方法
transaction.finished() #判断transaction是否执行完成 transaction.rollback() #回滚,撤销当前transaction的所有操作
graph也还有一些和transaction无关的“静态”操作,例如
graph.match() graph.nodes graph.relationships
2.4 Cursor objects(游标)
Neo4j的游标和SQL的游标应该是类似的,就是一个数据缓冲区,存放Cypher语句的执行结果。它可以逐个地读取执行结果,做进一步处理。
下面的代码就可以逐个打印出记录,
while cursor.forward(): print(cursor.current[“name”])
cursor.forward()还可以设置amount参数,如cursor.forward(amount=2),每隔两个读取结果;cursor.current读取游标当前的结果。
cursor.data()会以字典形式返回所有结果,代码如下:
from py2neo import Graph graph=Graph() graph.run(“MATCH (a:Person) RETURN a.name, a.born LIMIT 4”).data() [{‘a.born’: 1964, ‘a.name’: ‘Keanu Reeves’}, {‘a.born’: 1967, ‘a.name’: ‘Carrie-Anne Moss’}, {‘a.born’: 1961, ‘a.name’: ‘Laurence Fishburne’}, {‘a.born’: 1960, ‘a.name’: ‘Hugo Weaving’}]
结果还可以转换成其他各种数据形式:
cursor.to_data_frame() #pandas的DataFrame cursor.to_matrix() #numpy的ndarray cursor.to_series() #pandas的Series cursor.to_subgraph() cursor.to_table()
3. Entity Matching(实体匹配)
3.1 Node Matching(节点匹配)
这里v4的查找节点的函数和v3不一样了,代码示例如下:
graph = Graph() matcher = NodeMatcher(graph) matcher.match(“Person”, name=”Alice”).first()
上面的例子查找了name属性为”Alice”的Person节点,这里对name属性的查找是一种精确匹配,实际上还可以进行各种各样不那么精确的查找,通过在属性名字后面加后缀的方式实现,下面的表格进行了总结:
描述 | 后缀 | 表达式 | 代码示例 | 对应的Cypher语句 |
---|---|---|---|---|
相等 | __exact | = | matcher.match(“Person”, name__exact=”Kevin Bacon”) |
MATCH (:Person) WHERE name = “Kevin Bacon” RETURN |
不等 | __not | <> | matcher.match(“Person”, name__not=”Rick Astley”) |
MATCH (:Person) WHERE .name <> “Rick Astley” RETURN _ |
大于 | __gt | > | matcher.match(“Person”, born__gt=1985) |
MATCH (:Person) WHERE .born > 1985 RETURN _ |
大于等于 | __gte | >= | matcher.match(“Person”, born__gte=1965) |
MATCH (:Person) WHERE .born >= 1965 RETURN _ |
小于 | __lt | < | matcher.match(“Person”, born__lt=1965) |
MATCH (:Person) WHERE .born < 1965 RETURN _ |
小于等于 | __lte | <= | matcher.match(“Person”, born__lte=1965) |
MATCH (:Person) WHERE .born <= 1965 RETURN _ |
以…开头 | __startswith | STARTS WITH | matcher.match(“Person”, name__startswith=”Kevin”) |
MATCH (:Person) WHERE .name STARTS WITH “Kevin” RETURN _ |
以…结尾 | __endswith | ENDS WITH | matcher.match(“Person”, name__endswith=”Smith”) |
MATCH (:Person) WHERE .name ENDS WITH “Smith” RETURN _ |
包含 | __contains | CONTAINS | matcher.match(“Person”, name__contains=”James”) |
MATCH (:Person) HWERE .name CONTAINS “James” RETURN _ |
也可以用NodeMatch.where()
方法对查找进行进一步的定义,这里下划线_
指查找的节点,~ 'K.*'
表示匹配正则表达式以K开头的字符串
list(matcher.match(“Person”).where(“_.name =~ ‘K.*’”))
还可以对查找的结果进行排序和个数限制
list(matcher.match(“Person”).where(“.name =~ ‘K.*’”).order_by(“.name”).limit(3))
len()
可以返回查找结果的个数
len(matcher.match(“Person”).where(“_.name =~ ‘K.*’”))
skip(n)
可以跳过前n个查找结果
list(matcher.match(“Person”).where(“_.name =~ ‘K.*’”).skip(3))
3.2 Relationship Matching(关系匹配)
查找关系的操作和查找节点是类似的,代码如下
from py2neo.matching import RelationshipMatcher graph = Graph() matcher=RelationshipMatcher(graph) list(matcher.match(r_type=’KNOWS’))
同样的,也可以用order_by, limit, len, skip等方法
在RelationshipMatcher.match()
的文档说明中,还有参数nodes,默认值是None,可以接受一个set作为参数,来查找某些节点(无论是起始节点还是终止节点)对应的关系
但是我做了不同的尝试,都没有成功,最后在这里找到了解决办法,原来nodes接受的set参数必须是NodeMatcher返回的形式
nodematcher=NodeMatcher(graph) findnode=nodematcher.match(‘Person’).first() list(graph.match((findnode,),r_type=’KNOWS’))
这里用.first()
只定义了单个节点,那么用set(nodematcher.match(‘Person’))做参数查找一系列节点是否可以呢?也不可以,返回的错误是”Node set cannot be larger than two”,看来只能够用来查找单个节点相关联的关系
而且上例查找的结果来看,只能找出以findnode为起点的关系
总之用上面的方法来查找某些节点相关的关系还是不太方便,不如直接用graph.run(“Cypher语句”)好了
4. Object-Graph Mapping(OGM)
OGM类似于ORM(Object Relational Mapping),后者是对象与关系数据库之间的映射,neo4j是图数据库,对应地,OGM就是对象和图数据库之间的映射。
示例代码如下:
from py2neo.ogm import GraphObject,Property,RelatedFrom,RelatedTo class Movie(GraphObject): primarykey = “title”
title = Property()
tag_line = Property("tagline")
released = Property()
actors = RelatedFrom("Person", "ACTED_IN")
directors = RelatedFrom("Person", "DIRECTED")
producers = RelatedFrom("Person", "PRODUCED")
class Person(GraphObject): primarykey = “name”
name = Property()
born = Property()
acted_in = RelatedTo(Movie)
directed = RelatedTo(Movie)
produced = RelatedTo(Movie)
这里定义了Movie和Person对象,它们扩展了GraphObject对象,具有属性(Property)、标签(Label)、与之相关联的关系对象(Related Objects)。然后我们就可以对这些对象创建实例、修改属性等等,而不用频繁地直接去访问数据库。
下面依次介绍GraphObject、Property、Label、Related Objects和对象的查找、操作。
4.1 GraphObject
GraphObject是py2neo OGM框架的核心,所有要映射到图数据库的对象都是基于它创建的。每一个GraphObject可以看做是对一个节点及其相关联的关系的封装,
GraphObject的实例就像其他的python类的实例一样创建,同时也可以和图数据库中的节点进行匹配,每个实例包含的属性(attributes)包括节点的标签、属性,还有与这个节点相关联的关系对象。
这些属性里有几个很重要的:__primarylabel__
,节点的标签,也就是类的名字__primarykey__
,主键,具有唯一性,如果不定义,默认是“id”__primaryvalue__
,主键的值__node__
,所封装的节点
用GraphObject.wrap(node)
方法可以将node封装为GraphObject对象,GraphObject.match(graph,primary_value=None)
可以从graph中查找节点
实验了一下,如果类名和目标节点的标签名不一致,是查找不到节点的:
a = Node(“People”, name=”Alice”) #创建了一个节点,标签为People
classNode=Person.wrap(a) #将节点封装为Person类 graph.push(classNode) #封装后的Person类更新到图数据库中(图数据库中显示的标签仍为People) list(Person.match(graph)) #在图数据库中查找Person类,结果查找不到
4.2. Properties
在前面的示例代码中,已经有了Property的设置,例如name=Property()
,表示Person类的节点具有name属性。其实Property()
还可以传入两个参数:Property(key=None,default=None)
,key是数据库中节点属性的名称,default是创建对象实例时,该属性的默认值。
例如数据库中存储的节点属性都是中文的,python程序中我想都用英文变量名,可以将中文的属性名称赋给一个英文变量,代码如下:
class Person(GraphObject): primarykey = None
name = Property(key="姓名")
born = Property(key="出生年月")
4.3. Labels
文档中的这个Label很让人困惑,开始还以为是节点的标签,结果只是接受布尔值的Property,而且按文档示例代码运行还报错:name ‘Label’ is not defined
这一小节就跳过吧,暂时没看出意义所在
4.4. Related Objects
前面的示例代码中,也已经有了对Related Object的定义,
例一:acted_in=RelatedTo(Movie)
,表示Person类具有指向Movie类的关系,并将与之关联的Movie节点命名为“acted_in”;
例二:actors = RelatedFrom("Person", "ACTED_IN")
则表示Movie类具有来自Person类的关系“ACTED_IN”,并将与之关联的Person节点命名为actors。
无论是RelatedTo()
还是RelatedFrom()
,返回的都是Related Objects,其实也就是GraphObject的实例。所以acted_in=RelatedTo(Movie)
中尽管是用“acted_in”命名,看起来是关系的名称,但它返回的还是GraphObject(这里是Movie)。
那么例一中我们是不是可以随便用个其他的变量名来代替“acted_in”呢?不可以,随便取个变量名,怎么知道匹配数据库中的哪种关系呢。但是如果在RelatedTo()
中指定了rel_type那就可以了,和例二中actors = RelatedFrom("Person", "ACTED_IN")
的用法是一致的。
还有一个问题,例一和例二中,类名(Movie/Person)一个有引号,一个没引号,测试了一下,两种用法都是可以的。
4.5. Object Matching
查找单个节点:
Person.match(graph, “Alice”).first()
查找多个节点:
list(Person.match(graph).where(“_.name =~ ‘A.*’”))
4.6. Object Operations
和之前介绍的其他对象一样,GraphObject也可以进行push/pull/create/merge/delete操作,区别是GraphObject不用首先create/merge,直接可以push/pull
alice = Person() alice.name = “Alice Smith” graph.push(alice)
文档到这里基本就要结束了,最后两小节是关于Cypher语句的处理,个人觉得没有太大用处(本质就是字符串的处理,有很多方法可以实现),例如cypher_escape()
可以动态地构建Cypher语句:
“MATCH (a:{label}) RETURN id(a)”.format(label=cypher_escape(“Employee of the Month”))
用字符串格式化方法也是可以很方便实现的。
好了,py2neo v4文档就全部撸完了,有些小细节实际使用过程中再慢慢领会吧。