ITPub博客

首页 > 数据库 > > 古诗词知识图谱Demo

古诗词知识图谱Demo

作者:兔子爱喝奶茶 时间:2020-05-21 09:49:20 0 删除 编辑

早前调研了知识图谱的基础概念和技术框架,最近这两个月倒腾了一个古诗词的图谱demo,仅以此文记录一下实验过程。从零开始做这个Demo,整个过程大致分为三大步骤:数据采集,数据存储以及图谱应用,全文将按这三步进行记录。

一、数据采集

既然是从零开始,那第一步就是要爬取数据。搜了几个诗词网站,对比网页排版结构和内容丰富程度,个人觉得古诗词网是个不错的选择,在这里感谢站长为经典文化传承作出的贡献。

1. 网页分析

F12打开诗词列表页的源码,查看头部信息如下图:

请求的url格式固定,只有页码改变;请求的类型为get。

多看几个页面,可以发现请求头中Cookie的hm_lvt和hm_lpvt为两个时间戳,不同页面只有hml_pvt发生改变;old_url取值为当前页码;Referer也是随页码改变的固定格式url。

请求列表页面的返回结果为json列表,可以非常方便地提取需要的信息,而不用去html中定位并解析目标元素,省去了爬虫中的一半工作量:

每一个json对应一首诗词,包含标题、正文、作者、朝代、标签、体裁、作者介绍、译注、赏析等信息,这种结构化的数据,也免去了数据抽取和整理的很多工作。

2. 爬虫代码

这一类网站广告很少,也没有收费业务,带有公益性质,网站服务器一般也扛不住爬虫的压力,常常会采取一些反爬措施,比如封禁IP。为了爬取这些网站,一方面要降低爬取速度;另一方面要维护代理池,在被封的时候更换IP。爬取过程中及时保存爬虫结果,并记录爬取失败的页面,方便以后再重爬。

def crawl_pages(page_list, save_path, ip_pool, retry_times=5):
    fail_list = list()
    lvt_code = int(time.time())
    ip = random.choice(ip_pool)
    for page in page_list:
        time.sleep(3 * random.random())
        lpvt_code = int(time.time())
        page_url = 'https://poetry_list?page={0}'.format(page)
        referer = 'https://p_{0}'.format(page)
        headers = {'Host': '',
                   'Connection': 'keep-alive',
                   'Accept': '*/*',
                   'X-Requested-With': 'XMLHttpRequest',
                   'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36',
                   'Referer': referer,
                   'Accept-Encoding': 'gzip, deflate, br',
                   'Accept-Language': 'zh-CN,zh;q=0.9',
                   'Cookie': 'JSESSIONID=48304F9E8D55A9F2F8ACC14B7EC5A02D; Hm_lvt_98209c07e81fcbdd5f79bd9e94c617eb={0}; Hm_lpvt_98209c07e81fcbdd5f79bd9e94c617eb={1}; \
                             old_url=/p_{2}'.format(lvt_code, lpvt_code, page)}
        times = 
        flag = False
        while times <= retry_times:
            times += 1
            try:
                response = requests.get(page_url, headers=headers, proxies={'https': ip}, verify=False, timeout=10)
                flag = True
                with open(os.path.join(save_path, page), 'w', encoding='utf-8') as f:
                    json.dump(response.text, f, ensure_ascii=False)
                break
            except:
                ip = random.choice(ip_pool)
        if not flag:
            fail_list.append(page)
    with open(os.path.join(save_path, 'fail'), 'w', encoding='utf-8') as f:
        json.dump(fail_list, f, ensure_ascii=False)

二、数据存储

知识图谱的数据层有多重存储方式,本文选择采用Neo4j搭建。Noe4j是一个高性能的轻量级图形数据库,应对小型知识图谱绰绰有余。虽然关系型数据库通过多重join也可以实现数据间的复杂关系查询,但是多表数据join然后过滤筛选导致性能会非常差,而图数据库很好地解决这样的问题,它只用遍历相关节点,不用操作全量数据,性能会大大提升。

1. Neo4j安装

从Neo4j官网可以下载开源的Noe4j社区版,解压到D盘,然后配置环境变量。注意:Neo4j依赖于Java运行环境,安装Neo4j前请检查本机是否安装Java并配置Java的环境变量。

