今天分享一个自己从数据爬取到d3可视化的中式菜谱知识图谱可视化系统的搭建流程。
Github项目地址:https://github.com/ngl567/CookBook-KG
访问可视化系统GithubPage地址:https://ngl567.github.io/CookBook-KG/
本项目开发的系统名称为AI Food Time,中文名为爱食光。通过收集网上完全公开的有关中式菜谱的数据,经过数据清洗和分析,转换为知识图谱的存储结构,并提供可视化展示和模糊搜索等功能,为热爱美食与烹饪的人们提供方便快捷的中式菜谱服务,并以知识图谱的形式直观显示出不同菜品的关系及所用原料,在生活中具有很大的实际应用需求,包括:
怎么样,是不是觉得还有那么点意思。在可视化系统中,同一类实体用相同颜色的节点表示,鼠标位于某个节点上方时显示其相关联的其它实体和之间的关系名称;具有同一类实体显示开关,节点显示模式转换,并支持搜索功能;同时,我们对每种菜品的信息栏中显示菜品对应的成品图片,并进行了实体对齐,消除了食品原料中比如“蒜”和“大蒜”这类的冗余信息。
这里展示的是mini版系统,包含10大类,50种菜品之间的关联关系,包括菜品制作的各种食材和制作步骤,建议用电脑浏览器打开,如需体验可直接进入Github Page(如果打开没有可视化显示,请尝试多刷新几次)访问入口
所有菜谱的数据都是从完全公开的网页上爬取的,这些网页上的数据是以半结构化知识呈现的,如图所示:
使用XPath可以很轻松地从网页上将这些半结构化知识爬取出来,先以树形结构存储菜谱及属性数据:
菜品大类
|_具体的精品特色菜
|_主料
|_辅料
|_配料
|_制作步骤
接着,对于树形结构存储的数据,以三元组的格式:菜品大类-属于-具体的精品特色菜,精品特色菜-主料-主料名,精品特色菜-配料-配料名,精品特色菜-辅料-辅料名,精品特色菜-制作步骤-制作步骤列表表示所有数据。用于可视化的数据分为三元组组成的关系图结构数据vizdata.json和实体属性组成的数据entities_items.json。
三元组组成的关系图结构数据vizdata.json存储的是字典数据,“links”键对应的是所有头实体-关系-尾实体组成的三元组,“nodes”设定了节点的类型、名称和大小等属性。
"links": [
{
"relation": "选材",
"source": "山楂红烧肉",
"target": "五花肉",
"value": 3
},
{
"relation": "选材",
"source": "山楂红烧肉",
"target": "山楂",
"value": 3
},
...
]
"nodes": [
{
"class": "菜品大类",
"group": "0",
"id": "红烧肉",
"size": "16"
},
{
"class": "精品特色菜",
"group": "1",
"id": "家庭版水煮活鱼",
"size": "10"
},
...
]
实体属性组成的entities_items.json文件存储所有实体属性的字典数据,是以”菜品“为一级索引,对应的主料、辅料、配料和制作步骤分别为二级索引,以及属性值组成的列表,如下所示:
"桂香红烧肉": {
"主料": [
"五花肉: 两条"
],
"辅料": [
"葱: 一根",
"姜: 两片",
"桂皮: 一块",
"香叶: 两片"
],
"配料": [
"老抽: 一汤匙",
"生抽: 两汤匙",
"冰糖: 适量",
"料酒: 两汤匙"
],
"特色": [
"口味: 咸甜",
"工艺: 烧",
"耗时: 一小时",
"难度: 简单"
],
"制作步骤": [
"1: 五花肉放入沸水中,煮至断生,洗净",
"2: 准备材料",
"3: 五花肉切块",
"4: 取砂锅,热锅冷油,加入葱姜,桂皮,香叶爆香。放入五花肉,加高汤。",
"5: 加入冰糖,料酒,生抽和老抽,加盖小火煮一个小时,开盖收汁。"
]
},...
D3是基于数据的文档操作javascript库,D3能够把数据和HTML、SVG、CSS结合起来,创造出可交互的数据图表。我分别用D3知识图谱力导向图和Neo4j分别构建了知识图谱可视化系统,D3在可视化方面具有更好的展示和灵活性,因此选用D3进行知识图谱的可视化。
对于上面得到的关系图数据vizdata.json和实体属性数据entities_itmes.json两个文件可以存储在自己的github项目中,因为D3可视化只支持从web服务读取json数据。由于字数限制,本文给出D3可视化的几个主要模块。
首先,需要设定可视化的样式,具体可以看github中的代码。然后,需要从json文件中读取关系图数据:
var graph;
d3.json("https://raw.githubusercontent.com/ngl567/CookBook-KG/master/visualization
/vizdata_mimini_aglin.json", function(error, data)
用vizdata.json中的links数据去驱动两个节点之间的边的线宽:
//边上的文字(实体之间的关系)
var linktext = svg.append('g')
.attr("class", "linetexts")
.selectAll("text")
.data(graph.links)
.enter()
.append("text")
.style("display","block")
.style("color","red")
.text(function(d){
return d.relation;
});
用vizdata.json中的links数据去驱动两个节点之间的边的线宽:
//边上的文字(实体之间的关系)
var linktext = svg.append('g')
.attr("class", "linetexts")
.selectAll("text")
.data(graph.links)
.enter()
.append("text")
.style("display","block")
.style("color","red")
.text(function(d){
return d.relation;
});
添加所有的节点,并对每个节点按照不同的类型设置节点颜色:
// 添加所有的node
var node = svg.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(graph.nodes)
.enter().append('circle')
.attr("r", function(d) {
return d.size
})
.attr('fill', function(d){ // 填充的颜色
return colors[d.group];
})
.attr('stroke', 'none') // 没有描边
.attr('name', function(d){
return d.id;
})
.call(d3.drag() // 绑定d3的拖动函数
.on("start", dragstarted) // 拖动开始
.on("drag", dragged) // 拖动进行
.on("end", dragended)); // 拖动结束
通过点击圆点和文字两种方式表示节点切换不同的模式:
// 处理模式点击后的事件(这些元素页面上本来有)
$('#mode span').click(function(event) {
// 把mode里面所有span的active全部去掉
// 把被点击的这个设置为active
$('#mode span').removeClass('active')
$(this).addClass('active')
if ($(this).text() == 'Circles') {
// 隐藏所有文本里面的svg元素
// 把node里面的显示出来
$('.texts text').hide();
$('.nodes circle').show();
}
else {
$('.texts text').show();
$('.nodes circle').hide ();
}
});
不同类型的实体有一个开关,决定一类实体节点是否显示:
// 处理开关1点击后的事件(这些元素页面上本来有)
$('#switch1 span').click(function(event) {
// 把mode里面所有span的active全部去掉
// 把被点击的这个设置为active
$('#switch1 span').removeClass('active')
$(this).addClass('active')
if ($(this).text() == 'On') {
sw1 = true;
d3.select('#svg1 .nodes').selectAll('circle').attr('class', function(d){
// 当前选中类型实体显示
if (d.group == 0 && sw1 == true) {
return '';
}
else if (d.group == 1 && sw2 == true){
return '';
}
else if (d.group == 2 && sw3 == true){
return '';
}
else{
return 'inactive'
}
});
当鼠标悬浮在某个实体节点上方时,实体的属性信息都能够显示出来,如果是精品特色菜类的实体,菜品图片等信息都能显示:
// 增加各个菜品的图片
if (typeof(info[name]) != "undefined") {
//avatar_ID = info[name]['ID'][0]
//if(outlier_avatar_ID.indexOf(avatar_ID) != -1) {
// avatar_ID = avatar_ID + '0'
//}
if ('主料' in info[name]){
$('#info').append('' + '' + '
');
}
}
for (var key in info[name]) {
value = info[name][key];
var flag_none = false;
for (var item in value) {
if (value[item] == null || value[item] == 'N/A' || value[item] == '') {
flag_none = true;
break;
}
}
if (flag_none == true) { // 排除为空的属性值
continue;
}
$('#info').append('' + key + '
');
var item_info = '';
count = 0
for (var food_item in info[name][key]){
if (count == 0){
item_info = item_info + info[name][key][food_item];
}
else{
item_info = item_info + " || " + info[name][key][food_item];
}
count = count + 1;
}
$('#info').append('' + item_info + '
');
}
设置搜索功能,按照搜索框中的关键词显示所有匹配到关键词的所有节点:
// 搜索框
$('#search input').keyup(function(event) {
// 如果输入为空,全部显示出来
if ($(this).val() == '') {
d3.select('#svg1 .texts').selectAll('text').attr('class', '');;
d3.select('#svg1 .nodes').selectAll('circle').attr('class', '');;
d3.select('#svg1 .links').selectAll('line').attr('class', '');;
d3.select("#svg1 .linetexts").selectAll('text').attr('fill-opacity', 0);
}
else { // 筛选,判断这三个东西里的元素是否包含输入的东西
var name = $(this).val();
d3.select('#svg1 .nodes').selectAll('circle').attr('class', function(d) {
if (d.id.toLowerCase().indexOf(name.toLowerCase()) >= 0) {
return '';
} else {
return 'inactive';
}
});
d3.select('#svg1 .texts').selectAll('text').attr('class', function(d) {
if (d.id.toLowerCase().indexOf(name.toLowerCase()) >= 0) {
return '';
} else {
return 'inactive';
}
});
d3.select("#svg1 .links").selectAll('line').attr('class', function(d) {
return 'inactive';
});
d3.select("#svg1 .linetexts").selectAll('text').attr('fill-opacity', 0);
}
});
具体一些比如页面设计的代码具体可以看github中的代码,如果有任何问题也可以与我交流讨论,希望这个工作可能帮助大家一起来做点有意思的小项目。
同时,如果对我们的文章感兴趣,欢迎关注知乎专栏“人工智能遇上知识图谱“,也欢迎关注”人工智能遇上知识图谱“微信公众号,我们会力求为您呈上知识图谱领域的精彩内容,让我们一起学习并交流讨论人工智能与知识图谱技术。