如果你是一个python爱好者。

如果你使用CSDN而又不满意页面样式,使用wordpress又觉得插件太复杂,同时又对jekyll的ruby和hexo的node.js没什么兴趣,那么去试试Pelican。不过本篇不是要介绍这些博客系统,如果你在使用别人写的博客系统的同时,对代码高亮,评论框,文章的模板,插件的定制,页面的style感到达不到心中所想的那样(只是突然想造个轮子),那么你也许可以尝试自己来写一个博客系统。

博客系统的基本需求

按照现在的潮流,一个静态博客系统就足够了,然后得支持markdown,再然后产生页面的速度要快,代码要能高亮,等等。总结一下大概是:

  • 支持markdown
  • 静态编译产生博客
  • 代码能高亮
  • 页面可定制
  • SEO要好
  • 使用python编写

整个需求中最关键的部分大概就是如何将markdown转换成html,只要这一步达成了,其他的几个都好说。这个功能,我推荐github上的Mistune,这是一个纯python编写的一个据说是目前python实现的md2html中最快的一个解析器。
另一方面,代码高亮当然是使用pygments,页面模板当然交给jinja2,酷炫的网页显示当然少不了jquery和bootstrap。在稍微思考了一下后,这个系统的样子大致应当是下面这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
|-blog
|  |-source
|  |-template
|  |-post
|  |  |-my_post.md
|  |-html
|  |  |-my_post.html
|  |  |-.git
|  |-main.py
|  |-Makefile

在source目录下放的是js和css,template放的是页面的模板,也就是每次解析md文件后产生的不完全的html将填到这个模板中去,再然后post就是写的markdown文件存放的地方,而html就是最后解析出来的页面所存放的地方,main.py是我们的主要脚本,而Makefile只是为了方便管理工程。使用make来产生html,使用make clean来清除上一次产生的结果。最后,你只需要在html目录下创建一个git仓库将它定时的push到github上,就能完成blog的展示了。

将markdown解析为html

首先,最基本的,将md文件解析为html,并设置代码高亮。

Mistune

这个活得交给Mistune来做,对于Mistune来说,用html渲染一个页面是一件很麻烦的事情,你得设置用何种方式渲染,甚至得重载很多函数,不过正是这样,可供我们定制的地方就越多。
Mistune的基本用法是:

1
2
3
4
import mistune
mdp = mistune.Markdown(escape=True, hard_wrap=True)
markdwon('I am using **mistune markdown parser**')
# output: <p>I am using <strong>mistune markdown parser</strong></p>

不过单单这样还不够称之为一个好页面。首先是得解决代码高亮的问题

pygments

pygments总之就是一个非常厉害的专门做代码高亮的一个模块。它和Mistune配合使用就能解析markdown中带有反撇号们围起来的代码块了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# main.py
import mistune
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter

def block_code(text, lang, inlinestyles=False, linenos=False):
    if not lang:
        text = text.strip()
        return u'<pre><code>%s</code></pre>\n' % mistune.escape(text)

    try:
        lexer = get_lexer_by_name(lang, stripall=True)
        formatter = HtmlFormatter(
            noclasses=inlinestyles, linenos=linenos
        )
        code = highlight(text, lexer, formatter)
        if linenos:
            return '<div class="highlight">%s</div>\n' % code  #这句中的highlight是高亮代码的祖先选择器
        return code
    except:
        return '<pre class="%s"><code>%s</code></pre>\n' % (
            lang, mistune.escape(text)
        )


class HighlightMixin(object):
    def block_code(self, text, lang):
        # renderer has an options
        inlinestyles = self.options.get('inlinestyles')
        linenos = self.options.get('linenos')
        return block_code(text, lang, inlinestyles, linenos)

按照Mistune的文档重写block_code,然后把HighlightMixin这个类加到Mistune的脚本中:

1
2
3
4
5
class TocRenderer(HighlightMixin, mistune.Renderer): 
    pass

renderer = TocRenderer(linenos=True, inlinestyles=False)
mdp = mistune.Markdown(escape=True, renderer=renderer)

这样产生的html就是使用pygments加了特技的html了。
不过这还没完,现在的页面还不是代码高亮的。我们还得使用pygments来产生对应的css

1
pygmentize -f html -a .highlight -S monokai > pygments.css

其中的.highlight对应之前的祖先选择器,而monokai是我喜欢的一个配色方案。

jinja2

