許多網路上的文章都只說明如何從動態的網誌系統轉到靜態網誌架構, 但當我們認定動靜之間各有優劣時, 能否兩者整合並存?

Worpress 是一套全世界最廣為使用的動態網誌系統, 採用 PHP 編寫, 一般使用 MySQL 資料庫存放資料, 由於 Wordpress 的安裝與使用都非常直覺, 從許多角度來看, 是一套接近完美的動態網誌系統.

但是, PHP 加上 MySQL 的架構仍然需要在伺服器中運行全球資訊網加上 PHP 解譯器, 同時還要 MySQL 資料庫的支援才能啟動運作, 假如管理者沒有跟上 Wordpress 程式碼或 plugin 的漏洞更新, 或者 MySQL 資料庫未能正常提供資料, 這個用 Wordpress 架構的網誌就會出現危機或者無法使用.

雖然 Wordpress 網誌中的動態程式與資料庫具有潛在缺點, 但是動態性也同時展現優點, 因為使用者一般可以透過瀏覽器, 隨時更改網誌系統的設定, 可以即時更新所有內容.

至於 Pellican 靜態網誌系統, 則針對 Wordpress 動態的問題, 將網誌的編寫格式定調在資料提供者能夠閱讀的 Markdown 或其他類似格式, 然後在近端用各種編輯器完成初步 Markdown 文章的存檔後, 再執行 Pelican 的轉檔指令, 讓 Python 程式將一堆設定與一堆 Markdown 資料中, 轉換成一整套所有內容之間互相串連的 html 檔案, 之後再將這批純 html 格式的文檔加上一些 css 與 Javascript 檔案, 送到全球資訊網伺服器中運行.

Pelican 的網誌內容, 因為不需要動態的程式編譯執行, 而只在全球資訊網伺服器中存有 html 與 Javascript, 因此沒有動態程式碼漏洞更新的問題, 也不會有線上的網誌管理系統被入侵的問題, 唯一會產生問題的只有全球資訊網伺服器, 運作或不運作, Javascript 有沒有正確存取的問題, 相較於 Wordpress 的動態程式與資料庫互動, 性質單純許多.

但是 Pelican 的靜態性也同時存在問題, 因為大多數的用戶通常採用文字編輯器來準備或管理 Markdown 檔案, 而且是採用命令列的方式來執行 Pelican 轉檔指令, 因此對於電腦程式操作較不熟悉的使用者來說, 導入 Pelican 靜態網誌的距離仍然遙遠, 況且靜態網誌也有不夠動態與直覺的問題, 許多在 Wordpress 線上能夠直接預覽的功能, 在 Pellican 就沒有那麼方便, 而且有很多的 html 與 css 的特定格式, Pelican 的 Markdown 語法根本就不支援.

可是 Pelican 純文本的 Markdown 與 html, 還有一個最大的好處, 就是可以透過 Github Pages 的網頁架構系統, 讓每一個版本的靜態網誌都以分散式版本系統管理, 這是 Wordpress 現存的版次管理 Plugin 所無法做到的功能, 因此一份內容, 同時呈現在 Wordpress 與 Pelican 網誌是最理想的情況, 但是該如何完成?

由於要整合 Pelican 靜態網誌與 Wordpress 動態網誌, 需要透過程式方法來進行, 這裡只先提供可行的初步技術, 驗證魚與熊掌可以兼得, 我們所採用的管理系統為 Leo Editor: https://github.com/leo-editor/leo-editor, 所有的資料處理流程都是依靠 Python3 程式完成.

首先, 看看 Leo Editor 如何透過 Python3 的程式方法與 Wordpress 網誌互動, 這裡所使用的是 XMLRPC 協定 https://en.wikipedia.org/wiki/XML-RPC, 基本上, 從 Leo Editor 節點, 將新資料送到 Wordpress 系統的按鈕程式.

# new_to_wp 按鍵
#coding: utf-8
import xmlrpc.client
import datetime
import os

def get_cat_tag_content(data):
    data_list = data.split("\n")
    # 只有一項資料的 category 型別為字串
    category = data_list[0]
    # 只有一項資料的 tags 型別為字串
    tags = data_list[1]
    # 有多項資料的 content 型別為數列
    # 再將第3行之後的資料數列串回成以跳行隔開的資料
    content = "\n".join(data_list[2:])
    return category, tags, content

