python 知识图谱demo_古诗词知识图谱Demo
早前調研了知識圖譜的基礎概念和技術框架,最近這兩個月倒騰了一個古詩詞的圖譜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的服務。
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
新建關系:不能單獨創建關系,必須指明關系的起始節點和終止節點。--表示無向關系,->和 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 = [
'[\"\'“‘《]?(?:是|出自|來[自|源])(?:哪[首|篇|個|里|兒|]?|什么)的?(?:[詩詞][文句]?|文章|句子)?',
'[\"\'“‘《]?的(?:來源|出處|(?:整[首|篇]|完整|全)[詩詞文])',
'(?:含有?|包[含括])[\"\'“‘《]?的(?:[詩詞][文句]?|文章|句子)'
]
source_list = list(map(re.compile, source_list))
查找作者的正則:
author_list = [
'[\"\'“‘《]?的(?:作者|[詩詞]人)',
'[\"\'“‘《]?是(?:誰|哪[位個])(?:作者|[詩詞]人)?',
]
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,后續還有很多地方需要完善,如有遺漏或錯誤,請大家不吝指出,歡迎交流。
總結
以上是生活随笔為你收集整理的python 知识图谱demo_古诗词知识图谱Demo的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: libcurl库使用详情、libcurl
- 下一篇: python 计算器 casio_CAS