打开cmd命令行窗口,进入安装目录下的bin文件夹,执行“neo4j install-service”命令,安装Neo4j服务。然后执行“neo4j start”命令,启动Neo4j的服务。

在浏览器中打开localhost:7474/browser/,即可看到Neo4j的可视化操作页面:

2. Neo4j操作

Neo4j是一种图数据库,其中并没有数据表的概念,只包含节点和边,节点表示实体,边表示实体间的关系(分为有向关系和无向关系),节点和边可以包含键值对表示的属性。

用惯了关系型数据库,初次接触图数据库感觉有点别扭。为了便于自己理解,我是这样来类比的:

节点和关系是图数据库中定义的最原始的两个基类,节点(关系)的标签表示由节点(关系)基类派生出来的一类节点(关系)。当导入数据之后,具有属性值的某一个节点(关系),就是该标签对应的节点(关系)派生类所生成的实例。

(1)导入数据

Neo4j提供import命令,可以批量导入csv格式的数据。针对json格式的数据,可以转为csv格式,然后用import导入,注意json中的双引号需要进行转义。为避免格式转换过程中的错误,可以调用apoc函数库中的json导入工具。

apoc的安装方式为:从github中下载apoc的jar包,将jar包复制到Neo4j安装目录的plugins路径下,在neo4j.conf中配置apoc.import.file.enabled=true,表示允许apoc导入文件。重启Neo4j服务,调用apoc.load.json即可导入json数据。

(2)查询数据

Neo4j的查询语言为Cypher(第一眼看成了Cython,然而这两个半点不沾边)。官网有完整版的Cypher手册,本文只挑选最基础的几个语句简要介绍。

A.增:

新建节点: CREATE (node: NodeType {AttributeKey : AttributeValue})

// 创建一个姓名为Jack的Person类的节点,并返回该节点
CREATE (a:Person {name:"Jack"}) 
RETURN a

新建关系:不能单独创建关系,必须指明关系的起始节点和终止节点。--表示无向关系,->和<-表示有向关系。CREATE StartNode - (relationship: RelationshipType {AttributeKey : AttributeValue}) -> EndNode

// 创建两个Person之间Knows的关系,并返回节点和关系
CREATE (a:Person)-[k:KNOWS]-(b:Person)
RETURN a, k, b

B.查:

查询节点:MATCH (node: NodeType {AttributeKey : AttributeValue}) WHERE node.AttributeKey = AttributeValue

// 查询1970年后出生的Person节点,并返回节点
MATCH (n) 
WHERE n.born > 1970
RETURN n;

查询关系:MATCH StartNode - (relationship: RelationshipType {AttributeKey : AttributeValue}) -> EndNode WHERE relationship.AttributeKey = AttributeValue

// 查询自从2015年起居住(LIVES_IN)在NewYork城市(City)的名叫Mike的人(Person),并返回节点和关系
MATCH (p:Person {name:"Michel"})-[s:LIVES_IN]->(c:City {name:"NewYork"}) 
WHERE s.since = 2015 
RETURN p,s,c 

C.改:

修改属性:MATCH (variable : NodeType|RelationshipType) SET variable = {AttributeKey : AttributeValue}

// 查询名为Jack的Person类节点,并将名字改为Michel,年龄改为23
MATCH (p:Person) 
WHERE p.name = "Jack" 
SET p = {name: "Michel", age: 23}

D.删:

删除节点:与该节点相关的关系也需要删除。MATCH (node) - [relationship] - () DELETE node, relationship

// 删除名为Jack的Person节点及关联关系
MATCH (p:Person)-[relationship]-() 
WHERE p.name = "Jack" 
DELETE relationship, p

删除属性:MATCH (node) - [relationship] - () REMOVE node.AttributeKey, relationship.AttributeKey

// 删除名为Michel的Person节点的年龄属性
MATCH (p:Person) 
WHERE p.name = "Michel" 
REMOVE p.age

3. Neo4j实践

本文设计的知识图谱包含三类节点:诗词(Poem)、作者(Author)、标签(Tag)。作者与诗词是写作(WRITE)的关系,诗词、作者与标签是标识(LABEL)关系。

