说明

假设这样一个应用,从传统的mysql中读出原始数据,并将其合理的存储到neo4j中。以便进行模式查询 。

0 模式

neo4j的存储有几种模型,可以参考这篇文章:如何将大规模数据导入Neo4j

python批量提取合同数据_时间戳


其中:

  • 1 create 语句:我使用py2neo和cypher(通过neo4j提交)都实现过,效果和上面差不多
  • 优点:非常灵活,特别是cypher
  • 场景:如果有比较多的,复杂的变化,用这种方式比较好
  • 2 load csv语句:这个可能更适合中等规模的业务,好像没试过
  • 3 三个大批量导入(需要停库的方法),都要提前转为csv
  • 1 Batch Inserter/Batch Import: 适合冷库重建,以及大批量更新
  • 缺点:不熟悉java
  • 2 Neo4j-import: 适合冷库重建,只能生成新库
  • 缺点:每次都是重建,不过问题也不大

1 元数据

1.1 节点

1 节点 nid
2 名称 name
3 创建时间戳 create_time
4 更新时间戳 update_time
5 是否可用(0/1) is_enable

1.2 关系

1 关系 rid
2 创建时间戳 create_time
3 更新时间戳 update_time
4 是否可用(0/1) is_enable

2 索引

节点id, nid

在数据库中可以查询当前索引:schema

python批量提取合同数据_时间戳_02


建立/删除索引的命令(似乎一定要指明集合建立索引)

# 给MALE集合的name属性建立索引
CREATE INDEX ON :MALE(name);
DROP INDEX ON :MALE(name);

建立索引非常重要,否则存取速度会线性下降

3 动态导入模板

为了实现更加通用的操作,我认为最好通过两层方法进行cypher插入语句的生成。

3.1 python生成jinja模板

3.1.1 关系基础模板

有一部分的结构是固定的,可以先写一个txt文本(避免程序中出现大段的文本)
rel_base.txt

with 
[
{% for obj in data_list %}

{%if not loop.first%}
,
{%endif%}

{
ITER_ROWS_CONTENT
}

{% endfor %}

] as data 
UNWIND data as row 

merge (n1{nid:row.from})
merge (n2{nid:row.to})

create unique (n1)-[r:RELTYPE]->(n2)
set REL_ATTRS_SET
return r.rid

这个模板是为了向库中写入数据,通过nid去匹配节点(如果没有就创建该节点);找到节点后会为这两个节点建立一条唯一的关系(主数据模式,如果想记录日志就不要unique)。

里面ITER_ROWS_CONTENT, RELTYPE, REL_ATTRS_SET分别指定了jinja对数据的填充方法(如果存在就更新)、关系类型以及更新的变量。

这里约定:

  • 1 节点的id变量: nid (from, to 对应的都是节点id)
  • 2 关系的id变量: rid

假设读到的是一个表格数据,需要指定要存的列,以及每列的属性(num, char)

以下函数生成ITER_ROWS_CONTENT需要的字符串

def render_iter_rows(cols, cols_type):
    assert len(cols)==len(cols_type),'列的长度和属性必须一一对应'
    # 字符型变量if模板
    str_if_template_obj = '''{%% if obj['%s'] %%}
                %s:'{{obj['%s']}}'
                {%% endif %%}'''
    # 数值型变量if模板
    num_if_template_obj = '''{%% if obj['%s'] %%}
                %s:{{obj['%s']}}
                {%% endif %%}'''


    attr_list = []
    for i in range(len(cols)):
        if cols_attr[i].lower() == 'num':
            tem_var = num_if_template_obj % (cols[i],cols[i],cols[i])
        else:
            tem_var = str_if_template_obj % (cols[i],cols[i],cols[i])
        attr_list.append(tem_var)
    attr_list_str = ','.join(attr_list)
    return attr_list_str

以下函数生成REL_ATTRS_SET需要的字符串

# 根据columns生成数据的update
def render_set_attrs(cols):
    set_attr_list = []
    for c in cols:
        tem_set = 'r.%s =  row.%s' %(c, c)
        set_attr_list.append(tem_set)

    set_attr_list_str = ','.join(set_attr_list)
    return set_attr_list_str

根据以上函数生成jinja文本

