over 5 years ago

看到很多用 command line 安裝的程式,都會有類似 "#####"、"..." 或是 "[===]" 的 progress bar,所以想說來研究一下怎麼在 C 的程式實現這個進度條的顯示,程式不長,直接貼上來解說。

#include<stdio.h>

int main(void){
   int i = 0, j = 0;
   for(i = 0; i <= 10000; i++){      // 這是用來控制進度的
      for(j = 0; j < 10000; j++)     // 這是用來控制速度的
      {  }
      printf("\rIn progress %%%d[", i/100);     // 輸出 percentage,\r 是 return (回到行首)
      for(j = 0; j < (i/100); j++)
         printf("=");           // 輸出 "="
      printf("]");
      fflush(stdout);           // 清除螢幕(standard output)
   }
   printf("\n");
   return 0;
}

Note:雖然使用了 \r 和 fflush(stdout); 來刷新螢幕,但是如果單一行的輸出超出螢幕最右方可以顯示的範圍,好像會因為 overflow 而造成 fflush(stdout) 失效,重複填滿整個螢幕。

Demo Video: http://youtu.be/R7LRsSCmSuo

 
over 5 years ago


Rove.io 這個網站可以讓你客製化你要的環境,然後幫你產生 Vagrantfile 和 Cheffile (需要另外預先安裝好 library-chef:gem install library-chef),下載後解壓縮進入資料夾,輸入 curl -L http://rove.io/ | bash,就會自動下載 box 並安裝好你客製化的軟體 (應該跟 Docker 一樣是堆疊方式),vagrant ssh 就可以連入測試了


After Generate:

After curl -L install http://revo.io | bash finished:
NOTE:基本上過程沒有錯誤的話,curl 結束後就會完成安裝,就可以直接 vagrant ssh 進去了


但是如果中途有斷掉過,你可能需要手動執行套件下載和安裝
librarian-chef install

NOTE: 此時,因為 curl 已經(或可能)讓 vm 已經 run 起來,因此需要先 vagrant halt 才能安裝客製化的應用程式

接著要 vagrant provision

這個動作才會真的 Setting the run_list to ["recipe[apt]", "recipe[vim]", "recipe[git]", "recipe[apache2]", "recipe[mysql::server]"] from JSON,將 chef cookbook 設定且下載的應用程式裝載 vm 中

最後 vagrant up,然後 vagrant ssh 後:

  • curl 時遇到問題:
    /usr/local/Cellar/ruby/2.1.4/lib/ruby/2.1.0/net/http.rb:920:in `connect': SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (OpenSSL::SSL::SSLError)
    

花了很久時間嘗試,終於解決了!!!!!!!!
因為我是在 Mac 上安裝 Vagrant
原來在 Mac 上要這樣指定 SSL_CERT_FILE:export SSL_CERT_FILE='/usr/local/etc/openssl/osx_cert.pem'
不是指定到 /usr/local/etc/certs/cacert.pem
(osx_cert.pem 是另外下載安裝的)

  • vagrant up 遇到的問題:
    Vagrant cannot forward the specified ports on this VM, since they
    would collide with some other application that is already listening
    on these ports. The forwarded port to 3000 is already in use
    on the host machine.
    
    
    

    To fix this, modify your current projects Vagrantfile to use another
    port. Example, where '1234' would be replaced by a unique host port:

    config.vm.network :forwarded_port, guest: 3000, host: 1234

    Sometimes, Vagrant will attempt to auto-correct this for you. In this
    case, Vagrant was unable to. This is usually because the guest machine
    is in a state which doesn't allow modifying port forwarding.



    這表示你有沒關掉的 vargant instance,導致預設要綁定的 port 已經被某個 process 佔用了,因此請先用 ps aux | grep vm 來看:
    veck            78729   1.1  5.6  3000248 471340   ??  U     4:45AM   2:08.39 /Applications/VirtualBox.app/Contents/MacOS/VBoxHeadless --comment rove-27c8af801d4c671e25f2791b7311bbe0_default_1414614223406_26774 --startvm 541cafa7-5bb3-45d7-bd27-899a824a3e74 --vrde config
    

    再用 kill -9 [PID] 砍掉 Process,然後重新 vagrant up 就可以了

 
over 5 years ago

最早是從 LISP 的 quote( ' ), quasiquote( ` ), unquote( , ) 演變而來
quote 的意思是避免 expression 被 evaluate (求值)
例如原本寫 (+ 1 2) 會得到 3,因為直譯器會將 + 當成 function (事實上是將 +, 1, 2 都當成函數去求值)
如果寫成 '(+ 1 2) 會得到 (+ 1 2),也就是這個敘述變成了 symbol
更進一步說:

