HOME
HOME
文章目录
  1. 1、概念
    1. 1.1 、节点
    2. 1.2、 关系
    3. 1.3、属性
  2. 2、 Cypher查询语句
    1. 2.1、增删改查
  3. 3、实践

图数据库学习记录

最近工作中在解析IP地址与网络关系的时候,用到了图数据库,这里记录一下学习过程。

1、概念

图数据库相对于普通的关系型数据库的概念有很大区别,普通的关系型数据库就是以表格行列模式的结构化数据进行存储,而图数据库则直接是一种抽象的图形及图形关系进行存储。即使是抽象的数据存储,但图数据库并没有想象的那么复杂,在对网络拓扑解析的应用中得心应手,很容易就解决了我的问题。

图数据库一共存在3种概念,分别是:节点、关系、属性。理解这三种概念后就基本把图数据库的理念给搞明白了。在最新的Neo4j图数据库版本中,还增加了标签的概念。

1.1 、节点

节点(Node)是图数据库中间的基本元素,表示一个实体记录,类似于关系型数据中的一条数据记录。一个节点中包含多个属性(property)和多个标签(label)。

1.2、 关系

节点与节点之间的连线称为关系,图数据库相对于关系型数据库就是可以直接将抽象的关系进行存储。关系包含开始节点和结束节点,两头不能为空。关系是存在方向的。在最新版本的数据库中,关系也可以添加属性,但在我使用过程中发现基本没有必要在关系上添加属性。

1.3、属性

节点和关系都可以拥有多个属性,属性是由键值对组成的。属性值可以是基本的数据类型,或者由基本数据类型(boolean、byte、short、int、long、float、double、char、string)组成的数组。

需要注意的是属性没有null的概念,如果一个属性不需要了,直接键值对就删除了,在使用cypher查询语句的时候可以使用IS NULL判断属性是否为空。

2、 Cypher查询语句

关系型数据库中有SQL语言,图数据库中的查询语言就是Cypher。

Cypher是一种声明式的图数据库查询语言,能够对图数据库进行增删改查。在实际使用的过程中,除了Cypher针对图数据的一些特殊的语句,用起来跟SQL查询语句极为相似,在熟悉SQL查询语句的情况下,使用Cypher的难度较小。

节点语法

Cypher使用一对圆括号来表示节点,括号中间可以进行匿名查询和属性查询

1
2
3
4
()   // 匿名节点
(foo) // 节点的变量为foo
(foo:Node) // 节点变量为foo的Node节点
(foo:Node {title: 'test'}) // 属性title为test

关系语法

Cypher词啊用--表示无方向的关系,有方向的关系为-->或者<--,同样在关系查询语句中也包含变量和属性的查询

1
2
3
4
5
-->
-[role]->
-[:HAS_NODE]->
-[role:HAS_NODE]->
-[role:HAS_NODE {title: 'test'}]->

模式语法

将节点语法和关系语法组合在一起,就是一个模式。

1
(foo:Node)-[:HAS_NODE]->(target:Node {title: 'test'})

2.1、增删改查

使用create创建节点

1
CREATE (n:USer {name: "Test"})

使用create创建关系

1
2
MATCH (n {name: 'a'}), (m {name: 'b'})
CREATE (n)-[r:KNOWNS]->(m)

使用MERGE创建节点

使用MERGE创建节点的时候,类似于django orm中的get_or_create,如果能够查询出节点,则使用该节点,如果查询不出这个节点,则创建一个节点。

1
2
MERGE (n: Node{name: 'a'})
ON CREATE SET n.created=timestamp()

MERGE创建关系也一样

1
2
MATCH (a:Person {name: 'a'}), (b:Person{name:'b'})
MERGE (a)-[r:LOVES]->(b)

更新数据

使用set更新数据

1
2
MATCH (n: Node{name:'a'})
SET n.name = 'b'

删除数据

使用delete可以删除数据

1
MATCH (n) DELETE n;

在节点存在关系的时候,无法直接删除,需要增加DETACH

1
MATCH (n) DETACH DELETE n;

REMOVE移除数据

使用REMOVE可以移除一个数据,比如移除一个节点移除一个属性

1
2
MATCH (n: {name: 'b'})
REMOVE n.name

条件过滤查询

1
2
3
MATCH (n:Node)
WHERE n.name <> "a"
RETURN n

联合查询

1
2
3
4
5
MATCH (a)-[:KNOWNS]->(b)
RETURN b.name
UNION
MATCH (a)-[:LOVES]->(b)
RETURN b.name

CASE子句

1
2
3
4
5
6
MATCH (n:Node) RETURN
CASE n.name
WHEN 'a' THEN 1
WHEN 'b' THEN 2
ELSE 3
END as test

3、实践

在实际编程过程中的时候,需要用到其他语言相关的库来连接图数据库。在本篇文章中,使用了py2neo库。

模拟拓扑

在进行解析的时候,我们假定了一个拓扑如下,存在一个多层网络,通过防火墙的端口映射,或者NAT将内部的端口或者IP给映射到公网,我们需要通过图数据库来解析这个拓扑。