os.environ['TZ'] = 'Asia/Taipei'

#################################
# Open a file, 這裡將存取 Wordpress 網誌的對應帳號與密碼, 存在操作系統中
# 路徑則從資料節點上層根節點的 body 內文取得
fo = open(p.parent().b, "r+")
data = []
for line in fo.readlines():
    data.append(line)
    #print(line)
#print(data[0])

# Close opend file
fo.close()

# 從網誌節點的 parent().h 取得 wp_url
# 從網誌節點的 parent().b 取得帳號與密碼對應的文字檔案路徑
wp_url = "https://"+p.parent().h+"/xmlrpc.php"
wp_username = data[0]
wp_password = data[1]
#################################
wp_blogid = "0"

status_draft = 0
status_published = 1

server = xmlrpc.client.ServerProxy(wp_url)

title = p.h
#content = p.b
category_str, tags_str, content = get_cat_tag_content(p.b)
# 指定時間出版
'''
date_created = xmlrpc.client.DateTime(datetime.datetime.strptime("2013-01-01 00:00",
"%Y-%m-%d %H:%M"))
'''
# 以現在時間出版, 考慮與 Server 時間差八個小時 (480 分鐘), 因此要在 8 個小時前發表 (因為伺服主機與操作端時差而定)
date_created = xmlrpc.client.DateTime(datetime.datetime.strptime((datetime.datetime.now()- \
datetime.timedelta(minutes=480)).strftime('%Y-%m-%d %H:%M'),"%Y-%m-%d %H:%M"))
#categories = ["Uncategorized"]
#tags = ["python", "測試"]
categories = [category_str.split(":")[1]]
# 請注意, 因為 tags 用逗點隔開, 因此必須透過 split() 再分開成為 list 資料
tags = tags_str.split(":")[1].split(",")
data = {'title': title, 'description': content, 'dateCreated': date_created, 'categories': categories, 'mt_keywords': tags}

post_id = server.metaWeblog.newPost(wp_blogid, wp_username, wp_password, data, status_published)
# 利用最後的 child 節點來儲存 post_id
to_save_post_id = p.insertAsLastChild()    
to_save_post_id.b = post_id
to_save_post_id.h = "文章 id"
# 因為新增節點, commander 必須 redraw
c.redraw() 

g.es("post_id 為", post_id)
g.es("已經送出資料!")
'''
其他 metaWeblog 的用法:
metaWeblog.newPost (blogid, username, password, struct, publish) returns string(postid)
metaWeblog.editPost (postid, username, password, struct, publish) returns true
metaWeblog.getPost (postid, username, password) returns struct(blog content)
'''


至於在 Leo Editor 系統中, 用來編輯既有的 Wordpress 文章的按鈕節點程式碼, 則為:

#edit_to_wp
#coding: utf-8
import xmlrpc.client
import datetime
import os

def get_cat_tag_content(data):
    data_list = data.split("\n")
    # 只有一項資料的 category 型別為字串
    category = data_list[0]
    # 只有一項資料的 tags 型別為字串
    tags = data_list[1]
    # 有多項資料的 content 型別為數列
    # 再將第3行之後的資料數列串回成以跳行隔開的資料
    content = "\n".join(data_list[2:])
    return category, tags, content

os.environ['TZ'] = 'Asia/Taipei'

#################################
# Open a file
fo = open(p.parent().b, "r+")
data = []
for line in fo.readlines():
    data.append(line)
    #print(line)
#print(data[0])

# Close opend file
fo.close()

# 從網誌節點的 parent().h 取得 wp_url
# 從網誌節點的 parent().b 取得帳號與密碼對應的文字檔案路徑
wp_url = "https://"+p.parent().h+"/xmlrpc.php"
wp_username = data[0]
wp_password = data[1]
#################################
wp_blogid = "0"

status_draft = 0
status_published = 1

server = xmlrpc.client.ServerProxy(wp_url)