虽然Mistune帮我们生产了HTML但产生的页面还缺点东西。比如head还有body的标签对。我们采用jinja2来布置我们的html模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE HTML>
<html lang="zh">
<head>
<title></title>
  <link rel="stylesheet" href="/source/css/bootstrap.min.css" type="text/css">
  <link rel="stylesheet" href="/source/css/font-awesome.css" type="text/css">
  <link rel="stylesheet" href="/source/css/style.css"  type="text/css">
  <link rel="stylesheet" href="/source/css/highlight.css" type="text/css">
  <link rel="stylesheet" href="/source/css/pygments.css" type="text/css">
  <link rel="stylesheet" href="/source/css/google-fonts.css" type="text/css">
  <link rel="stylesheet" href="/source/css/responsive.css" type="text/css">  
  <link rel="stylesheet" href="/source/css/sidenav.css" type="text/css">  
  <script src="/source/js/jquery-2.0.3.min.js"></script>
</head>
<body>
    <div class="col-xs-12 col-sm-9 col-md-9 note">

    <div id="disqus_thread"></div>
    <div class="container-narrow">
    <footer>
    <p>&copy; 2015 septicmk with help form <a href="https://github.com/lepture/mistune" target="_blank">mistune</a>.
    Theme by <a href="http://github.com/wzpan/hexo-theme-wixo/" target="_blank">Wixo</a></p>
    </footer>
    </div>
    <script src="/source/js/jquery.imagesloaded.min.js"></script>
    <script src="/source/js/gallery.js"></script>
    <script src="/source/js/bootstrap.min.js"></script>
    <script src="/source/js/jquery.tableofcontents.min.js"></script>
    <script src="/source/js/tocgenerator.min.js"></script>
    <script src="/source/js/main.js"></script>
    <link rel="stylesheet" href="/source/fancybox/jquery.fancybox.css" media="screen" type="text/css">
    <script src="/source/fancybox/jquery.fancybox.pack.js"></script>
    <script type="text/javascript">
    (function($){
    $('.fancybox').fancybox();
    })(jQuery);
    </script>
</body>
</html>

当然,我们的css要重头写起是很难受的,于是我就从hexo的某个主题中偷了一个Wixo。在此感谢Mistune和Wixo的作者。(github开源大法好)
在使用jinja2之后你就可以写一个这样的函数来解析你的markdown了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def md2html(md_pwd, mdp, template, root_path, des):
    try:
        template = open('template/'+template+'.html','r').read()
        utils = open('template/utils.html').read()
        md = open(md_pwd, 'r').read()
    except:
        tool.log('error')('file not found')
        return 
    mdp.renderer.reset_toc()
    args, md = meta.parse(md)
    if args['categories'] == 'draft':
        return args
    md = Environment().from_string(utils+md).render()
    content = mdp(md) 
    args['content'] = content
    args['root_path'] = root_path
    args['meta'] = generate_meta(args)
    args['this_href'] = "blog.septicmk.com/" +  args['categories'] + '/' + args['shortcut'] + '.html'
    html = Environment().from_string(template).render(**args)
    try:
        _pwd = os.path.join(des, args['categories'], args['shortcut']+'.html')
        _dir = os.path.dirname(_pwd)
        if not os.path.exists(_dir):
            os.makedirs(_dir)
        f = open(_pwd,'w')
        f.write(html)
        return args
    except:
        tool.log('error')('html dir not found')

至于其中的args就是你的markdown页面前的那些文章属性

title: 搭建自己的静态博客系统
date: 2015/08/12
shortcut: Miki-blog
categories: blog
---

你可以用正则表达式来提取这些属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import re
INDENTATION = re.compile(r'\n\s{2,}')
META = re.compile(r'^(\w+):\s*(.*(?:\n\s{2,}.*)*)\n')
def parse(text):
    """Parse the given text into metadata and strip it for a Markdown parser.

    :param text: text to be parsed
    """
    rv = {}
    m = META.match(text)

    while m:
        key = m.group(1)
        value = m.group(2)
        value = INDENTATION.sub('\n', value.strip())
        rv[key] = value
        text = text[len(m.group(0)):]
        m = META.match(text)

    return rv, text

这些工具类的函数在Mistune的一个补充版中有可见mistune-contrib

定制页面

评论框