// 在三类节点上创建索引
create index on :Poem(uuid);
create index on :Author(name);
create index on :Tag(tag);

// 将数据导入Neo4j
call apoc.periodic.iterate('call apoc.load.json("web_file_poetry.json") yield value as poem',
                           'merge (p:Poem{uuid: poem.poem_id})
                            set p.title = poem.title, p.content=poem.poem, p.tag=poem.tag, p.appreciation=poem.appreciation, p.background=poem.background
                            // 作者节点
                            merge (a:Author{name: poem.poet, dynasty: poem.dynasty})
                            // 作者到诗词的关系
                            merge (a)-[r1:WRITE]->(p)',
                            {batchSize:100000, iterateList:true, parallel:true});

// 建立诗词、作者与标签之间的关系
match (a:Author)-[:WRITE]->(p:Poem)
where p.tag <> ''
unwind split(trim(p.tag), ",") as tag
// 标签节点
merge (t:Tag{tag: tag})
// 诗词到标签的关系
merge (p)-[r1:LABEL]->(t)
// 作者到标签的关系
merge (a)-[r2:LABEL]->(t);

图数据库创建成功之后,可以查询看看效果,Neo4j的可视化做的还是挺好看的。


三、图谱应用

知识问答是基于知识图谱的一项应用,前沿的问答系统多采用深度学习、自然语言处理等技术。本文采用最简单的正则匹配( ̄▽ ̄)~*

1.首先,定义可以回答的问题类型:

查找诗词的正则:

source_list = [
    '[\"\'“‘《<]?(\S+?)[\"\'”’》>]?(?:是|出自|来[自|源])(?:哪[首|篇|个|里|儿|]?|什么)的?(?:[诗词][文句]?|文章|句子)?',
    '[\"\'“‘《<]?(\S+?)[\"\'”’》>]?的(?:来源|出处|(?:整[首|篇]|完整|全)[诗词文])',
    '(?:含有?|包[含括])[\"\'“‘《<]?(\S+?)[\"\'”’》>]?的(?:[诗词][文句]?|文章|句子)'
]
source_list = list(map(re.compile, source_list))

查找作者的正则:

author_list = [
    '[\"\'“‘《<]?(\S+?)[\"\'”’》>]?的(?:作者|[诗词]人)',
    '[\"\'“‘《<]?(\S+?)[\"\'”’》>]?是(?:谁|哪[位个])(?:作者|[诗词]人)?',
]
author_list = list(map(re.compile, author_list))

查找标签的正则:

tag_list = [
    '(?:写|描[写绘述]|表达)(\S+?)的?[诗词]',
]
tag_list = list(map(re.compile, tag_list))

整合问题正则:

rules = {
    'source': source_list,
    'author': author_list,
    'tag': tag_list,
}

2.其次,根据正则判断问题类型,并提取问题中的要素

def match(question):
    match_result = None
    for mode, temp_list in rules.items():
        for temp in temp_list:
            text = temp.findall(question)
            if text:
                match_result = (mode, text[])
                break
    return match_result

3.最后,从Neo4j中根据问题要素查找并返回问题答案

def parse(question):
    match_result = match(question)
    if match_result is None:
        return None
    mode, text = match_result
    res = None
    cql = None
    if mode == 'source':
        cql = 'match (p:Poem) where p.content contains "{0}" return p.title, p.content'.format(text)
    elif mode == 'author':
        cql = 'match (a:Author)-[:WRITE]->(p:Poem) where p.content contains "{0}" return a.name'.format(text)
    elif mode == 'tag':
        tag_list = jieba.lcut(text)
        cql = 'match (p:Poem)-[:LABEL]->(t:Tag) where t.tag in {0} return p.content, t.tag'.format(tag_list)
    else:
        pass
    if cql:
        res = neo4j_graph.run(cql).to_data_frame()
    return res

后记:

本文只是搭建了一个非常小的图谱demo,后续还有很多地方需要完善,如有遗漏或错误,请大家不吝指出,欢迎交流。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69918097/viewspace-3158682/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2019-04-26

  • 博文量
    1470
  • 访问量
    11183