縱覽全局的博客文章組織模式-想法和基於Hexo的實現

歡迎訪問我的博客:TZLoop's Blog (zonelyn.com)javascript

轉載本文請註明原始出處。css

有人說螞蟻的世界是二維的(很是不許確),那是由於它們永遠不知道何爲高矮深淺。 由於它們感官的「無能」,導致它們喪失感覺世間萬物的機會。html

對知識系統(eg.博客)而言,良好的組織結構是極爲重要的,尤爲是當內容增多,關聯複雜後顯得尤其重要。傳統的「分類(Categories)+標籤(Tags)」的二級模式雖足以應付大部分用戶的需求,但本質上其仍是須要用戶對已有分類和標籤有良好的組織,這對不少用戶來講是根本作不到,由於咱們每每缺的就是這種「縱覽全局」的能力。java

分類每每越分越多,標籤也是隨意放置,長此以往,不只已有的分類和標籤雜亂無章,更爲甚者是新增內容時根本不知從何下手,每每須要遍歷過往的標籤和分類,才能作出最終定奪。如今,經過圖佈局的方式,能夠以一種近乎完美的方式對複雜的內容進行組織,詳細效果請查看 該頁面node

縱覽全局

對於知識系統(以後均以博客代指)而言,傳統的模式只是簡單的分支,或者稱其爲樹形結構,在探索過程當中,用戶就如同「螞蟻」同樣,只得選擇先從哪進入,而後再進入到哪裏。對於單篇內容而言並沒有影響,但當須要感知全局時,每每這種模式就會出現問題。算法

分級/樹形組織方式的不足

  • 用戶開始便直接但願查閱某些內容,且不肯定分類時,沒法定位(局部要求) 能夠經過搜索功能完成該需求。
  • 新增分類和標籤時,缺乏對已有項的感知能力(全局要求) 尤爲對於標籤,會更加的隨意和雜亂,會出現重複、同義等等問題,在每次打標籤時都要頭疼一番。
  • 對於所打的標記,沒有評價方法,永遠不知道分類和標籤是否匹配(全局要求) 對於已存在的標籤或分類,這樣打標籤是否合理,因爲標籤的「鬆散」特性,不一樣分類中能夠出現同一標籤,這樣在傳統分級模式下,分類和標籤的契合程度如何,系統的維護者無從知曉。

自然的解決方案:圖佈局

分級/樹形標記模式自己就是一個分類過程,本身的知識內容(博客文章)是對象,維護者將其放置在不一樣的類別下。**標籤(Tags)**則更像是分類過程當中的副產物,更貼近文章內容,但又言簡意賅,經過分級的思考方式,分類和標籤和文章的關係是:數據庫

分類-標籤-文章(1:M:N)編程

對於上述關係,分別用A、B、C表示的話,則整個系統其實就是一個「Ai-Bi-Ci」的三元組集合。該集合的好壞(即質量)就是其在語義上的契合程度,例如:json

分類:軍事 -> 標籤:爆炸 -> 文章:伊拉克遭遇恐怖襲擊
分類:娛樂 -> 標籤:爆炸 -> 文章:阿富汗遭遇恐怖襲擊

當抽象爲網絡/圖以後,軍事類別和娛樂類別會經過「爆炸」這一標籤相連,如是,明顯的會發現「爆炸」位置不對。(雖然例子很蠢,但當語義區分模糊、標籤數量繁多時,極易出現該狀況)。下面直接拿已完成的佈局來解釋:api

粉紅色爲分類、藍色爲標籤、節點半徑爲被使用的次數

  • 語義不符的鏈接點(異常的跨類標籤),若是鏈接點對某一方語義不匹配,那麼極可能該文章是特殊的,或者該標籤不該該出如今該文章。(下圖裏可視化的文章在這兒,屬於特殊文章,正常「生活分類」和「可視化」的語義並不匹配) [圖片上傳失敗...(image-3cbab4-1578849188423)]

  • 合格的鏈接點(跨分類的標籤):雖然標籤出如今不一樣分類中是很是正常的,例如「總結」,能夠出如今任何分類中。但相似「總結」這類標籤每每數量不少,即屢次的出如今不一樣的類別中,那咱們就說這是一個合格的跨分類標籤image.png

  • 對於分類點,以本博客爲例,因爲是對已存在數據進行分析,因此若是某分類下屬節點不足,那麼高度懷疑該分類不合理,除非是須要往後擴充的分類。這一需求在圖佈局的視圖下很是容易分辨出來,合格的類別應該有衆多葉節點,當葉節點不足,則應考慮將其降級至標籤。(例以下圖中的「樸素貝葉斯」,可將其降級爲標籤,並歸類到「研究方向」中) image.png

