欢迎访问树枣文字网!

古诗词知识图谱Demo

147小编 分享 时间: 加入收藏 我要投稿 点赞

 

早前调研了知识图谱的基础概念和技术框架,最近这两个月倒腾了一个古诗词的图谱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://www.gushici.com/poetry_list?page={0}.format(page) referer = https://www.gushici.com/p_{0}.format(page) headers = {Host: www.gushici.com, 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 = 0 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的服务。

在浏览器中打开http://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[0]) 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,后续还有很多地方需要完善,如有遗漏或错误,请大家不吝指出,欢迎交流。

221381
领取福利

微信扫码领取福利

微信扫码分享