ShoujoA中的那些坑

很多年前—-我本来想这么说的,好吧,其实就是半年前。那时候的某个程序,我居然很精神地对着那几百行程序分析了足足七篇文章,现在想着……我擦,我居然闲得这么无聊过……

详细解释程序没什么意义,也有混字数的嫌疑(虽然不是写稿,也没稿费,orz),所以这次对ShoujoA的分析我也就懒得那么讲究了,随便谈谈程序逻辑或者坑啊坑啊什么的就好。

这篇文章没什么条理性,所以不分索引了,随便看看就好了,orz。

简明易懂的静态博客程序写法

事实上,一个静态博客程序就核心功能而言,就是把一个封装好的post渲染成html页面。基本上,github上那些满目琳琅的类似程序都是这样干的。而且通常而言,静态博客都是支持markdown写法的—-很容易理解,markdown本来就是文本和html之间的一架天桥。然后,网上有很多封装好的markdown的模块……于是您知道怎么干了?事实上,这也就是一个最简单的静态博客程序。就像下面的,不过提醒一下,我是假设有一个叫做markdown的模块……

import markdown
f = open('in.md','r')
html = markdown.render(f)
out = open('out.html','w')
out.write(html)

OK,您写好了您自己的第一个静态博客程序……

坑爹么?但是最简单的写法的确是这样,后面当然您可以做一些合理的扩展。比如,您认为直接在程序中写文件名实在太坑,于是把命令行解析模块加上;您觉得应该有条理地封装从文本中取出的数据,于是您写了一个类;您发现您需要好看的主题模板,于是考虑利用jinja来渲染;但是您又发现jinja中需要引入一些您本地的参数,于是又写了一个类。但是这时候您发现程序中配置乱成一锅粥,于是您使用json或者yaml实现了程序与配置的分离……但是基本的程序,也就是上面的几行。如何,很简单不是么?

储存数据

就本身来说,我不希望ShoujoA使用数据库。原因如下:

  1. 数据库程序太臃肿

  2. 某些数据库您的vps提供商并不提供

  3. 储存在vps上,指不定某天被墙,数据取回就很麻烦

  4. 您写的是一个静态博客程序……

但是事实上,您还是需要储存数据的。我这里使用了python的序列化模块,把所有的posts塞进一个列表,然后序列化就好了。这样其实效率不算很高,但是对一个静态博客,特别是个人用的静态博客是足够的,至少性能不会是瓶颈。而且对pythoner来说,列表和类都很熟,处理起来轻车熟路。这里其实可以讨论一下为什么用列表而不是字典,其实我觉得两种都可行,甚至字典某些操作更加合适,但是我能想到的不方便的是,字典排序很麻烦,也不太符合字典使用的习惯……更主要的是,由于个人一开始就选用了列表,所以没详细研究过用字典是否可以更简单,欢迎讨论。

备份数据

害怕数据丢失?dropbox拯救您,推荐的方式是在dropbox文件夹中建立一个posts文件夹,然后软链接出来,在这里面写博客。这样数据就会很安全的。本地写好再上传永远是推荐的,毕竟vps的cpu速度和流畅度永远没有本机快。

自定文本写法规则

上一篇讨论过,为了让博文有标签,分类之类的属性,所以需要自定义某些写法规则,解析器自己写一个就好了,也很简单。不过就我个人的经验来说,文本规则应该尽可能简单并且您的解析器应该有足够的容错性。比如说,标签通常是多个,中间分割符应该用空格合适还是逗号合适?如果您选择使用逗号分割,应该需要支持中文逗号和英文逗号,否则使用起来总是需要切换是很麻烦的。总之,请尽量让您的解析器更温和,毕竟主要用的是自己,不方便的写法或者斗大的、莫名其妙error提示还是挺不爽的……

python文件部分读取的坑

这个问题挺吊诡……也是在写解析器时遇见的,且听我慢慢道来。

讨论一:

import codecs
f = codecs.open('example','r','utf-8')
print f.readline()
print f.read()

说明:example是一个至少一行的文件,建议大于5行。

上面的代码,应该是先打印出一行,然后再打印剩下的内容—原本的想法。事实上,f.read()那部分打印不完全。

讨论二:

import codecs
f = codecs.open('example','r')
print f.readline()
print f.read()

上面的代码没有译码的那个参数,然后运行没问题。

讨论三:

import codecs
f = codecs.open('example','r')
print f.next()
print f.read()