ITER_ROWS_CONTENT = attr_list_str
RELTYPE='TEST'
REL_ATTRS_SET =  set_attr_list_str

tem_j2_str =  base_content.replace('ITER_ROWS_CONTENT',ITER_ROWS_CONTENT).replace('RELTYPE',RELTYPE).replace('REL_ATTRS_SET',REL_ATTRS_SET)

3.2 将数据灌入jinja模板生成cypher语句

通过python的操作,为本次特定的数据表生成了一份jinja语句(要通过jinja对象才能初始化为模板),内容如下

with 
[
{% for obj in data_list %}

{%if not loop.first%}
,
{%endif%}

{
{% if obj['rid'] %}
            rid:{{obj['rid']}}
            {% endif %},{% if obj['from'] %}
            from:{{obj['from']}}
            {% endif %},{% if obj['to'] %}
            to:{{obj['to']}}
            {% endif %},{% if obj['charvar'] %}
            charvar:'{{obj['charvar']}}'
            {% endif %},{% if obj['numvar'] %}
            numvar:{{obj['numvar']}}
            {% endif %}
}

{% endfor %}

] as data 
UNWIND data as row 

merge (n1{nid:row.from})
merge (n2{nid:row.to})

create unique (n1)-[r:TEST]->(n2)
set r.rid =  row.rid,r.from =  row.from,r.to =  row.to,r.charvar =  row.charvar,r.numvar =  row.numvar
return r.rid

接下来把这段文本变为jinjia模板,然后把数据灌入就生成了cypher语句

from jinja2 import Template
data_list = test_df.to_dict(orient='records')
template = Template(tem_j2_str)
the_cypher = template.render(data_list = data_list)

cypher语句如下(数据表中的两条数据被解开并转为cypher可执行的对象):

with 
[




{

            rid:1
            ,
            from:1
            ,
            to:2
            ,
            charvar:'abc'
            ,
            numvar:123
            
}




,


{

            rid:2
            ,
            from:2
            ,
            to:6
            ,
            charvar:'abcd'
            ,
            numvar:1234
            
}



] as data 
UNWIND data as row 

merge (n1{nid:row.from})
merge (n2{nid:row.to})

create unique (n1)-[r:TEST]->(n2)
set r.rid =  row.rid,r.from =  row.from,r.to =  row.to,r.charvar =  row.charvar,r.numvar =  row.numvar
return r.rid

有了cypher语句,通过graph.run就可以通过python交给数据库存入了,这种存储方式是效率最高的。

由于我们限定两个节点某一种关系只有一条边,因此以上操作是幂等的(即重复操作结果不变)

python批量提取合同数据_数据_03

4 基本操作

4.1 查询 query

4.1.1 随机查询多层

MATCH p=()-->()-->()-->() RETURN p LIMIT 25

复杂查询

深度运算符 , 参考文章

match (n1:Enterprise)-[r1:INVEST*1..3]->(n:Enterprise{nid:7934}) 
return n1, r1, n

直接使用with的多层查询好像没匹配到就不返回了

match (n1:Enterprise)-[r1:INVEST]->(n:Enterprise{nid:7934})
with n1, r1, n 
match (n)->[r2:INVEST]->(n2:Enterprise)
return n, n1,r1,r2

直接那种通过深度查询把关系绑在列表上的方法官方不推荐了(我猜是使用迭代器了)

python批量提取合同数据_Enterprise_04

match p = (n1:Enterprise)-[:INVEST*1..3]->(n:Enterprise{nid:3930}) 
with * # 这里好像没有with也没问题
return relationships(p) as r1

4.2 变更 alter

4.2.1 创建/更新 create/update

后补

4.2.2 修改 delete

后补

5 业务操作

5.1 以关系为核心

一条关系必不可少的几个元素:

  • 关系类型:reltype, 关系类型,通常可以一种类型一张表或者一个集合。
  • 关系id: rid ,对应一条唯一的行,或者记录号。
  • 源节点 : from , 对应一个节点的nid
  • 目标节点: to, 对应一个节点的nid(箭头指向的节点)

6 静态批量导入数据

6.1 neo4j的启停

切到neo4j安装目录下面的bin目录,例如
YOUR_PATH/neo4j/neo4j-community-3.4.7/bin

./neo4j stop