'1      => 1         ;本來就是符號
'a      => a         ;a 變成符號,不會當成變數求值
'"hi"   => "hi"      ;"" 和 hi 變成符號,不會被求值為字串

然後下面兩個一起說:

'(1 2 3) => (1 2 3) 
'(+ 1 2) => (+ 1 2)

事實上,'(1 2 3) 的概念是 (list '1 (list '2 (list '3))'(+ 1 2) 則是 (list '+ (list '1 (list '2)))
但是有時候,我們並不想針對整個敘述都當成 list symbol,例如 '(1 2 (+ 3 4)) 會得到 (1 2 (+ 3 4))
假如我們希望保留 1 和 2,然後運算 (+ 3 4),就可以利用『 ` 』取代『 ' 』,這個符號允許我們設定 quote 的運算中止點,用『 , 』指定之
例如:

`(1 2 ,(+ 3 4))    => '(1 2 7)

就是指定 (+ 3 4) 以後就不是 quote 運算,但後面運算完以後的結果,還是會變成 quote 的元素之一,因此 (1 2 7) 仍是一個 quote 運算產生的 symbol

Reference: http://courses.cs.washington.edu/courses/cse341/04wi/lectures/14-scheme-quote.html

 
over 5 years ago

Website: http://walac.github.io/pyusb/
GitHub: https://github.com/walac/pyusb/

目前教學只有教在 Ubuntu 和 Windows 上安裝的方法,我以 Ubuntu 為例,首先需要先有 python (應該是 builtin 了),然後要安裝 libusb-1.0-0

$ sudo apt-get install libusb-1.0-0

然後下載 library,解壓縮以後,進入資料夾,輸入 sudo python setup install,就完成安裝了

簡單的測試 USB 連線,你需要先知道各個 USB 裝置的 Vendor IDProduct ID,可以用 lsusb 來查看:

~/Desktop$ lsusb
Bus 002 Device 003: ID 046d:c016 Logitech, Inc. Optical Wheel Mouse
Bus 002 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 024: ID 0b05:5481 ASUSTek Computer, Inc. 
Bus 001 Device 002: ID 8087:0020 Intel Corp. Integrated Rate Matching Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

以第一個裝置 Logitech, Inc. Optical Wheel Mouse 為例,在 ID 後面的 046d:c016 分別對應到 Vendor ID 和 Product ID,接著我們進入 interactive shell,首先要 import usb,接著我們利用 usb 模組下的 core 類別的 find 方法,把剛才的兩個 id 給它 dev = usb.core.find(idVendor=0x046d, idProduct=0xc016)

>>> import usb
>>> dev = usb.core.find(idVendor=0x046d, idProduct=0xc016)

然後我們就可以印出 dev 的資訊,看看是不是有抓到我們指定的 USB 裝置:

>>> dev
<DEVICE ID 046d:c016 on Bus 002 Address 003>

 
over 5 years ago

Source Code: https://github.com/isnowfy/python-websocket
Tutorial: http://www.isnowfy.com/python-websocket-server/

Project 中的 index.html 就扮演著 Client 的角色,用瀏覽器執行後,他會發 request 給 server,其中主要建立 WebSocket 連線的 JavaScript 片段如下:

  function openConnection() {
      conn = new WebSocket('ws://localhost:7000/');   // 建立連線
      conn.onopen = function () {
        alert("open");
        conn.send("hello");
      };

      conn.onmessage = function (event) {
        alert(event.data);
        /* TODO: 做連線建立後想做的事情 */
      };

      conn.onclose = function (event) {
      };

      conn.onerror = function(event){
        alert(event.data);
      }
    }    

  setTimeout(function () {
    openConnection();
  }, 1000);

onopen, onclose, onerror 這三個方法比較直觀,就是在連線建立、連線關閉和連線出錯時,要執行的內容
onmessage 則是在連線成功建立以後要做的事情