值得注意的一點是: 這裏使用的圖佈局使用力導向(Force-directed)佈局算法,相關則相近,無關則疏遠,又完美的給佈局結果以語義上的解釋,即:

  • 當兩個類別及其葉子節點距離很遠時,其二者基本無關
  • 當兩個類簇距離很近時,其高度相關

image.png

反推設計

上節中的分析看似頗有道理,佈局結果的使用也很是方便,那麼如何從無到有將其構建出來?主要有如下幾個方面:

  • 自然的三元組集合:文章的特性(篇幅長)決定了其不能參與整個構建和評價過程,那麼剩下的二元組是自然的「關係數據」,對於關係數據的可視化,圖佈局算法/模式首當其衝。
  • **分析須要呈現的維度:**對於任意節點(佈局時類別和標籤並沒有分別)來講,主要有如下維度信息:
  1. 本身(若是是類別)包含哪些標籤;
  2. 本身是什麼類型的節點(類別?標籤?);
  3. 本身被使用了幾回;
  • 對應的可視化要素: a. 圖中節點的鄰節點(點、線) b. 類別爲粉色標籤爲藍色(顏色) c. 次數與節點的半徑成比例(圓面積)
  • 還能夠附着信息(擴展維度)的要素:
  1. 節點的形狀(三角形、圓、方)
  2. 連線的顏色(紅、藍)
  3. 連線的線型(虛線、實線)

上述過程當中,肯定**「圖佈局」模式是基礎,剩下的無非是將信息綁定到可視化元素上**,例如,已實現的佈局將「類別/標籤」用顏色區分,其實用形狀等其餘可視化元素區分也徹底能夠。

垂直打擊

到此爲止,只是上層結構,相似數據庫存儲,搞了半天只是在搞索引,並無觸碰到數據,因此目前爲止該網絡並無直通最底層(文章內容)的能力,這個問題剛好被Hexo的文件結構所解決,Hexo給每一個標籤和每一個分類都渲染了單獨的頁面,關聯的文章被放置在頁面中,在此,直接經過節點的文本信息構造訪問地址,將其綁定到文本上,便可點擊後跳轉到相關頁面,雖然不是直接跳轉文章,但也能夠說具有至關的垂直打擊能力了。

進階版本:變的更強

簡單粗暴的加入以前三元組被拋棄掉的文章信息,但因爲加入後過於散亂,因此有必要將文章信息固定,以便於視覺呈現。以下圖(d3.js實現的、用於可視化編程概念的可視化模型):

image.png

上圖就是簡單的帶固定節點的力導向佈局,但其實現代碼比較複雜,目前處在構造數據階段。通常的可視化模型套用的步驟:

閱讀原站代碼 -> 從原站抽離可視化部分 -> 搞清調用數據的方法及格式 -> 構造一樣的數據 -> 獨立運行 -> 放回本身的站點內

問題迎刃而解

到此,對於分級/樹形分類的三點不足,能夠發現很輕鬆就能夠解決。既有全局視角,又能夠同時具有直達的能力,對於組織內容數量較高(超過50)的站點很是適合該模式的導航、或輔助探索。

image.png

下文開始,詳細記錄瞭如何在Hexo博客中實現用圖組織內容的方法,可是,請注意:如下內容並不是操做教程,僅代表相信思路以供參考,或許您能夠實現出更好的版本,但僅依照下文內容並不保證必定能重現,一些嘗試和debug的細節過於繁瑣並未列出,若有疑問歡迎留言。

代碼實現

hexo.extend.helper.register

文檔說明,藉助該函數,能夠在Hexo渲染生成頁面文件以前,完成用戶的自定義JavaScript代碼。

其實,在Hexo的框架內,ejs(或其餘類型的)模板中的代碼就是渲染生成html的代碼,在這些頁面中,藉助Hexo內建的對象,好比.post對象和.achieves對象,能夠訪問到其中保存的所有文章信息及關聯信息。例如:

let posts = hexo.locals.get('posts');
let Xtags = posts.data[x].tags
let tagsY = Xtags.data[y].name

