本文實(shí)例講述了Python使用py2neo操作圖數(shù)據(jù)庫neo4j的方法。分享給大家供大家參考,具體如下:
1、概念
圖:數(shù)據(jù)結(jié)構(gòu)中的圖由節(jié)點(diǎn)和其之間的邊組成。節(jié)點(diǎn)表示一個(gè)實(shí)體,邊表示實(shí)體之間的聯(lián)系。
圖數(shù)據(jù)庫:以圖的結(jié)構(gòu)存儲(chǔ)管理數(shù)據(jù)的數(shù)據(jù)庫。其中一些數(shù)據(jù)庫將原生的圖結(jié)構(gòu)經(jīng)過優(yōu)化后直接存儲(chǔ),即原生圖存儲(chǔ)。還有一些圖數(shù)據(jù)庫將圖數(shù)據(jù)序列化后保存到關(guān)系型或其他數(shù)據(jù)庫中。
之所以使用圖數(shù)據(jù)庫存儲(chǔ)數(shù)據(jù)是因?yàn)樗谔幚韺?shí)體之間存在復(fù)雜關(guān)系的數(shù)據(jù)具有很大的優(yōu)勢。使用傳統(tǒng)的關(guān)系型數(shù)據(jù)庫在處理數(shù)據(jù)之間的關(guān)系時(shí)其實(shí)很不方便。例如查詢選修一個(gè)課程的同學(xué)時(shí)需要join兩個(gè)表,查詢選修某個(gè)課程的同學(xué)還選修什么課程,這就需要兩次join操作,當(dāng)涉及到十分復(fù)雜的關(guān)系以及龐大的數(shù)據(jù)量時(shí),關(guān)系型數(shù)據(jù)庫效率十分低下。而通過圖存儲(chǔ),可以通過節(jié)點(diǎn)之間的邊十分便捷地查詢到結(jié)果。
圖模型:
節(jié)點(diǎn)(Node)是主要的數(shù)據(jù)元素,表示一個(gè)實(shí)體。
屬性(Properties)用于描述實(shí)體的特征,以鍵值對的方式表示,其中鍵是字符串,可以對屬性創(chuàng)建索引和約束。
關(guān)系(Relationships)表示實(shí)體之間的聯(lián)系,關(guān)系具有方向,實(shí)體之間可以有多個(gè)關(guān)系,關(guān)系也可以具有屬性
標(biāo)簽(Label)用于將實(shí)體分類,一個(gè)實(shí)體可以具有多個(gè)標(biāo)簽,對標(biāo)簽進(jìn)行索引可以加速查找
2、Neo4j
Neo4j是目前最流行的圖數(shù)據(jù)庫,它采用原生圖存儲(chǔ),在windows中下載安裝訪問如下地址https://neo4j.com/download/community-edition/。在Linux下通過如下命令下載解壓
1
2
|
curl -O http: //dist .neo4j.org /neo4j-community-3 .4.5-unix. tar .gz tar -axvf neo4j-community-3.4.5-unix. tar .gz |
修改配置文件conf/neo4j.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# 修改第22行l(wèi)oad csv時(shí)l路徑,在前面加個(gè)#,可從任意路徑讀取文件 #dbms.directories.import=import # 修改35行和36行,設(shè)置JVM初始堆內(nèi)存和JVM最大堆內(nèi)存 # 生產(chǎn)環(huán)境給的JVM最大堆內(nèi)存越大越好,但是要小于機(jī)器的物理內(nèi)存 dbms.memory.heap.initial_size=5g dbms.memory.heap.max_size=10g # 修改46行,可以認(rèn)為這個(gè)是緩存,如果機(jī)器配置高,這個(gè)越大越好 dbms.memory.pagecache.size=10g # 修改54行,去掉改行的#,可以遠(yuǎn)程通過ip訪問neo4j數(shù)據(jù)庫 dbms.connectors.default_listen_address=0.0.0.0 # 默認(rèn) bolt端口是7687,http端口是7474,https關(guān)口是7473,不修改下面3項(xiàng)也可以 # 修改71行,去掉#,設(shè)置http端口為7687,端口可以自定義,只要不和其他端口沖突就行 #dbms.connector.bolt.listen_address=:7687 # 修改75行,去掉#,設(shè)置http端口為7474,端口可以自定義,只要不和其他端口沖突就行 dbms.connector.http.listen_address=:7474 # 修改79行,去掉#,設(shè)置http端口為7473,端口可以自定義,只要不和其他端口沖突就行 dbms.connector.https.listen_address=:7473 # 去掉#,允許從遠(yuǎn)程url來load csv dbms.security.allow_csv_import_from_file_urls= true # 修改250行,去掉#,設(shè)置neo4j-shell端口,端口可以自定義,只要不和其他端口沖突就行 dbms.shell.port=1337 # 修改254行,設(shè)置neo4j可讀可寫 dbms.read_only= false |
在bin目錄下執(zhí)行 ./neo4j start,啟動(dòng)服務(wù),在瀏覽器http://服務(wù)器ip地址:7474/browser/可以看到neo4j的可視化界面
3、py2neo
py2neo是一個(gè)社區(qū)第三方庫,通過它可以更為便捷地使用python來操作neo4j
安裝py2neo:pip install py2neo
,我安裝的版本是4.3.0
3.1、Node與Relationship
創(chuàng)建節(jié)點(diǎn)和它們之間的關(guān)系,注意在使用下面的py2neo相關(guān)類之前首先需要import導(dǎo)入:
1
2
3
4
5
6
7
8
9
10
11
|
# 引入庫 from py2neo import Node, Relationship # 創(chuàng)建節(jié)點(diǎn)a、b并定義其標(biāo)簽為Person,屬性name a = Node( "Person" , name = "Alice" ,height = 166 ) b = Node( "Person" , name = "Bob" ) # 節(jié)點(diǎn)添加標(biāo)簽 a.add_label( 'Female' ) # 創(chuàng)建ab之間的關(guān)系 ab = Relationship(a, "KNOWS" , b) # 輸出節(jié)點(diǎn)之間的關(guān)系:(Alice)-[:KNOWS]->(Bob) print (ab) |
Node 和 Relationship 都繼承了 PropertyDict 類,類似于python的dictionary,可以通過如下方式對 Node 或 Relationship 進(jìn)行屬性賦值和訪問
1
2
3
4
5
6
7
8
9
10
11
12
|
# 節(jié)點(diǎn)和關(guān)系添加、修改屬性 a[ 'age' ] = 20 ab[ 'time' ] = '2019/09/03' # 刪除屬性 del a[ 'age' ] # 打印屬性 print (a[name]) # 設(shè)置默認(rèn)屬性,如果沒有賦值,使用默認(rèn)值,否則設(shè)置的新值覆蓋默認(rèn)值 a.setdefault( 'sex' , 'unknown' ) # 更新屬性 a.update(age = 22 , sex = 'female' ) ab.update(time = '2019/09/03' ) |
3.2、Subgraph
由節(jié)點(diǎn)和關(guān)系組成的集合就是子圖,通過關(guān)系運(yùn)算符求交集&、并集|、差集-、對稱差集^
subgraph.labels返回子圖中所有標(biāo)簽集合,keys()返回所有屬性集合,nodes返回所有節(jié)點(diǎn)集,relationships返回所有關(guān)系集
1
2
3
4
5
|
# 構(gòu)建一個(gè)子圖 s = a | b | ab # 對圖中的所有節(jié)點(diǎn)集合進(jìn)行遍歷 for item in s.nodes: print ( 's的節(jié)點(diǎn):' , item) |
通常將圖中的所有節(jié)點(diǎn)和關(guān)系構(gòu)成一個(gè)子圖后再統(tǒng)一寫入數(shù)據(jù)庫,與多次寫入單個(gè)節(jié)點(diǎn)相比效率更高
1
2
3
4
5
|
# 連接neo4j數(shù)據(jù)庫,輸入地址、用戶名、密碼 graph = Graph( 'http://localhost:7474' , username = 'neo4j' , password = '123456' ) # 將節(jié)點(diǎn)和關(guān)系通過關(guān)系運(yùn)算符合并為一個(gè)子圖,再寫入數(shù)據(jù)庫 s = a | b | ab graph.create(s) |
3.3、Walkable
walkable是在子圖subgraph的基礎(chǔ)上增加了遍歷信息的對象,通過它可以便捷地遍歷圖數(shù)據(jù)庫。
通過+號將關(guān)系連接起來就構(gòu)成了一個(gè)walkable對象。通過walk()函數(shù)對其進(jìn)行遍歷,可以利用 start_node、end_node、nodes、relationships屬性來獲取起始 Node、終止 Node、所有 Node 和 Relationship
1
2
3
4
5
6
7
8
9
10
|
# 組合成一個(gè)walkable對象w w = ab + bc + ac # 對w進(jìn)行遍歷 for item in walk(w): print (item) # 訪問w的初始、終止節(jié)點(diǎn) print ( '起始節(jié)點(diǎn):' , w.start_node, ' 終止節(jié)點(diǎn):' , w.end_node) # 訪問w的所有節(jié)點(diǎn)、關(guān)系列表 print ( '節(jié)點(diǎn)列表:' , w.nodes) print ( '關(guān)系列表:' , w.relationships) |
運(yùn)行結(jié)果為:
(:Person {age: 20, name: 'Bob'})
(Bob)-[:KNOWS {}]->(Alice)
(:Person {age: 21, name: 'Alice'})
(Alice)-[:LIKES {}]->(Mike)
(:Person {name: 'Mike'})
(Bob)-[:KNOWS {}]->(Mike)
(:Person {age: 20, name: 'Bob'})
起始節(jié)點(diǎn): (:Person {age: 22, name: 'Bob', sex: 'female'}) 終止節(jié)點(diǎn): (:Person {age: 22, name: 'Bob', sex: 'female'})
節(jié)點(diǎn)列表: ((:Person {age: 22, name: 'Bob', sex: 'female'}), (:Person {age: 21, name: 'Alice'}), (:Person {name: 'Mike'}), (:Person {age: 22, name: 'Bob', sex: 'female'}))
關(guān)系列表: ((Bob)-[:KNOWS {time: '2019/09/03'}]->(Alice), (Alice)-[:LIKES {}]->(Mike), (Bob)-[:KNOWS {}]->(Mike))
3.4、Graph
py2neo通過graph對象操作neo4j數(shù)據(jù)庫,目前的neo4j只支持一個(gè)數(shù)據(jù)庫定義一張圖
通過Graph的初始化函數(shù)完成對數(shù)據(jù)庫的連接并創(chuàng)建一個(gè)graph對象
graph.create()可以將子圖寫入數(shù)據(jù)庫,也可以一次只寫入一個(gè)節(jié)點(diǎn)或關(guān)系
graph.delete()刪除指定子圖,graph.delete_all()刪除所有子圖
graph.seperate()刪除指定關(guān)系
1
2
3
4
5
6
7
8
9
10
|
# 初始化連接neo4j數(shù)據(jù)庫,參數(shù)依次為url、用戶名、密碼 graph = Graph( 'http://localhost:7474' , username = 'neo4j' , password = '123456' ) # 寫入子圖w graph.create(w) # 刪除子圖w graph.delete(w) # 刪除所有圖 graph.delete_all() # 刪除關(guān)系rel graph.separate(rel) |
graph.match(nodes=None, r_type=None, limit=None)查找符合條件的關(guān)系,第一個(gè)參數(shù)為節(jié)點(diǎn)集合或者集合(起始節(jié)點(diǎn),終止節(jié)點(diǎn)),如果省略代表所有節(jié)點(diǎn)。第二個(gè)參數(shù)為關(guān)系的屬性,第三個(gè)為返回結(jié)果的數(shù)量。也可以使用match_one()代替,返回一條結(jié)果。例如查找所有節(jié)點(diǎn)a認(rèn)識的人:
1
2
3
4
5
|
# 查找所有以a為起點(diǎn),并且屬性為KNOWS的關(guān)系 res = graph.match((a, ), r_type = "KNOWS" ) # 打印關(guān)系的終止節(jié)點(diǎn),即為a所有認(rèn)識的人 for rel in res: print (rel.end_node[ "name" ]) |
使用graph.nodes.match()查找指定節(jié)點(diǎn),可以使用first()、where()、order_by()等函數(shù)對查找做高級限制
還可以通過節(jié)點(diǎn)或關(guān)系的id查找
1
2
3
4
5
6
7
8
9
10
|
# 查找標(biāo)簽為Person,屬性name="Alice"的節(jié)點(diǎn),并返回第一個(gè)結(jié)果 graph.nodes.match( "Person" , name = "Alice" ).first() # 查找所有標(biāo)簽為Person,name以B開頭的節(jié)點(diǎn),并將結(jié)果按照age字段排序 res = graph.nodes.match( "Person" ).where( "_.name =~ 'B.*'" ).order_by( '_.age' ) for node in res: print (node[ 'name' ]) # 查找id為4的節(jié)點(diǎn) t_node = graph.nodes[ 4 ] # 查找id為196的關(guān)系 rel = graph.relationships[ 196 ] |
通過Graph對象進(jìn)行Cypher操作并處理返回結(jié)果
graph.evaluate()執(zhí)行一個(gè)Cypher語句并返回結(jié)果的第一條數(shù)據(jù)
1
2
3
4
|
# 執(zhí)行Cypher語句并返回結(jié)果集的第一條數(shù)據(jù) res = graph.evaluate( 'MATCH (p:Person) return p' ) # 輸出:(_3:Person {age: 20, name: 'Bob'}) print (res) |
graph.run()執(zhí)行Cypher語句并返回結(jié)果數(shù)據(jù)流的游標(biāo)Cursor,通過forward()方法不斷向前移動(dòng)游標(biāo)可以向前切換結(jié)果集的每條記錄Record對象
1
2
3
4
5
6
7
8
|
# 查詢(p1)-[k]->(p2),并返回所有節(jié)點(diǎn)和關(guān)系 gql = "MATCH (p1:Person)-[k:KNOWS]->(p2:Person) RETURN *" cursor = graph.run(gql) # 循環(huán)向前移動(dòng)游標(biāo) while cursor.forward(): # 獲取并打印當(dāng)前的結(jié)果集 record = cursor.current print (record) |
打印的每條Record記錄對象如下所示,可以看到其中的元素是key=value的集合,通過方法get(key)可以取出具體元素。通過方法items(keys)可以將記錄中指定的key按(key,value)元組的形式返回
1
2
3
4
5
6
7
8
9
10
11
|
<Record k = (xiaowang) - [:KNOWS {}] - >(xiaozhang) p1 = (_96:Person {name: 'xiaowang' }) p2 = (_97:Person {name: 'xiaozhang' })> record = cursor.current print ( '通過get返回:' , record.get( 'k' )) for (key, value) in record.items( 'p1' , 'p2' ): print ( '通過items返回元組:' , key, ':' , value) # 運(yùn)行結(jié)果如下 ''' 通過get返回: (xiaowang)-[:KNOWS {}]->(xiaozhang) 通過items返回元組: p1 : (_92:Person {name: 'xiaowang'}) 通過items返回元組: p2 : (_93:Person {name: 'xiaozhang'}) ''' |
還可以將graph.run()返回的結(jié)果通過data()方法轉(zhuǎn)化為字典列表,所有結(jié)果整體上是一個(gè)列表,其中每一條結(jié)果是字典的格式,其查詢與結(jié)果如下,可以通過訪問列表與字典的方式獲取數(shù)據(jù):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# 查詢(p1)-[k]->(p2),并返回所有節(jié)點(diǎn)和關(guān)系 gql = "MATCH (p1:Person)-[k:KNOWS]->(p2:Person) RETURN *" res = graph.run(gql).data() print (res) #結(jié)果如下: ''' [{'k': (xiaowang)-[:KNOWS {}]->(xiaozhang), 'p1': (_196:Person {name: 'xiaowang'}), 'p2': (_197:Person {name: 'xiaozhang'})}, {'k': (xiaozhang)-[:KNOWS {}]->(xiaozhao), 'p1': (_197:Person {name: 'xiaozhang'}), 'p2': (_198:Person {name: 'xiaozhao'})}, {'k': (xiaozhao)-[:KNOWS {}]->(xiaoli), 'p1': (_198:Person {name: 'xiaozhao'}), 'p2': (_199:Person {name: 'xiaoli'})} ] ''' |
通過graph.run().to_subgraph()方法將返回的結(jié)果轉(zhuǎn)化為SubGraph對象,接著按之前操作SubGraph對象的方法取得節(jié)點(diǎn)對象,這里的節(jié)點(diǎn)對象Node可以直接按照之前的Node操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# 查詢(p1)-[k]->(p2),并返回所有節(jié)點(diǎn)和關(guān)系 gql = "MATCH (p1:Person)-[k:KNOWS]->(p2:Person) RETURN *" sub_graph = graph.run(gql).to_subgraph() # 獲取子圖中所有節(jié)點(diǎn)對象并打印 nodes = sub_graph.nodes for node in nodes: print (node) # 輸出的節(jié)點(diǎn)對象如下: ''' (_101:Person {name: 'xiaozhang'}) (_100:Person {name: 'xiaowang'}) (_103:Person {name: 'xiaoli'}) (_102:Person {name: 'xiaozhao'}) ''' |
3.5、OGM
Object-Graph Mapping將圖數(shù)據(jù)庫中的節(jié)點(diǎn)映射為python對象,通過對象的方式對節(jié)點(diǎn)進(jìn)行訪問和操作。
將圖中的每種標(biāo)簽定義為一個(gè)python類,其繼承自GraphObject,注意使用前先import。在定義時(shí)可以指定數(shù)據(jù)類的主鍵,并定義類的屬性Property()、標(biāo)簽Label()、關(guān)系RelatedTo()/RelatedFrom。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from py2neo.ogm import GraphObject, Property , RelatedTo, RelatedFrom, Label class Person(GraphObject): # 定義主鍵 __primarykey__ = 'name' # 定義類的屬性 name = Property () age = Property () # 定義類的標(biāo)簽 student = Label() # 定義Person指向的關(guān)系 knows = RelatedTo( 'Person' , 'KNOWS' ) # 定義指向Person的關(guān)系 known = RelatedFrom( 'Person' , 'KNOWN' ) |
通過類方法wrap()可以將一個(gè)普通節(jié)點(diǎn)轉(zhuǎn)化為類的對象。
類方法match(graph,primary_key)可以在graph中查找主鍵值為primary_key的節(jié)點(diǎn)
可以直接通過類構(gòu)造方法創(chuàng)建一個(gè)對象,并直接訪問對象的屬性及方法,并通過關(guān)系方法add()添加關(guān)系
類的標(biāo)簽是一個(gè)bool值,默認(rèn)為False,將其修改為True,即可為對象添加標(biāo)簽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# 將節(jié)點(diǎn)c轉(zhuǎn)化為OGM類型 c = Person.wrap(c) print (c.name) # 查找Person類中主鍵(name)為Alice的節(jié)點(diǎn) ali = Person.match(graph, 'Alice' ).first() # 創(chuàng)建一個(gè)新的Person對象并對其屬性賦值 new_person = Person() new_person.name = 'Durant' new_person.age = 28 # 標(biāo)簽值默認(rèn)為False print (new_person.student) # 修改bool值為True,為對象添加student標(biāo)簽 new_person.student = True # 將修改后的圖寫入數(shù)據(jù)庫 graph.push(ali) |
在定義節(jié)點(diǎn)類時(shí)還可以定義其相關(guān)的關(guān)系,例如通過RelatedTo()定義從該節(jié)點(diǎn)指出的關(guān)系,RelatedFrom()定義指向該節(jié)點(diǎn)的關(guān)系。通過對象調(diào)用關(guān)系的對應(yīng)的方法完成節(jié)點(diǎn)周圍的關(guān)系操作,例如add()添加關(guān)系,clear()清除節(jié)點(diǎn)所有的關(guān)系,get()獲取關(guān)系屬性,remove()清楚指定的關(guān)系,update()更新關(guān)系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Person(GraphObject): # 定義Person指向的關(guān)系 knows = RelatedTo( 'Person' , 'KNOWS' ) # 定義指向Person的關(guān)系 known = RelatedFrom( 'Person' , 'KNOWN' ) # 新建一個(gè)從ali指向new_person的關(guān)系 ali.knows.add(new_person) # 清除ali節(jié)點(diǎn)所有的know關(guān)系 ali.knows.clear() # 清除ali節(jié)點(diǎn)指向new_person的那個(gè)know關(guān)系 ali.knows.remove(new_person) # 更新ali指向new_person關(guān)系的屬性值 ali.knows.update(new_person,year = 5 ) # 獲取ali指向new_person關(guān)系的屬性year的值 ali.knows.get(new_person, 'year' ) |
通過圖對象也可以調(diào)用match方法對節(jié)點(diǎn)、關(guān)系進(jìn)行匹配
1
2
3
4
|
# 獲取第一個(gè)主鍵name名為Alice的Person對象 ali = Person.match(graph, 'Alice' ).first() # 獲取所有name以B開頭的Person對象 Person.match(graph).where( "_.name =~ 'B.*'" ) |
也可以通過圖graph對節(jié)點(diǎn)對象進(jìn)行操作:
1
2
3
4
5
6
|
# 更新圖中ali節(jié)點(diǎn)的相關(guān)數(shù)據(jù) graph.push(ali) # 用圖中的信息來更新ali節(jié)點(diǎn) graph.pull(ali) # 刪除圖中的ali對象節(jié)點(diǎn) graph.delete(ali) |
希望本文所述對大家Python程序設(shè)計(jì)有所幫助。
原文鏈接:https://blog.csdn.net/theVicTory/article/details/100527171