而 server 就是 ws.py,使用 terminal 開啟以後,使用 python ws.py,就會去聽指定的 port (作者這邊預設為 7000),在 ws.py 中的 main 裡面的 select(socket_list, [], []) 會監看 socket_list 中的 socket 變化,一旦 client 端發出 request,server 端接收到以後,select 會知道,並回傳這個 socket,此 socket 會被丟入 WebSocket.handshake 中去解析認證,接著就 response 連線資訊給 client,如此就建立起連線了:

def main(handle=process):
    port = 7000     # 定義要監聽的 port
    try:
        server.bind(('', port))     # 綁定服務
        server.listen(100)          # 設定最大監聽連線數目 (可能有多個連線同時來,排隊等著回應)
    except Exception, e:
        print e
        exit()
    socket_list.add(server)         # 將新的監聽物件加入監聽列表中
    print 'server start on port %d' % port
    while True:                     # 開始監聽
        r, w, e = select(socket_list, [], [])   # 利用 select 來監控監聽物件的變化
        for sock in r:
            if sock == server:
                conn, addr = sock.accept()      # 取出物件連線資訊,包含 socket 物件和 client 位址
                if WebSocket.handshake(conn):   # 處理連線資訊
                    socket_list.add(conn)       # 保持 socket 監聽 (即保持連線)
            else:                               # 例外處理
                data = WebSocket.recv(sock)
                if not data:
                    socket_list.remove(sock)
                else:
                    handle(sock, data)

Server 接收到 HTTP request 後,會取出 Sec-WebSocket-Key 的值,並加上一串 magic string 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,接著使用 SHA-1 加密後在進行 BASE-64 編碼,最後將結果放到 response 封包中的 Sec-WebSocket-Accept,返回給 client,如此便完成連線,這就是 WebSocket 基本的連線建立原理,因此在 handshake 中會是如此:

def handshake(conn):
    key = None 
    data = conn.recv(8192)      # 從收到 request 的 socket 中取得資料,8192 是 buffer size
    if not len(data):
        return False
    for line in data.split('\r\n\r\n')[0].split('\r\n')[1:]:
        k, v = line.split(': ')     # 取出 Sec-WebSocket-Key
        if k == 'Sec-WebSocket-Key':
            key = base64.b64encode(hashlib.sha1(v + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest())
    if not key:
        conn.close()
        return False
    response = 'HTTP/1.1 101 Switching Protocols\r\n'\
               'Upgrade: websocket\r\n'\
               'Connection: Upgrade\r\n'\
               'Sec-WebSocket-Accept:' + key + '\r\n\r\n'
    conn.send(response)
    return True

Class WebSocket 中的 recv 和 send,以及全域方法 sendall 和 process 是在例外處理時會被呼叫的,這部份要自己客製化你要的處理方式

補充:
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 這個魔術字串,在 RFC 文件中有說明,它是一個 Globally Unique Identifier,詳可見 RFC6455(WebSocket) 與 RFC4122(Universal Unique Identifier)

 
over 5 years ago

寫在 TSOC 結束後...

研究所之前,就知道 open source 這個圈子,但是一直以為開源是指開發者讓大家取得程式的原始碼,客製化成自己的程式,如此而已,說來慚愧,2013 年畢業時,jserv 曾經公開徵求 f9 microkernel 的參與人,我當時期待可以有機會參與,然而等了一個暑假,都沒有等到進一步的通知,後來在 2013 年的 COSCUP (我第一次參與技術年會)上聽 jserv 的介紹 f9 專案,才驚覺原來 Open Source 的參與模式是主動的,知道自己學了很久的程式設計,其實可以實際參與專案貢獻自己的能力的,直覺已經比別人晚起步

進入研究所後,因為實驗室的幾位學長與前輩都是 Open source 領域參與者,有在 Mozilla 工作的學長、Google 社群活躍的前輩、資策會服役的學長、寫程式創業的前輩,跟我大學所認識的同學差很多,高手雲集,指導教授也很鼓勵我參與社群(例如 g0v)與學習 Open source 相關技術,所以我開始學習使用 GitHub、API,並且嘗試發表一些技術的學習心得或筆記到 Blog 上,廣泛吸收與接觸各種軟體

我第一個自己貢獻的 Open Source,是碩一時因為老師請我學習使用 Twitter API,我找到一份由 MIT 研究生開發的一個專案,但是因為 API 演進的關係,已經有需要修正的部分,所以在嘗試修正使用正確的 GitHub 指令,並與專案 owner 討論之下,成功貢獻了自己的程式碼 (Merged!)

我對於系統軟體很有學習熱誠,大學認真修習系統程式和作業系統,在知道 fork() 的原理以後,突然領悟一個 shell 是怎麼做出來的,開始想設計 OS、研究 Linux Kernel,後來在升研究所的暑假接觸到了 Firefox OS (B2G)這個作業系統專案,對其技術覺得很新鮮,透過在 Mozilla 的學長很熱心的帶領我參與 Firefox OS 解 Bug 行列下,利用課餘與實驗室事務之外陸續學習怎麼使用 Bugzilla、GitHub、Travis CI 等平台與工具來與世界各地的工程師互動,並成功貢獻了一些程式碼,今年暑假,學長建議我可以參與 TSOC 由 Moziila 提出的專案,因此與 Moziila 負責此專案的兩位導師每週利用線上討論進度,並使用 Bugzilla 與其他開發者討論解決方案,研究本專案使用的單元測試工具 karma 與相關測試工具 mocha、sinon 各自的程式碼和運作原理,逐步做出本專案希望的實驗性目標,並且對單元測試有更進一步的學習,也對參與軟體專案有了更豐富的經驗。

在研究所的這一年,我也參加了 COSCUP、SITCON Hackgen、PyCon、JuluDev、HITCON...等聚會、活動與討論,見識到了在技術圈子有很高造詣的先進,也會參加了系統軟體相關的課程和嘗試使用各種軟體,很感謝許多前輩的指導與肯定,我未來會繼續學習計算機科學與磨練技術。

 
over 5 years ago

Import Commonly(from v0.20.2)

import java.io.*;
import java.util.*;

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.conf.*;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.*;           //it's "mapred" before
import org.apache.hadoop.util.*;

Methods Defined

class MR_JOB_NAME implements Tool{
    public static class Map extends Mapper<KEYIN_TYPE, VALUEIN_TYPE, KEYOUT_TYPE, VALUEOUT_TYPE>{}
  public static class Reduce extends Reducer<KEYIN_TYPE, VALUEIN_TYPE, KEYOUT_TYPE, VALUEOUT_TYPE>{}
  public int run(String[] args) throws Exeption{}
  public static void main(String[] args) throws Exxception{}
}

其中 Mapper Class 中會定義:
public void map(KEYIN_VAR, VALUEIN_VAR, Context context) throws IOException, InterrupedException{ ... }

Reduce Class 中會定義:

run() 中去用 Job 來設定一些參數,如果有多個 job 的 MR Class,這裡會定義執行的 Jobs 先後順序

main() 使用 ToolRunner 的 run() 方法來驅動 run(),其中 ToolRunner.run(new MR_JOB_NAME(), args) 可以得知,這個 run() 方法應該不是上方定義的 run() 方法,而是會建立一個 MR_JOB_NAME 物件,並利用該物件和所給予的參數 args 去呼叫上方定義的 run() 來執行 MapRuduce

  • Hadoop 採用 Writable 的資料型態是因為它將資料 Serialize
  • Context 是用來給 MapReduce Process 間溝通用的資料型態
 
over 5 years ago

以 Mapper 來說

 public class TokenCounterMapper 
     extends Mapper<Object, Text, Text, IntWritable>{
    
   private final static IntWritable one = new IntWritable(1);
   private Text word = new Text();
   
   public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
     StringTokenizer itr = new StringTokenizer(value.toString());
     while (itr.hasMoreTokens()) {
       word.set(itr.nextToken());
       context.write(word, one);
     }
   }
 }

<Obejct, Text, Text, IntWritable> 分別對應到 KEYIN, VALUEIN, KEYOUT, VALUEOUT
而在 mapper 函數中使用到的部份:

  • public void map(Object key, Text value, Context context) 的頭兩個參數就是符合 KEYIN 和 VALUEIN 的部分
  • context.write(word, one) 的 word 和 one 就分別對應到 KEYOUT 和 VALUEOUT

以 Reducer 來說

 public class IntSumReducer<Key> extends Reducer<Key,IntWritable, Key,IntWritable>{
     private IntWritable result = new IntWritable();
   public void reduce(Key key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
     int sum = 0;
     for (IntWritable val : values) {
       sum += val.get();
     }
     result.set(sum);
     context.write(key, result);
   }
 }