title = p.h
#content = p.b
category_str, tags_str, content = get_cat_tag_content(p.b)
# 指定時間出版
'''
date_created = xmlrpc.client.DateTime(datetime.datetime.strptime("2013-01-01 00:00",
"%Y-%m-%d %H:%M"))
'''
# 以現在時間出版, 考慮與 Server 時間差八個小時 (480 分鐘), 因此要在 8 個小時前發表 (因為伺服主機與操作端時差而定)
date_created = xmlrpc.client.DateTime(datetime.datetime.strptime((datetime.datetime.now()- \
datetime.timedelta(minutes=480)).strftime('%Y-%m-%d %H:%M'),"%Y-%m-%d %H:%M"))
#categories = ["Uncategorized"]
#tags = ["python", "測試"]
categories = [category_str.split(":")[1]]
# 請注意, 因為 tags 用逗點隔開, 因此必須透過 split() 再分開成為 list 資料
tags = tags_str.split(":")[1].split(",")
data = {'title': title, 'description': content, 'dateCreated': date_created, 'categories': categories, 'mt_keywords': tags}

# 設法取得原 post 的 id
origin_post = p.getLastChild()
post_id = origin_post.b
status = server.metaWeblog.editPost(post_id, wp_username, wp_password, data, status_published)

if status:
    g.es("資料已經更新!")
else:
    g.es("有問題, 資料沒有更新!")
'''
其他 metaWeblog 的用法:
metaWeblog.newPost (blogid, username, password, struct, publish) returns string(postid)
metaWeblog.editPost (postid, username, password, struct, publish) returns true
metaWeblog.getPost (postid, username, password) returns struct(blog content)
'''


最後若要經由 Leo Editor 的節點按鈕, 取回既有的 Wordpress 網誌文章, 則可以使用 get_from_wp 按鈕節點:

#get_from_wp
#coding: utf-8
import xmlrpc.client
# 導入 html 模組, 使用 html.unescape 轉換 html 特殊符號
import html

#################################
# Open a file
fo = open(p.parent().b, "r+")
data = []
for line in fo.readlines():
    data.append(line)
fo.close()

# 從網誌節點的 parent().h 取得 wp_url
# 從網誌節點的 parent().b 取得帳號與密碼對應的文字檔案路徑
wp_url = "https://"+p.parent().h+"/xmlrpc.php"
wp_username = data[0]
wp_password = data[1]
#################################

server = xmlrpc.client.ServerProxy(wp_url)

# 設法透過上述網誌網址, 帳號與密碼, 以及文章 id, 取回 categories, tags, 文章標題, 文章內容等資

# 從最後的 child 節點來取 post_id
origin_post = p.getLastChild()
post_id = origin_post.b
# 取回與 post_id 對應的網誌文章內容
blog_content = server.metaWeblog.getPost (post_id, wp_username, wp_password)
title = blog_content["title"]
# 這裡要利用 html.unescape 轉回特殊符號
description = html.unescape(blog_content["description"])
mt_text_more = html.unescape(blog_content["mt_text_more"])
if mt_text_more != "":
    post_content = description + "\n\n"+mt_text_more
else:
    post_content = description
# 所取回的 categories 為 list
categories = blog_content["categories"]
# 索取回的 tags 為以逗點隔開的字串
mt_keywords = blog_content["mt_keywords"]
p.h = title
categories_str = ""
for category in categories:
    # 假如不是最後一個
    if category != categories[len(categories)-1]:
        categories_str += category + ", "
    else:
        # 這是最後一個
        categories_str += category
p.b = "categories:"+categories_str+"\ntags:"+mt_keywords+"\n"+post_content
# 因為節點資料更新, commander 必須 redraw
c.redraw() 
g.es("資料已經取回")


好了, 上面的3個 Leo Editor 的按鈕節點程式, 只是從操作系統的特定路徑取出能夠管理遠端 Wordpress 網誌系統的帳號密碼, 然後再透過 XML-RPC 協定, 進行 Wordpress 網誌文章的新增、編輯與取回, 表示使用者可以在一個 Leo Editor 專案檔中完成這些事, 接下來則需要讓 Pelican 轉出的 html 檔案, 能夠同步送到對應的 Wordpress 動態網誌系統, 並且可以做到即時的內容同步, 既可保有靜態網誌的版次管理與單純伺服架構, 而且又可以將 Wordpress 視為 Pelican 的另外一個出口 (意思就是說, Wordpress 中只有特定的內容由 Pelican 端提供, 其他的使用者則仍然透過瀏覽器的方法對 Wordpress 網誌提供內容), 至於後續的處理與可行性驗證, 將在隨後的文章中進行討論.


Comments

comments powered by Disqus