image-20220906113024097

图上的拓扑还是较为抽象,我们在解析的时候,需要将拓扑的映射关系先人工换算成表格的形式,方便我们做数据解析。同时也可以用关系型数据库将解析的映射关系给存起来,后期可以使用django的信号或者其他方式将关系型数据库的映射关系和图数据库中间存储的拓扑进行同步。

解析后的数据如下:

源地址 资产编号 源端口 目的地址 目的端口 备注
192.168.0.4 LB001 8080 10.0.1.2 80 负载均衡
192.168.0.5 LB002 8080 10.0.1.2 80 负载均衡
192.168.0.6 LB003 8080 10.0.1.2 80 负载均衡
10.0.1.2 80 172.168.0.33 80
10.0.1.4 Server001 445 172.168.0.33 445
10.0.1.5 Server002 172.168.0.35
10.0.1.4 Server001 3389 172.168.0.36 3389
172.168.0.33 80 211.211.211.4 80
172.168.1.5 Inter001 211.211.211.5
172.168.1.6 Inter002 3330 211.211.211.6 3330
172.168.0.35 5678 211.211.211.35 5678
10.0.2.3 DMZ001 25 211.211.211.6 25
10.0.2.4 DMZ002 80 211.211.211.6 80
10.0.2.5 DMZ003 4444 211.211.211.7 5555
10.0.2.3 DMZ001 25 172.168.2.6 25
10.0.2.4 DMZ002 80 172.168.2.6 80
10.0.2.5 DMZ003 4444 172.168.2.7 5555

py2neo的model

py2neo支持ogm创建model数据类型,来定义节点信息,在这个例子中,我们可以定义两种节点类型,一个是IP节点一个是Port节点,具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from py2neo.ogm import GraphObject, Property, RelatedTo, RelatedFrom, RelatedObjects, Related, Label


class IP(GraphObject):

addr = Property()
types = Property()
name = Property()
mapping_to = RelatedTo('IP', 'IP_MAPPING_TO')


class Port(GraphObject):
port_number = Property()
mapping_to = RelatedTo('Port', 'PORT_MAPPING_TO', )
belongs_to = RelatedTo('IP', 'BELONGS_TO')

IP节点包含3个属性和一个关系,Port节点包含一个属性和两个关系,RelateTo表示指向对应的节点。

解析表格使用pandas一行一行的进行读取,读取逻辑如下:

1、判断IP节点是否存在,不存在则新建,存在则获取

2、判断IP节点对应的端口节点是否存在,不存在则新建,存在则获取,并关联上IP节点

3、如果该行的port不为空,则连接两个port,否则连接两个ip

最后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import pandas as pd
from py2neo.ogm import Repository
from py2neo import AND
from models import IP, Port

rs = Repository('neo4j://localhost:7687', auth=('neo4j', 'password'))


def main():
df = pd.read_excel('ip.xlsx', dtype=str)
all_ip = df['源地址'].drop_duplicates().tolist()
all_ip = all_ip +df['目的地址'].drop_duplicates().tolist()
all_ip = list(set(all_ip))
for ip in all_ip:
node = IP()
node.addr = ip.strip()
rs.save(node)
for _, item in df.iterrows():
source_ip = rs.match(IP).where(addr=item['源地址']).first()
target_ip = rs.match(IP).where(addr=item['目的地址']).first()
if not pd.isnull(item['源端口']) and not pd.isnull(item['目的端口']):
# 端口映射
source_port = rs.match(Port).where('(_)-[:BELONGS_TO]->(:IP{addr: "%s"}) AND _.port_number="%s"'%(source_ip.addr, item['源端口'])).first()
target_port = rs.match(Port).where('(_)-[:BELONGS_TO]->(:IP{addr: "%s"}) AND _.port_number="%s"'%(target_ip.addr, item['目的端口'])).first()
if not target_port:
target_port = Port()
target_port.port_number = item['目的端口']
target_port.belongs_to.add(target_ip)
if not source_port:
source_port = Port()
source_port.port_number = item['源端口']
source_port.belongs_to.add(source_ip)
source_port.mapping_to.add(target_port)

source_ip.name = item['资产编号'] if not pd.isnull(item['资产编号']) else None
source_ip.type = 'endpoint' if not pd.isnull(item['资产编号']) else None
rs.save(source_port)
rs.save(target_port)
rs.save(source_ip)
rs.save(target_ip)
else:
# 地址映射
source_ip.name = item['资产编号'] if not pd.isnull(item['资产编号']) else None
source_ip.type = 'endpoint' if not pd.isnull(item['资产编号']) else None
source_ip.mapping_to.add(target_ip)
rs.save(source_ip)


if __name__ == '__main__':
main()

最后解析出来的图数据如下所示,后续就能够按照该数据进行查询和分析数据了。后续如何在前端页面展示图数据后面空了再写文章进行讲解。

image-20220906114148701