<Key, IntWritable, Key, IntWritable> 分別對應到 KEYIN, VALUEIN, KEYOUT, VALUEOUT
而在 reduce 函數中使用到的部份:

  • public void reduce(Key key, Interable values, Context context) 的頭兩個參數就是符合 KEYIN 和 VALUEIN 的部分
  • context.write(key, result) 的 key 和 result 就分別對應到 KEYOUT 和 VALUEOUT

Reference:

  1. MapReduce:并行计算框架
  2. Hadoop入门实践之类型与格式
 
over 5 years ago

翻譯自原文:http://codingjunkie.net/cooccurrence/

本文是《Data-Intensive Text Processing with MapReduce》提到的 MapReduce 演算法的系列文章的延續。這次我們會使用語料庫建立一個單字共現矩陣。

所謂共現矩陣可以看成是對某件特定事件的追蹤,並給予一段時間或空間窗口(windows,就像作業系統中的排程那樣),然後記錄還有其他哪些事件也發生了。本文中,我們的『事件(events)』都是文本中個別的『詞(words)』,而我們要追蹤看看(對於某個目標詞)其他哪些詞也在我們設定的窗口條件中出現,我們設定的窗口條件是相對於目標詞彙的位置,例如來看這句話:『The quick brown fox jumped over the lazy dog』,窗口值設為 2,則『jumped』的共現詞是『brown, fox, over, the』。

一個共現矩陣可以應用在很多其他需要找出『當某個事件發生,其他事件似乎也會同時發生』的領域,為了要建構我們的文本共現矩陣,我們需要實作 《Data-Intensive Test Processing with MapReduce》 第三章的 Pairs 和 Stripes 演算法,配合 MapReduce。本文用來建立我們的共現矩陣的文本資料來自於古騰保計畫的William Shakespear

Pairs 演算法

實作 pairs 演算法很簡單。當每次 map 函數被呼叫時傳入一行,便按照空白把傳入的行切割成字串陣列。接著是建立兩層迴圈。外層迴圈迭代陣列中的每個詞,内層迴圈走訪目前這個詞的相鄰詞。内層迴圈的迭代次數取決於需要捕捉的目前詞彙的相鄰距離。在内層迴圈的每次迭代的結束前,我們輸出一個 WordPair 物件(由兩個詞組成,目前的詞在左邊,相鄰詞在右邊)當做 key,這組詞的出現頻率 (one 的次數)當做 value。

以下是 pairs 演算法的程式碼:

public class PairsOccurrenceMapper extends Mapper<LongWritable, Text, WordPair, IntWritable> {
    private WordPair wordPair = new WordPair();
    private IntWritable ONE = new IntWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        int neighbors = context.getConfiguration().getInt("neighbors", 2);
        String[] tokens = value.toString().split("\\s+");
        if (tokens.length > 1) {
          for (int i = 0; i < tokens.length; i++) {
              wordPair.setWord(tokens[i]);

             int start = (i - neighbors < 0) ? 0 : i - neighbors;
             int end = (i + neighbors >= tokens.length) ? tokens.length - 1 : i + neighbors;
              for (int j = start; j <= end; j++) {
                  if (j == i) continue;
                   wordPair.setNeighbor(tokens[j]);
                   context.write(wordPair, ONE);
              }
          }
      }
  }
}

Pairs 演算法中的 Reducer 只是單純的將當做 key 的同一 WordPair 的計數總和:

public class PairsReducer extends Reducer<WordPair,IntWritable,WordPair,IntWritable> {
    private IntWritable totalCount = new IntWritable();
    @Override
    protected void reduce(WordPair key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int count = 0;
        for (IntWritable value : values) {
             count += value.get();
        }
        totalCount.set(count);
        context.write(key,totalCount);
    }
}

Stripes算法

共現矩陣中的 stripes 演算法實踐一樣很簡單。不過跟 pairs 演算法不同的是,每個詞的所有相鄰詞被存在一個 hashmap 中,以此相鄰詞 key,詞的出現頻率為 value。當迴圈走訪完每個詞的所有相鄰詞後,這個詞和跟他有關的 hashmap 會被輸出。
以下是 stripes 演算法的程式碼:

public class StripesOccurrenceMapper extends Mapper<LongWritable,Text,Text,MapWritable> {
  private MapWritable occurrenceMap = new MapWritable();
  private Text word = new Text();