这个会上抛一个异常:ValueError: Mixing iteration and read methods would lose data,我感觉能理解。

讨论四:

import codecs
f = codecs.open('example','r','utf-8')
print f.next()
print f.read()

吊诡的来了,联系讨论三,这个应该会上抛异常,但是实际上,这个行为和讨论一一模一样……

于是看见了么,由于这个坑,所以先readline再read是不行的。但是需要先部分读取再全部读取怎么办?我是用readlines处理了……效率应该说挺糟糕的,还有更好的办法么?啥,xreadlines()?哈……

markdown的换行问题

其实这个问题很容易出现……因为默认的,markdown换行是需要一行末尾有两个或以上数目的空格。但是很明显,一般写文本的时候没人会这么写,所以,很容易出现文章不分段的糟糕效果。必须对\n进行处理。幸好处理起来很容易,您可以选择使用正则式直接替换\n为两个空格+\n。也可以逐行处理,对每行最后的\n,先strip然后拼接空格再加\n就好。总之处理方式很多,个人用了一个列表推导式处理。

通过配置文件指定文件夹

在真正应用的时候,我们不希望用户自己建立文件结构,而是通过填写配置文件自动生成文件结构。所以引入了init这个概念。我的建议是,请务必花少量的时间写成这个函数,这可以使得使用变得方便很多。主要做的事:建立文件结构,初始化必要的数据。

如果本地已经有了同名文件夹,友好地抛出一个问询是一个不错的方式。个人是用了一串try except,感觉不太优雅,其实可以写得更好。

insert函数

个人感觉这个函数还是很重要的,即,给予用户更改文章顺序的权力。但是如何实现这个函数就是各抒己见的事儿,下面是ShoujoA的实现方法。

说一下ShoujoA的排序方法,即,如何决定一篇文章在列表中的顺序。ShoujoA的排序是根据文件的修改时间,由大到小进行排序,考虑到一篇文章写好之后,除了当天或者第二天进行几次小修改,就几乎不会有更多修改(我这里是这样)。所以我认为用修改时间来排序还是很合理的。当然这个方法有些脏,不过还是管用的。这个排序,每次提交都会进行一次。如果是批量提交,则会在批量提交结束之后排序一次。

那么,怎么让一篇博文的位置发生变动?答案就很明显,修改修改时间即可。python中提供了utime函数,给了我们修改时间的权力。

如果要把某一篇文章插入到某个特定的索引,需要考虑四种情况:

  1. 索引溢界,那么,抛出异常,退出程序

  2. 索引为0,即,把这个post插入到开头,这就需要取得现在列表中第0位post的时间,在这个时间上进行+的操作。

  3. 索引为不溢界的最大值,那么取得最后一个post的时间,-操作。

  4. 索引在某两个post之间,那么简单地取得这两个post的时间,平均即可。

注意到第三点上使用*不溢界的最大值*这个词。之所以这样说,是因为很可能这篇文章已经在您的列表中,不过您希望更改位置。

ShoujoA处理的时候,基本遵循上面的规则,不过对于提交的post,会先进行一个in处理(当然是用一个函数,python 自带in函数很明显不能用),如果在列表中,直接remove就好,这样处理会很简化。

这里还有一个讨论,我的时间加减操作究竟应该设为多大的值?

我的意见是,应该设为一个尽量小的值,我这里使用0.01。

这里还有一个坑,如果您阅读过前面的文章,您会知道,shoujo会自动对每一份博文进行备份,而如果postAll没有给定参数,shoujo会直接使用备份文件夹中的文件进行批重建。这里有可能的一个结果是,由于多次批重建和insert操作,备份文件夹中的文件时间有可能变得一样……虽然通常使用基本不可能复原这个bug,但是是有这个风险的。一个保守的方法是,批重建每一个post的时候,请给一段睡眠的时间,比如一秒,这样,可以尽力降低顺序错乱的风险。

代码着色

pygments支持各种漂亮的代码着色,不过事实上使用的时候,许多也不是太好看……个人推荐rrt那个。

还有,对misaka的话,代码着色还是个麻烦事。官方提供的方法不支持utf-8,不过也有人提供了方法。请见这个:https://gist.github.com/hit9/5220527

……哎,不知不觉貌似东西挺多的,还有一点剩余的东西,命令行参数解析想说呢,那个也是个大坑……不过就这样,留给下一次吧,好了好了,阅读到这里的您辛苦了~~

以上。