上述內容,能夠最終獲得第X篇文章(POST)中的第Y個標籤的文本。相似的方法一樣能夠獲得某篇文章的Categories的信息。這就是構造可視化數據的基本方法。(在渲染前構造、藉助.post對象) 關於位置,在ejs模板中放置構造代碼固然能夠,可是不優雅,Hexo中建議的插入方式是:

  1. 在專門放置自定義JavaScript處理邏輯的文件中(plugin.js)放入代碼,並使用內建函數。
  2. 在ejs(或其餘)模板的相關位置,使用<%%>方式調用上述內建函數
  3. 使用console.log在渲染html時(hexo generate時的黑框)輸出至Console裏,拿到輸出數據,放入到可視化的頁面中便可。
  4. 或者一鼓作氣,直接將可視化的代碼寫入ejs模板中,即第一次渲染結束時產生的html就已經完成可視化頁面的生成。

因爲處在嘗試階段,因此這裏使用步驟3 的方法,這樣各模塊相對獨立,對主題源代碼入侵小。

可視化頁面

這裏採用的是 D3.js 進行的可視化呈現,基本上是複用的 d3 的官方模板,但將文本信息一併和節點進行可視化展現。這段代碼首先須要被抽取出來,這對於 d3 來講很是簡單,只需注意引入的JavaScript庫以及使用的json文本數據。

<svg width="1000" height="1000"></svg> //d3繪製的內容所有放置在該畫布上
<script src="https://d3js.org/d3.v4.min.js"></script> 
<script>

  var sss = 'JSON字符串'; //這就是整個代碼所可視化的數據

  var abc = parseInt($(".card").css("width").replace("px",""));
  if(abc>1080) abc=1050;
  else if(abc>1040) abc=1020;
  else abc=abc-40;
  $("svg").css("width",abc);
  $("svg").css("height",abc); //此部分將畫布大小跟隨文章頁寬度變化

  var svg = d3.select("svg"),
    width = abc,
    height = abc;

  var color = d3.scaleOrdinal(d3.schemeCategory20);

  var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }))
      .force("charge", d3.forceManyBody().strength(-180).distanceMin(10).distanceMax(300).theta(1))
      .force("center", d3.forceCenter(width / 2 - 40, height / 2 - 30));

  var graph = JSON.parse(sss);

  var link = svg.append("g")
      .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
      .attr("stroke-width", function(d) { return Math.sqrt(d.value); });

  var node = svg.append("g")
      .attr("class", "nodes")
    .selectAll("g")
    .data(graph.nodes)
    .enter().append("g")
    
  var circles = node.append("circle")
      .attr("r", function(d) { 
			if(d.group>=100) return d.group/100*(10.00/48.00)+1; //取整
			else return d.group+1;
		})
      .attr("fill",  function(d) { 
			if(d.group>=100) return "#ff4081";
			else return "#3f51b5";
		})
      .call(d3.drag()
          .on("start", dragstarted)
          .on("drag", dragged)
          .on("end", dragended));

  var lables = node.append("text")
      .html(function(d) {
		if(d.group>=100) {
			var p = d.group/100*(10.00/48.00)+10;
			return "<a style='font-size:"+p+"px;font-weight:600;color:red' href='/categories/"+d.id.replace("_","-")+"'>"+d.id+"</a>";
		}else{
			var q = d.group+10;
			return "<a style='font-size:"+q+"px;' href='/tags/"+d.id+"'>"+d.id+"</a>";
			}
      })
      .attr('x', function(d) { 
			if(d.group>=100) return d.group/100*(10.00/48.00)+5; //取整
			else return d.group+3;
		})
      .attr('y',function(d) { 
			if(d.group>=100) return d.group/100*(3.00/48.00)+5; //取整
			else return 5;
		});

  node.append("title")
      .text(function(d) { return d.id; });

  simulation
      .nodes(graph.nodes)
      .on("tick", ticked);

  simulation.force("link")
      .links(graph.links);

  function ticked() {
    link
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node
        .attr("transform", function(d) {
          return "translate(" + d.x + "," + d.y + ")";
        })
  }

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}
function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}
function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
</script>

構造數據格式

須要匹配示例的輸入格式,這樣才能最大化的複用代碼。上述內容的官方示例中使用的格式是:

{
  "nodes": [
    {"id": "Myriel", "group": 1},
    ... ...
    {"id": "Mme.Hucheloup", "group": 8}
  ],
  "links": [
    {"source": "Napoleon", "target": "Myriel", "value": 1},
    ... ...
    {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1}
  ]
}

即,須要在可視化頁面被渲染出來以前就獲得上述格式的數據,這即是要藉助Hexo的輔助函數來完成,將構造數據的代碼封裝成一個函數,而後在適當的ejs模板中調用一下,便可在 hexo generate 以後,從Console中拿到構造好的數據。

在此,構造規則是:類別永遠單向的指向標籤,類別不互連,標籤不互連,同時,還須要計算的是類別和標籤出現的次數