  @Override
 protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
   int neighbors = context.getConfiguration().getInt("neighbors", 2);
   String[] tokens = value.toString().split("\\s+");
   if (tokens.length > 1) {
      for (int i = 0; i < tokens.length; i++) {
          word.set(tokens[i]);
          occurrenceMap.clear();

          int start = (i - neighbors < 0) ? 0 : i - neighbors;
          int end = (i + neighbors >= tokens.length) ? tokens.length - 1 : i + neighbors;
           for (int j = start; j <= end; j++) {
                if (j == i) continue;
                Text neighbor = new Text(tokens[j]);
                if(occurrenceMap.containsKey(neighbor)){
                   IntWritable count = (IntWritable)occurrenceMap.get(neighbor);
                   count.set(count.get()+1);
                }else{
                   occurrenceMap.put(neighbor,new IntWritable(1));
                }
           }
          context.write(word,occurrenceMap);
     }
   }
  }
}

Stripe 演算法的 Reducer 有點複雜,因為我們需要迭代過一整個 map 集合,然後對每個 map,再迭代過所有它的 value:

public class StripesReducer extends Reducer<Text, MapWritable, Text, MapWritable> {
    private MapWritable incrementingMap = new MapWritable();

    @Override
    protected void reduce(Text key, Iterable<MapWritable> values, Context context) throws IOException, InterruptedException {
        incrementingMap.clear();
        for (MapWritable value : values) {
            addAll(value);
        }
        context.write(key, incrementingMap);
    }

    private void addAll(MapWritable mapWritable) {
        Set<Writable> keys = mapWritable.keySet();
        for (Writable key : keys) {
            IntWritable fromCount = (IntWritable) mapWritable.get(key);
            if (incrementingMap.containsKey(key)) {
                IntWritable count = (IntWritable) incrementingMap.get(key);
                count.set(count.get() + fromCount.get());
            } else {
                incrementingMap.put(key, fromCount);
            }
        }
    }
}

結論

比較這兩種演算法,看得出來相較於 Stripes 演算法,Pairs 算法會產生生更多的 key-value pair。而且 Pairs 算法捕捉到的是單一的共現事件,而 Stripes 演算法能夠捕捉到所有的共現事件。Pairs 演算法和 Stripes 演算法的實踐都非常適合使用Combiner。因為這兩種演算法實作產生的結果都是可交换(commutative)與可結合(associative) [*1],所以我們可以輕易地重用 reducer 作為 Combiner。如前所述,共現矩矩陣不只能應用於文本處理,而且會是 MapReduce 演算法上一個有用的武器。謝謝你的閱讀。

註1: 可使用combiner 的資料必須能夠滿足交換律與結合律
註2: Pairs 和 Stripes 分別是兩種不同的共現矩陣演算法

參考資料

 
over 5 years ago

http://hexo.io/

有鑑於網路上大部分的文章都是教學如何佈署到 GitHub 和 Heroku 為主的教學
雖然我的需求沒什麼太大不同,還是分享一下給需要的人參考
簡單建立專案、基本設定、套用主題、產生靜態檔案和佈署設定這邊不說,我針對佈署到個人站台做說明

  1. 建立好專案和寫了些 post 之後,使用 hexo generatehexo g 產生靜態檔案,會放在 public 這個資料夾下,當我們使用 hexo deploy 的時候,其實是把 public 的內容傳上 GitHub 或 Heroku 等空間(更正確來說是上傳 .deploy 的內容),因此我們同樣只需要把 public 的東西放到站台上就好了

例如我在:http://fancy.cs.nccu.edu.tw 伺服器中的 Apache 根目錄中建立了 project/vexo 這個目錄,我打算把專案放在這裡,因此我用 FTP 傳送到這個目錄下

接著我們就可以連上網址看結果了

Oops! 看來 CSS 和 JavaScript 等靜態資料沒有正確的載入
開一下 console 來看

resource 的路徑直接 request 根目錄下的 img、css 和 js,所以當然找不到,因為我們是放在 根目錄下的 project/novex
解決方法是設定好 _config.yml 的 url 和 root

   url: http://fancy.cs.nccu.edu.tw/project/novex
   root: /project/novex

把 root 設定為我們專案的根目錄,然後再 generate 一次後放到站台上

參考文章:

PS: 後來發現是台灣人寫的,採用 MIT License,也真的是 Made in Taiwan,感動!