Stopping Neo4j.... stopped

切到数据目录下删库
/neo4j/neo4j-community-3.4.7/data/databases

# 删库
rm -rf graph.db

重新启动

./neo4j start
Active database: graph.db

重启的时候很干净(此时会自动重新创建graph.db)

python批量提取合同数据_时间戳_05

关于neo4j的备份可以参考这篇文章

6.2 按格式制作节点和关系的样例数据

格式还是略有点坑的,没有整特别明白,看着测试结果好就行了。

首先,数据文件要整理成csv。这里要注意的是尽量规避数据字段可能存在的干扰(不要有英文逗号、不要缺失),另外如果是文本字段,尽量用双引号括起来。

import re 
# 正则提取短语里的中文 + 英文
def get_alpha_str(some_str):
    return  ''.join(re.findall('[\u4e00-\u9fa5]|[a-z][A-Z]+', some_str))

对应的一些的代码:

import csv
import pandas as  pd

company_df[col] = company_df[col].apply(lambda x: '"' + str(x) +  '"')
# 如果数据里确实会有引号,那么不要加 quoting=csv.QUOTE_NONE
company_df.to_csv('company_node.csv', index=False,quoting=csv.QUOTE_NONE)

示例:

id:ID(Enterprise),:LABEL,name,estiblish_time,reg_capital,reg_status,credit_code,reg_number
e1,Enterprise,"a有限公司","2001-05-17 00:00:00","50.000000万人民币","注销","a111","a222"
e2,Enterprise,"b工程有限公司","2008-09-11 00:00:00","1000.000000万人民币","开业","b111","b222"
e3,Enterprise,"c科技有限责任公司","2003-05-14 00:00:00","500.000000万人民币","开业","c111","c222"

其次,表头要按格式填写。

以下是我猜的哈

  • 表头通过:进行ket-value式的声明。对于节点来说,一定要有id和表名/集合名(label)。
  • 在上面这个例子里,id列是节点id属性,所以在后面使用ID(Enterprise)来指明这个是Enterprise集合的ID。
  • 第二列指定节点的集合(应该是可以用逗号分割同时指定多个标签,但是我没有实验)
  • 其他的属性(都是字符型变量)都保留了原始的名称,并都用引号括起来。

类似地,对于关系数据:

:TYPE,:START_ID(Enterprise),:END_ID(Enterprise),rid,amount:float
Invest,e1012,e1010,"r1557",4000.0
Invest,e1014,e1010,"r1559",1000.0
Invest,e18458,e18457,"r23429",60.0
  • 指明了关系类型(TYPE),起始(START)和终止(END)节点
  • 没有加引号的,要么是固定的三个变量:关系、起始和终止节点,要么指明了数据类型(amount:float)
  • 为了便于编号,每个结合的节点用字母开头,数值结尾,neo4j是支持的。(我称为abc123类型)
  • 其他未加引号的属性应该会默认为字符,例如下面的rid我就没加引号
:TYPE,:START_ID(Person),:END_ID(Enterprise),amount,rid
Invest,p647,e327,37.5,r495
Invest,p649,e327,12.5,r496
Invest,p3265,e1604,1.0,r2543

6.3 执行导入命令

命令要写成一行,切换到neo4j的bin目录下执行

./neo4j-import --multiline-fields=true --bad-tolerance=100 --into NEO4j_path/data/databases/graph.db --id-type string  --nodes  YOURPATH/test_node1.csv --nodes  YOURPATH/test_node2.csv  --relationships YOURPATH/test_rel.csv
  • multiline-fields : 我猜是允许并行写入的意思
  • bad-tolerance:允许的错误条数。这个要注意,如果错误的记录数超过了限制导入会直接失败的
  • into:将数据导入到哪个库(默认是graph.db)
  • id-type: abc123是string类型的
  • nodes :后面可以跟你的节点数据。(多个节点就多个nodes参数)
  • relationships:后面跟你的关系数据。(多个关系就多个relationships参数)

导入成功:

python批量提取合同数据_数据_06


python批量提取合同数据_python批量提取合同数据_07