hexo.extend.helper.register('getPostData', () => {

	var posts = hexo.locals.get('posts');
	var tagsMap = new Map(); //counter

    // 利用posts對象獲取類名和標籤名
	for(var i = 0; i< posts.length; i++){
		var nameCS;
		posts.data[i].categories.forEach(function(k, v) {
			nameCS = k.name;
			return;
		})
		for(var j = 0; j< posts.data[i].tags.length; j++){
			var pname = posts.data[i].tags.data[j].name;
			var pval = tagsMap.get(pname);
			if(pval != null){  
				// 將類名和標籤名壓制在一塊兒
				tagsMap.set(nameCS+">"+pname, parseInt(tagsMap.get(pname))+1);
			}else{
				// 
				tagsMap.set(nameCS+">"+pname, 1);  
			}
		}
	}
	//由此開始,構造符合特定格式的JSON字符串  
	let obj= [];
	let setss =  new Map();
	for (let[k,v] of tagsMap) {
	    var st = k.split(">");
	    var str = {};
	    str.source = st[0];
	    str.target = st[1];
	    str.value  = v;
		obj.push(str);
		if(setss.get(st[0]) != null){  
			// 類節點 每次加100
			setss.set(st[0], parseInt(setss.get(st[0]))+100);
		}else{
			//
			setss.set(st[0], 100);
		}
		if(setss.get(st[1].trim()) != null){  
			// 標籤節點 每次加1
			setss.set(st[1], parseInt(setss.get(st[1]))+1);
			setss.set(st[0], parseInt(setss.get(st[0]))+100);
		}else{
			// 
			setss.set(st[1], 1);
			setss.set(st[0], parseInt(setss.get(st[0]))+100);
		}
	}
	
	let obk= [];
	for (let [k,v] of setss) {	 
	   var str = {};
	   str.id = k.trim();
	   str.group = v; //經過數量分類
	   obk.push(str);
	}
	let d3str = {};
	d3str.nodes = obk;
	d3str.links = obj;
	console.log(JSON.stringify(d3str).trim()); //按第三步說的,能夠手動放置數據到可視化頁面
	return JSON.stringify(d3str).trim(); //或按第四步,將數據返回至ejs模板中,直接渲染出可視化頁面
 
});

注意上述代碼中的註釋,這裏利用了類節點和標籤節點出現的次數,來分辨兩種節點的種類,由於繪製時類節點和標籤節點都是一視同仁的被繪製。如何分辨呢?在可視化頁面中有如下代碼:

var circles = node.append("circle")
  .attr("r", function(d) { 
		if(d.group>=100) return d.group/100*(10.00/48.00)+1; //取整
		else return d.group+1;
	})

按照不一樣的次數計算步長,獲得的類節點的次數必定是100的倍數,而標籤節點的次數必定小於100,這個值能夠設的很大,從而讓二者不可能出現交集。在判斷時「若是次數大於100」,那麼就是類節點,取整百的好處是,歸一化方便。例如上述代碼須要給定節點的大小,類節點的次數統計多是100-4800(1-48次),而標籤節點的次數倒是1-10(1-10次),如是,二者應繪製的同樣大。這就須要歸一化,只須要縮放100倍再乘比例係數便可。

最終調用

上文中**hexo.extend.helper.register('getPostData', () => {})**的「getPostData」即註冊的函數名,在ejs(或其餘)模板中直接調用便可。但因爲我但願把這個可視化模塊放在個人評論頁或者關於頁面,而這兩個頁面都不是渲染出來的,因此就只能採用先前第三步的作法,只構造出數據,再手動放入可視化頁面。

// 在 index.ejs 內添加:
<% var arr = getPostData(); %>

因此,須要作的就是找一個渲染頁面的ejs,調用下該函數便可,這裏放在index.ejs裏,注意因爲分頁可能該模板會構造不少次,因此就會重複輸出不少遍JSON數據。

image.png

總結

基本上仍是抓住代碼執行的輸入輸出作文章。從待改造代碼的輸入找格式,而後從原代碼的框架中構造出該格式的數據(輸出),就像適配同樣,如此即可以利用Hexo能夠得到的數據,藉助D3.js等可視化庫,把本身的博客(知識系統)作一個梳理和呈現,從而更好的幫助本身管理維護,也給了本身二次挖掘本身知識的機會。 image.png

最終效果傳送門,請用PC查看


本文做者:TZLoop 我的博客:TZLoop's Blog (zonelyn.com) 轉載本文請註明原始出處。

相關文章
相關標籤/搜索