最近有根据 HTML 生成文章摘要的需求,所谓文章摘要即是在文章以列表显示的时候, 不想全文输出,只显示文章的部分内容,使列表内容显得干净整洁, 又能让人明白整个文章的主题, 即是文章摘要。很多网站都会支持这种方法,但是支持程度各有优劣,于是想看看应该用哪种方法能更好的解决问题。

许多做过 wordpress 主题的朋友知道, Wordpress 里面提供一个函数,叫做 wp_trim_excerpt, 这个函数默认会在文章里找 55 个空格, 然后截取之前的内容作为文章摘要,但是中文情况下就不太好用了。跟英文不同, 中文里很少会有空格出现,而英文正好是以空格作为单词分隔的。而且这个函数的效果有限,有时候会去掉文章的格式,只剩纯文本。

所以我们来尝试自己编写一个生成文章摘要的程序。

所以,想要编写不那么 suck 的文章摘要生成程序,需要达成哪些目标?

  1. 输入的 html 应该是合法的, 输出的 html 也应该是合法的。
  2. 输出摘要的字数是可以指定的。
  3. 尽量保留文章格式, 比如链接, 加粗,下划线等等。

闭合标签

熟悉 html 的朋友都知道,html 内嵌套这个时候处理比较烦人,我们要保证 html 标签的合法性,必须要知道哪些标签是必须闭合的, 哪些是可以不用闭合的, 另外,如果有行内属性或者行内样式的话,简单的对 html string 进行 slice 是无法截取到指定字数的可见字符串的。

首先,获取无需闭合标签的集合:

area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr

以上这些标签是无需闭合标签,除了这些,其他的基本都是必须闭合标签,这样我们获取到了 html 标签合法性检查的标准。

然后, 如何获取除标签以外的可选字符串呢?

过滤标签

这里我们可以用正则表达式来解决,python 或者 go 的话 正则表达式为:<(/?)([A-Za-z0-9]+).*?>,(Javascript 里的正则表达没有 lazy 模式,所以应该是<(\/?)([A-Za-z0-9]+)[^>]*>, lua 里面没有正则表达式,应当改写为相应的:<(%/?)([A-Za-z0-9]+)[^%>]*>),此正则表达式可以匹配 html 里的标签。

接下来,首先截取 html 字符串, 截取的 html 可以是标签不闭合的,但是可见字符串必须是符合规定数量的。而且要保留格式。

比如截取下文里10个字符串:

<p class="class-name">Hello world!</p>

结果应该是:

<p class="class-name">Hello worl

而不是:

<p class="

也不是:

Hello worl

标签补全

获取到截断的 html 后, 再为截断的 html 校验合法性并补全标签。

依然可以利用匹配标签的正则表达式监测标签合法性,注意忽略无需标签闭合的集合,另外需要注意标签闭合的顺序:

<p>Hello world <a href="#"><span>link...

的正确补全方式为:

<p>Hello world <a href="#"><span>link...</span></a></p>

中文处理

中文字符串在某些程序语言中处理会有问题,对于 Go 语言用户, 可以用正则表达式:&#?[A-Za-z0-9]+;进行 Unicode 字符串匹配,Lua 或 luvit 中可以用 ([%z\1-\127\194-\244][\128-\191]*) 对字符串进行分组匹配,这样可以防止截取半个中文字符形成乱码的情况出现。

注意可选添加 省略号(ellipsis) 作为文末的修饰。这样就完成了文章摘要。

我已经完成了一个 luvit 版本,地址在:View on Gist

实际效果:

欢迎交流!

Have a nice day!


I have heard about Luvit for a long time, but I never get deep into it until recently. Luvit is a nodejs style lua libray with callback hell Asynchronous feature by Tim Caswell :) , it works well in most of the platforms, it's small and fast. It's really an interesting pramgraming language, and it's very easy to learn.

sample code of luvit:

local http = require('http')
http.createServer(function (req, res)
    res:writeHead(200, {['Content-Type'] = 'text/plain'})
    res:finish('Hello World\n')
end):listen(1337, '127.0.0.1')
print('Server running at http://127.0.0.1:1337/')

looks right like nodejs, right?

So I write a http framework library base on http module, it's luvit-mooncake. I'm trying to make it looks like connect or express framework of nodejs as possible as I can. and below is the demo usage of luvit-mooncake:


Intro:

Firstly, you can install luvit-mooncake with npm(yes, it's npm).

npm install mooncake

Then you can use mooncake in your lua file:

local MoonCake = require("./node_modules/mooncake")
local server = MoonCake()

-- route your application
server:get("/", function(req, res)
local content = "<p>Hello world from MoonCake</p>"
res:send(content, 200)
end)

server:start(8080)

Router:

you can route your application by calling server:get, server:post, server:put, server:delete directly like the sample code above, or you can add your route in this way:

server:route({
  ["get"] = {
    ["/users/:id"] = function(q, s)
      s:send("List User in Databases => " .. q.params.id)
    end
  }
})

avaialble method for route: "get", "post", "put", "delete" and "all", all means any method will be routed.

server:all("/hello", function(q, s)
  s:send("HELLO!")
end)

server:get("/admin", function(q, s)
  -- if user not login then
  s:redirect("/login")
end)

Response:

extra method for response res:

  • res:send
    res:send("Hello world!")
  • res:redirect
    res:redirect("/login")
  • res:render
    res:render('./view/index.html')
    see renderer below
  • res:status
    res:status(404):render("/view/404.html")

Request

You can get post data via req.body, get query data(such as "/users?page=1") via req.query.page

req.body

server:post("/posts/new", function(req,res)
  if req.body.title and req.body.content then
    print("new post")
    -- Save to DB:
    -- DB.save("post", {title = req.body.title, content = req.body.content})
    res:redirect("/posts")
  end
end)

req.params

server:get("/posts/:id", function(req, res)
    post = DB.find("post", {id = req.params.id})
    res:render("./view/post.html", {post = post})
end)

req.query

-- [GET] 'http://example.com/posts?page=4'
server:get("/posts", function(req, res)
    local page = req.query.page
    local skip = (page-1) * 10  --(10 post per page)
    res:render("./view/post-list.html", {
        post = DB.find("post", {
            skip = skip, 
            limit = 10
        })
    })
end)

renderer

create a html file with content like this:

{(header.html)} <!--import layout-->
<h1>Post List</h1>
<ul class='posts'>
<!--loop in content with lua syntax style. -->
{% for _, post in ipairs(posts) do %}
    <li>
    <h2>{{post.title}}</h2>
    <p>{{post.content}}</p>
    </li>
{% end %}
</ul>
{(footer.html)}

Template renderer powered by https://github.com/bungle/lua-resty-template/ , you can use it simply with res:render(TEMPLATEPATH, CONTEXT):

You can get more about render template syntax at: https://github.com/bungle/lua-resty-template/ .

server:get("/posts", function(req,res)
  -- get post list, render template.
  res:render("./view/post-list.html", {posts = DB.find("posts")})
end)

Static files:

One more thing, static server for static files!

root option means mount path, for example: "/static/" means url path will be "http://example.com/static/file.ext"

server:static("./public/", {
  root = "/static/",
  maxAge = 31536000 -- one year
})

Start Server:

Start your server: (server:start must be called lastly.)

server:start(8080)

Now you can open your browser with url: http://localhost:8080, here we go!

If you are interested in this project, please don't hesitate to let me know. All stars, comments, suggestions, contributions and Pull Requests are welcome :)

Fork me at github!


最近重写了 DoubanFM 客户端,这次没用 Node-webkit, 改为用 Atom-Shell(此项目已改名为Electron), Electron 支持全局快捷键, 无边框窗口, 背景透明, 跨域请求, 不用另外适配 ffmpeg 等, 完全满足我的需求。所以这次又重写了这个项目。现在已经放出 release 下载, 或者您也可以打开命令行, 然后:

$> git clone git@github.com:cyrilis/Douban-FM-Express.git
$> cd Douban-FM-Express
$> npm install atom-shell -g
$> atom-shell .

如果使用中遇到任何问题,欢迎给我提 issue 或者 pull-request

正在尝试 Swift + WebView for Mac 版本, 可以缩小软件包提交并提高运行效率,目前尚不稳定,暂时就不放出了。

接下来是预览图片和视频:

DoubanFM screenshot-1 DoubanFM screenshot-1

Thank's for reading and sharing, and that's all for this week.

You can follow me at Twitter or fork me at Github.


Lately I published a node module on npmjs.com:

https://www.npmjs.com/package/epub-gen

It's a nodejs library for generating Epub book in a simpe way. It's quite fast and cost less compute resource. By the way, it can auto download remote images within source html.

Demo Preview:

Sample html from Learning JavaScript Design Patterns - by Addy Osmani

Making a Epub ebook is much easier than mobi ebook, If you want to generate a mobi Ebook, you have several options:

All of them need to install third party library and cost a quite mount cpu and memeries, especialy kindlegen, it sucks.

But up to epub, it's a open document, an Epub file is essentially a zip file. You can extract a epub file with zip software, then you'll get a folder like following files:

  | mimetype
  |--META-INF
  |    container.xml
  \–-OEBPS
      |  content.opf
      |  toc.ncx
      |--images
      |    cover.png
      |    image_1.jpg
      |    image_2.jpg
      \--text
           chapter_1.html
           chapter_2.html
           chapter_3.html
           .....

Oppositely, you can compress a directory comtaining those files in to a .epub file. hover, if you compress zip file with maximum compression option, some ebook reader will fail to open it. It's because of the mimetype file in the root directry shouldn't be compressed, this file should be simply saved into the generated file. So you can compress the direcory in this way:

$ zip -X -0 book.epub mimetype
$ zip -X -9 -r book.epub * -x mimetype

All source file like chapter_1.html and cover.png should be decleared in content.opf with media-type. if you specify the html file type as “application/xhtml+xml”, you may run into some format issues like unclosed tag, because xhtml is a strict file format. to solve this, you can specify html file type as "text/html", so that generated epub file could be rendered correctly in most of the ebook readers. toc.ncx is a optional file used to specify the table of content of the book, so that you can see the index of the book in ebook readers.

That's it, generating an Epub ebook is much easier than a Mobi ebook. I have a kindle(paperwhite), it works. Yes, it just works, but it's not a quite good book reader to me. It's slow and with blank-to-black flash every time I turn a page which contains images. but the iBook on iPad is really good. It smooth and elegant. The only one defect is I can't sync my books in iBook between iPad and Mac automaticly, if you want to sync your books, you have to use the buggy iTunes. However, overlook this issue, iBook on iPad is still a good reader to me. So, throw away your kindle, let's read on ipad.

That's all for this week, have a nice day.

[Updated 2015-3-1:]

The only notable e-reader lacking support for the EPUB format is the Amazon Kindle. http://www.wikiwand.com/en/Comparison_of_e-book_formats#/EPUB


相信很多人都看过 Dribbble 上的 Color Palettes, 如下图。我当年看到后就一直很喜欢,我也想知道这些颜色是怎么抽取出来的。如果知道了所有的像素颜色,如何提取出指定数量的出现次数最多的颜色呢?

Dribbble Color Palettes

去年的时候还专门找过解决方案,当时找到的解决方案有以下两个:

1.ImageMagick

Fred's ImageMagick Scripts: SPECTRUMHIST

这个应该是比较成熟的解决方案了, 可以直接指定颜色数量,色彩空间(colorspace), 压缩等级,排序方式等。核心部分是用的 ImageMagic 的 -colors 参数。我参考这个脚本也写了一个,方便输出 JSON 格式。代码放在 Gist:

https://gist.github.com/cyrilis/eec13e35870a27a0bc96


[2015-3-1 更新:]

已发布为 Node Module:

https://www.npmjs.com/package/colors-palette


2.Color-Thief

第二个可能很多人知道了,Javascript 实现的,地址是: https://github.com/lokesh/color-thief ,Demo 地址:http://lokeshdhakar.com/projects/color-thief/ ,算法后面会讲到。

通过一些图片的测试,我还是觉得 ImageMagick 的更加精准一些。他们是怎么实现的呢?以下是我的个人理解:

算法

在 Wikipedia 上有个关于颜色压缩的介绍:http://en.wikipedia.org/wiki/Color_quantization ,关于核心算法,介绍最全也最容易理解的的相关论文应该是 Anthony H. Dekker 的这篇: Kohonen Neural Networks for Optimal Colour Quantization。里面详细介绍了几种 Color Quantize 的核心原理:见下图:MMCQ(modified median cut quantization),sophisticated Median, Oct-tree,和 Kohonen Neural Networks.

此外,业界对颜色压缩的论文可以找到一大堆,不过大概不超过以上几种。

颜色差异计算公式:

r,g,b 分别为颜色的 RGB 色彩空间里的对应的 red, green, blue 通道的值。

1.MMCQ 算法:

MMCQ 算法是指的是 将色彩空间里的颜色按照密集程度分组,先将整个 ColorSpace 视为一个整体A,然后通过计算颜色差值:

找到 median point,然后切割成两个分组A1 和 A2,然后再找到最大的一个组(比如A1),计算差值找到 median point, 继续切割成A3,A4,现在我们有了三个分组(A2,A3,A4),继续找到最大分组切割,直到达到所需的颜色数量 N。然后得到 N 个分组,这 N 个分组的 median point 的颜色值即为要求的颜色值。颜色比例即为 N 个分组的大小比值。

2.Oct-Tree 算法

Otc-Tree Algorithm 所谓 Otc-tree 算法,跟 MMCQ 算法相反,先将所有颜色分布在 ColorSpace 里,视为一个 Otc-tree, 每个树上的分支有含有pixel 的节点。然后遍历所有节点,根据差异计算找到差异最大的节点,然后对此节点收缩到上级分支,然后再进行下一轮遍历, 直到分支数量等于或者小于所需颜色数量 N。

3.Equal-sized 算法

Equal-sized Algorithm 这个算法就简单了,将颜色按数量均分,median point 的计算方法不是按照差异距离算法,而是按照数量多少来计算,就是说,每个分组的数量是相同的,所以这种算法只能用在要求的颜色数量为2的幂值的情况。而且获得的颜色结果并不准确。个人不推荐这种算法。

4.Kohonen Neural Networks 算法

神经网络算法。这个完全看不懂。希望有理解的可以解释下,不胜感激。

总结

Nick Rabinowitz 将 Leptonica 的 MMCQ 算法 的算法改写成了 Javascript, 上面说到的 Color-thief 就是用的 Nick Rabinowitz 的 核心算法。 除此之外我还找到另外一个 MMCQ 算法的实现,用 TypeScript 写的,相对更容易理解一些。

ImageMagic 的则是用的 Otc-Tree 的算法,官方对此有大概算法解析:http://www.imagemagick.org/script/quantize.php ,解释了 Otc-tree 算法的原理。不明白的可以仔细看看这篇文章。

Otc-Tree 算法相比好像计算量更加大,如果用 Javascript 的话估计不太现实, 我试过自己用 JS 写了一遍,但效率很差,一个 600x800 的图片 筛选10 个颜色要大概10s左右才能取得结果。相比之下 MMCQ 计算量要更小一些。但 C++ 写的 ImageMagick 效率明明很不错,一定是我算法写的不够好,C 和 Js 效率不可能差那么多。

大概就是这些了。水平有限难免有疏漏之处,望大家指正。

Have a nice day!

Reference:

  1. Color quantization using modified median cut
  2. Color quantization using octrees
  3. Computerized simulation of color appearance for dichromats
  4. Optimized Mean Shift Algorithm for Color Segmentation in Image Sequences
  5. An Algorithm for Fast Segmentation of Color Images
  6. Color Reduction Using K-Means Clustering


这两天打算重写 Douban-FM-Express , 因为看到以前写的代码觉得惨不忍睹。想重新做一下设计, 重构一下代码, 也是对自己的一个锻炼吧。 期间看到以前写的json代理脚本又觉得不爽,于是重新写了一下, 放到 gist 上, 当作工具备份吧。

功能: 实现json代理转发,解决浏览器端的跨域问题。主要用来调试, 而不是生产项目上。本人不对其安全性负责。

Usage:

node app -d <sampleDomain> -p <listenPort>  

eg:

> node app -d dribbble.com -p 4000
# Proxy started on port 4000, API domain is set to: dribbble.com

Source:

Gist 地址:https://gist.github.com/cyrilis/9644051

这样测试的时候调用 目标网站api 的时候将域名替换为 localhost 加 监听端口, 就可以实现 json 调用了,免去跨域问题。当然项目部署的时候要将域名改回来哟。

Have a nice day!


因为VPS服务商跑路, 网站直接挂掉, 也丢了一些数据, 所以干脆买了 Linode, 用 Nodejs 自己写了个博客程序搭起来了。

去年开始看了一下 nodejs 相关的东西,开始喜欢上用 nodejs 写东西 , 而且感觉nodejs 的优势还是挺明显的 ,异步调用,事件驱动,高性能,熟悉js就能很快入门,学习门槛没多高,就是有时候需要写好多层嵌套的回调,一环套一环, 代码是竖向波浪形的。虽然有 q.js 和 async 之类可以很方便的队列,但总归不是很方便。不过通过回调倒是能方便的了解服务的运行逻辑。还有就是各种服务器错误必须handle,否则轻则500, 重则Service直接挂掉,另外顺便还学了 CoffeeScript,但是觉得虽然写法上像 ruby 了,但是由于没有括号作为标记,总觉得阅读起来特别困难, 所以干脆一直用 js 了  (现已完全转投CoffeeScript阵营)。

node社区很活跃, 像 ExpressKoa 之类的框架都发展的很好,很方便扩展为自己的MVC框架, 像个小型的 rails了, 说到rails, node 有个框架叫做  sails,模仿rails的node框架, 去年看的时候还没有实现真正的 ORM, 不知道现在进行的怎么样了。学了下MongoDB, MongoDB 是 ODM 型数据库,是面向文档结构而不是面向对象, 可以进行非对称存储 , 当然也属于NoSQL数据库,所以好处是不用写 SQL 语句(虽然用rails的时候也没写过)。

现在用的博客即如开头所说,也是用node 写的, 基于Express, 数据库是 MongoDB, 效率应该是没问题啦, 但我写后台经验不足,不知道安全方面怎么样。代码放在github上,欢迎大家Fork:

Fork me at Github

Have a nice day!



等了很久的 Ghost 博客程序终于对 公众开放了, 此前一直处于封测阶段, 收到邮件后立马下载本地部署了一下, 虽然还有很多规划中的特性没有实现, 但是已经可以一窥其中的风采了。

下图是博客的编辑界面,自适应屏幕宽带的哦。

ghost-preview

可是。。。 说好的挂件呢? 那些漂亮的 widget 哪儿去了?

看来还是得等正式版了。

程序根目录下的config.js 里面可以自定义网站地址、邮件发送配置、数据库系统、还有服务器地址和端口等一些信息, 默认的数据库是 sqlite3。程序还支持自定义主题, 目前只有一个主题可以选择, 可以看源码了解主题的编写规则。

跟wordpress相比, Ghost的优势在于简洁,轻量,完全专注于博客这一个点。 wordpress太过强大, 都可以用来当作CMS系统了。

Ghost的前景还是很好的, 虽然目前来说还有许多 feature 尚未实现,但已经奠定好了一个不错的基础, 当然, 我现在还是不会选择用它来当自己的博客系统,我还是自己写一个nodejs版的吧。


看到很多人都在说 github 上的 node-webkit 项目很不错,可以将nodejs应用直接转为桌面客户端, 原理是内嵌一个webkit浏览器,所以在跨平台上面很方便。关键是可以调用nodejs, 这一点很是难得。所以手欠又折腾了一个豆瓣电台客户端。

豆瓣电台的桌面客户端并未公开,这里用的是别人通过抓包分析的豆瓣官方客户端的接口, 此外,Allocator 的介绍也很详细,参考了很多。感谢。

由于主要是json的调用,所以原本的后台nodejs在这个程序里面是不需要的, 我只用到了webkit浏览器。 但是浏览器调试的时候nodejs作为json代理还是帮了大忙。

界面大概是这样的:

Douban-FM-Express

可以登录,收藏,不再收听等。但是因为我太懒, 本来想加上切换频道什么的,以后会再加上。现在默认是私人频道。git clone 以后运行package里面的nw.exe 就可以了。界面什么的等以后有时间再改吧。

源码在 Github:

Update 2015-05-23:

已经改用 atom-shell(Electron):

http://cyrilis.com/posts/another-doubanfm-implementation


好久没有更新博客了, 今天推荐 Windows 上的一款 Markdown 工具 --- Markdownpad

软件截图:

markdownpad2是markdownpad的升级版本, 提供了几乎任何你可以想到的markdown 功能, 这款软件支持多标签页, markdown实时预览, 全屏模式(Full Screen), 自定义css文件, 添加css文件(需Pro版), 拼写检查(Spell checking ), 显示行号, 自动保存等功能. 另外还可以自定义字体,可以导出为PDF或者带样式的PDF(需Pro版)和HTML,自定义html的Head信息(需Pro版). 实在是居家旅行之必备神器.免费版一直可用. 适合经常需要用Markdown写文档的同学.

This article is just Written With MarkdownPad :)

下载在这里下载点此

如果你不喜欢新版的MarkdownPad, 这里有第一版的(完全免费), 我个人觉得已经满足我的需求了.

第一版下载 :  第一版下载