当然是disqus了,翻墙就翻墙吧,如果你不想被多说的垃圾广告评论怼的话。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<div id="disqus_thread"></div>
  <script type="text/javascript">
      /* * * CONFIGURATION VARIABLES * * */
      var disqus_shortname = 'your name';

      /* * * DON'T EDIT BELOW THIS LINE * * */
      (function() {
          var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
          dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
          (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
      })();
  </script>
  <noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript>

把这段代码插到模板中你想显示评论区的地方就可以了。当然前提是你得有disqus的账号。

用jinja2来写一些宏

这就是自己写博客系统的一大好处了,你可以随意的修改页面生产的方式。随意的向页面中添加div标签。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{ macro btn(value, link, type='info') }
<a class="btn btn-{type}" href="{link}" target="_blank" rel="external">{value}</a>
{ endmacro }

{ macro label(value, type='info') }
<span class="label label-{type}">{value}</span>
{ endmacro }

{ macro badge(number) }
<span class="badge">{number}</span>
{ endmacro }

{ macro alert(value, type='danger') }
<div class="alert alert-{type}">
    { if type == 'warning' }
    <i class="fa fa-bell"></i>
    { elif type == 'danger' }
    <i class="fa fa-bug"></i>
    { elif type == 'success' }
    <i class="fa fa-lightbulb-o"></i>
    { else }
    <i class="fa fa-info"></i>
    { endif }
    {value}
</div>
{ endmacro }

这些宏的效果我就不一一展示了。大概看看代码就知道是啥效果了哈。

注意: 上面的代码为了避免被jiaja2识别都做了处理,有毒,请勿直接食用

上面这个框就是其中一个效果。

Latex数学公式

这个好办,直接用mathjax。

1
2
3
4
5
6
7
8
<script type="text/x-mathjax-config"> 
    MathJax.Hub.Config({ 
        tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]} 
    }); 
</script>
<script type="text/javascript"
    src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>

把这段代码放到</body>这个标签前面就能自由的使用Latex写数学公式啦。

SEO

meta标签

首先页面的title是一定要写的,除了title之外。你还得写一些meta标签来讨好搜索引擎。

1
2
3
<meta name="author" content="septicmk">
<meta name="description" content="如何搭建静态博客">
<meta name="Keywords" content="python,jinja2,pygments,SEO">

根据google的一些设定,你必须有一个herlang的标签

1
<link rel="alternate" hreflang="zh-Hans" href="blog.septicmk.com/">

sitemap

当然,要得到好的SEO效果的话,得自己主动去讨好baidu和google的搜索引擎,所以得老老实实到这两个平台的站长工具中验证自己的网站.其中有一条要求提交网站的sitemap。 你可以写一个这样的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def sitemap_update():
    pwd = os.getcwd()
    _pwd = os.path.join(pwd,'html','sitemap.xml')
    des = open(_pwd, 'w')
    conf = """<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
"""
    conf += u"<url><loc>http://blog.septicmk.com/</loc>"
    conf += u"<lastmod>"+ get_mod_time("html/index.html") +u"</lastmod>"
    conf += u"<changefreq>weekly</changefreq>"
    conf += u"<priority>1.00</priority></url>"
    list_dirs = os.walk('html')
    for root, dirs, files in list_dirs:
        for f in files:
            if f.endswith('.html'):
                conf += u"<url><loc>http://blog.septicmk.com/"+ os.path.join(root, f)[5:] + u"</loc>"
                conf += u"<lastmod>"+ get_mod_time(os.path.join(root,f)) + u"</lastmod>"
                conf += u"<changefreq>weekly</changefreq>"
                conf += u"<priority>0.80</priority></url>"
    conf += u"</urlset>"
    des.write(conf)

记得更新一下自己的sitemap就OK了

发布

github-page

首先是官方文档。 很简单的。无非是新建一个username.github.com的库,然后把生成的html页面push到这个repo就可以了。

独立域名

自己去godaddy上买一个 然后写一个名为CNAME的文件把域名填上去就OK了。

定时发布

如果觉得自己手动push太烦,可以写一个自动deploy的脚本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash
path_blog= ~/blog
cd $path_blog
make clean
make
make sitemap
cd html
git add -A
git commit -m `date +%Y%m%d`
git push blog master

使用crontab来定时任务。

1
2
crontab -e
30 21 * * * ~/auto/auto_deploy.sh

定个每天晚上9点半自动push。

其他

写于2015年8月11日by septicmk。
如果你想直接使用成品的话,可以看看我在github的Miki这个库。

源码下载

若文中有误,欢迎指正。
╯     ╰
●      ●
"    ^  "