如果执行多次导入,需要按以下步骤:

  • 1 停服务:cd YOUR_NEO4j/bin && ./neo4j stop
  • 2 删除库 cd YOUR_NEO4j/data/databases &&rm -rf graph.db
  • 3 导入 cd YOUR_NEO4j/bin && ./neo4j-import --multiline-fields=true --bad-tolerance=100 --into YOUR_NEO4j/data/databases/graph.db --id-type string --nodes Your_CSV1 --relationships Your_CSV2
  • 4 重新启动cd YOUR_NEO4j/bin && ./neo4j start

静态导入速度非常快,10分钟建完了6000万节点和1千万关系。

6.4 建立索引

导入完成后别忘了建立索引,根据实际需要查询的字段建立(cypher好像需要一条条命令执行)

CREATE INDEX ON :Person(name);
CREATE INDEX ON :Person(id);
CREATE INDEX ON :Enterprise(name);
CREATE INDEX ON :Enterprise(id);

建立索引的速度还是比较快的

python批量提取合同数据_时间戳_08


查看当前库索引

python批量提取合同数据_Enterprise_09

7 py2neo 操作数据

这里主要谈使用cypher查询的原生处理(没有用py2neo自己的方法)。

# 查询 
the_cypher = '''match SOME_PATTERN return p'''
start = time.time()
res = graph.run(the_cypher)
end = time.time()
print('Takes %.2f' %(end-start))

在6000万节点,1千万关系的条件差,两层关系的查询只花费了0.03秒。这种查询方式类似pymysql向数据查询,返回一个游标(迭代器),数据取出以后就空了。

python批量提取合同数据_数据_10


几个地方值得注意一下:

  • 1 data() :将结果以列表方式解开,每个元素和cypher里返回的有关。例如我返回的p,那么就是一个路径
  • 2 to_series(), to_data_frame(): 与pandas对接
  • 3 to_subgraph(): 和py2neo定义的子图对接,进一步可以进行快速的图计算(或者转到networkx计算)

8 关于导入格式

在大批量插入数据时可能会遇到文本不干净的问题, 这时候可能需要对csv的分隔符进行修改。以下部分参考这篇文章,导入命令的完整参数如下

usage: neo4j-admin import [--mode=csv] [--database=<name>]
                          [--additional-config=<config-file-path>]
                          [--report-file=<filename>]
                          [--nodes[:Label1:Label2]=<"file1,file2,...">]
                          [--relationships[:RELATIONSHIP_TYPE]=<"file1,file2,...">]
                          [--id-type=<STRING|INTEGER|ACTUAL>]
                          [--input-encoding=<character-set>]
                          [--ignore-extra-columns[=<true|false>]]
                          [--ignore-duplicate-nodes[=<true|false>]]
                          [--ignore-missing-nodes[=<true|false>]]
                          [--multiline-fields[=<true|false>]]
                          [--delimiter=<delimiter-character>]
                          [--array-delimiter=<array-delimiter-character>]
                          [--quote=<quotation-character>]
                          [--max-memory=<max-memory-that-importer-can-use>]
                          [--f=<File containing all arguments to this import>]
                          [--high-io=<true/false>]
usage: neo4j-admin import --mode=database [--database=<name>]
                          [--additional-config=<config-file-path>]
                          [--from=<source-directory>]

可以看到这里有两个关于分隔符的控制。

[--delimiter=<delimiter-character>]
[--array-delimiter=<array-delimiter-character>]

没有找到很匹配的例子, 在官网文档找到这个

python批量提取合同数据_时间戳_11


如果我想用类似’!@#'这样的字符串分割的化,理论上应该是–array-delimiter这样的分隔符,不过没有例子,回头等我实验一下再补结果。

导入表头支持的数据格式

参考官网

使用中的一个int,long,float,double,boolean,byte,short,char,string,point,date,localtime,
time,localdatetime, datetime,和duration指定为属性的数据类型。如果未提供数据类型,则默认为string。
要定义数组类型,请追加[]到该类型。默认情况下,数组值用分隔;。可以使用来指定其他定界符--array-delimiter。
如果布尔值与文本完全匹配,则它们为truetrue。所有其他值均为假。包含定界符的值需要通过用双引号引起来或
使用带有--delimiter选项的其他定界符来转义。

9 导入时间

导入耗时较少,但是索引建立的时间较长

节点数

索引变量长度

时间

2000万

<24个字

3m30s

7000万

<24个字

23min