新史太阁记-一个不世出的产品经理成长史

因为刚看完了《丰臣秀吉》,想找些其他描写日本社会文化历史以及现状的书籍时,一不小心就看到了这本司马辽太郎的《新史太阁记》。

书的内容基本沿袭大的历史框架,不再有秀吉嗜好女色的大篇幅描写,也没有更多的民间传说的影子,总之更贴近历史的脉络,少了些民间的历史玄幻。小说从秀吉的少年开始,一直到巅峰时期,戛然而止。

这期间陆陆续续看了些秀吉的书,还有影视材料,最震撼的不仅是日本战国的武士情怀,还有出于职业背景的条件反射:如何成为一个伟大的产品经理。注意,我特意使用了伟大一词。

对于技术人的成功,我一直坚持认为:技术-产品-公司,这3个梯度的进阶和成功,才是真正的成功。而这三个体度的难度系数相差远不止一个数量级。而要完成这样的转变,你要有开阔的眼界和思维,要理解社会的发展和人性的需求,要有理想主义者的坚持和奉献牺牲。

我看到过很多技术牛人,他们热衷于 一系列能让人感觉新奇的技术,但是他们却很难理解要为产品或者公司的成功去奉献和坚持,他们更关注短期的自我的收获,缺乏长远的对产品成功,公司成功的认知和追求。会新奇的技术是有价值的,但最终为大众提供价值的是产品本身,这个涵盖技术,设计,交互,服务等,远不是很多技术人想象的因为一个所谓的技术就能怎样。

好吧,扯远了,说一说我从秀吉的故事中总结的一个伟大的产品经理的一些要素吧。

我们一般说的产品经理的价值:

清晰准确的产品定义:功能需求定义,目标用户群体定义,商业模型定义,生命周期定义。
秀吉:每一次的战事秀吉都有着清晰的产品定义,他知道自己最终要的是臣服和胜利,所以他会选择合理的战术方略,而不是简单粗暴野战屠杀,故事里面每一战事秀吉都有着很深入的分析和合适的方案提出,而这一切都是因为他对目标有着清晰的定义,能外交的就不兵刃,能围城的就不破城,因为我要的是胜利,而不是摧毁对方和杀戮。

技术质量的保障:依赖自身的知识背景,采用合理的管理方式,选择正确的技术架构,来保证可靠,低成本,快速的实现产品解决方案。
秀吉:故事里的秀吉每一此都能根据天时地利提出一套低成本而且行之有效战事方略,这些战术的方案最终保证了秀吉在保存力量的情况下取得成功。

沟通协调:在遇到资源问题或者对外需求时,能高效的沟通和获取资源保证团队的进度需求,在收到内部领导的不合理需求时,也能顶住压力和风险,保证团队能按照正确的方式和方向去开发。
秀吉:毋庸置疑,历史上真正的秀吉无论跟信长还是家康比,秀吉无疑是一个善于沟通,善于外交的政治家。

士气目标管理:产品经理不是简单的让相关的设计开发坐在一起开发就好了,要了解每个人的性格,让所有人都能认识到产品项目的价值,都能认识到自身的价值,充分体现自豪感和主动性,从而发挥巨大的价值,最终形成团队超强的战斗力。在取得阶段的进步或者奖励时,也能适时的鼓励团队,鼓励成员,保证团队高昂的士气,从而取得不可能达到的成绩。
秀吉:秀吉的部下基本都是草莽出身之所以能取得最后的高水准的胜利,得益于秀吉队伍高昂的士气,无论是通过金钱的奖励还是封地的奖赏。

背黑锅:大家都是凡人,不是神仙,到最后难免有错误的决定或者失败的计划,产品经理的另外一个用户就是替大家来背黑锅。因为产品经理就是产品和团队的负责人,失败了,产品经理是最大的责任人,无需客观理由。

而要成为伟大的产品经理,还需具备:

对产品方向执着的信念:无论是早期对信长的神一般的信仰还是后面对胜利的信念,秀吉能走这么远,能在众多的力量中崛起,最重要的是秀吉是一个理想主义者,靠执着的信念在一次次的战事和政治中取得成功。

克服一切困难创造条件解决问题的能力:面对困难时,在其他人迟疑时,秀吉积极承担,克服不利,最终树立了在织田家的声望和地位,比如美浓之战的善后工作。

愿意付出一切保证产品成功的奉献精神:秀吉对每一次的任务都是倾尽全力的,可以说为达目标不择手段,甚至去做一些被人唾骂的残忍行为。

最终,秀吉从一个茅屋少年成为日本战国时代的一代枭雄,成为了一个伟大的产品经理。

丰臣秀吉

加藤广 著
曹宇 译
2014年第一版

一直一来对日本文化都很感兴趣,小时候看的很多日本动画片,还有宫崎骏,北野武(很喜欢的另一个原因是北野武事务所一直是支持贾樟柯电影的,这两个人我都很喜欢),黑泽明等大师的很多作品。记得小时候(80年代)看的一个引进的似乎叫阿香的很长的日本电视剧(里面就有主人公日常的生活和日本社会的大量描写),那时候就对日本留下了深刻的印象。

本书是作者根据大量史记和民间传说编撰的伪纪实传记小说,每一章节开始都有摘引自史记史实的事件作息行踪记录,然后精心地穿插着作者编撰的精彩故事。

因为本书大量的日本人名和地名,本文不想写成准确的读书笔记,只想把自己读完的大概思绪以作记录,想了解详情的还请移步小说。

羽柴秀吉(藤吉郎)出身贫苦,从小体弱,身高不显,少年丧父。其父是个会计,擅长算数,因担心儿子未来生计,从小训练藤吉郎算数,使得秀吉具有缜密而灵活的头脑(纵观一生,真正使其收益的应该是其谦逊和不断学习的精神)。因为穷苦,藤吉郎早期走街串巷以卖针为生,积累了大量的地理和世俗情报,后加入当时还未一统的织田信长手下,因助其逃过劫杀而得到赏识和重用,从普通士卒成为大将大名。起初秀吉最多也只能算是众多武将中一个算是能干的普通将领,排名不是很靠前,但靠一系列的战役和英勇表现,再加上所谓的天意暗示(有高僧推荐,有手相暗示)等,最终成为一代枭雄。

军事和谋略方面,秀吉是天才,越是大事件大场面越能显示出其超常人的思维和冷静决绝的判断。
个人生活和精神情感方面,其一生都是大写的悲剧,从未停止。
社会方面,幕府时代的日本是一个家臣社会,武士忍者只能对主公负责,是主公的私有财产和工具。女性普遍不被尊重,没有地位,导致可以随便被欺凌(权势)。信长时代,西方基督教已经大举入侵日本,引进了西方的知识和理念,当然紧随其后的就是铁甲舰,洋枪洋炮和梅毒。
生活物资方面,由于日本特殊的地理资源限制,日常生活清贫(从赏赐上就能看出来)。天皇家室受人尊敬,但迫于幕府威胁和生活所迫,忍辱负重,利益权谋。

半兵卫,秀吉文化水平不高,但依赖其谦逊的态度和其不嗜杀的作风,赢的半兵卫的辅佐,半兵卫乃不世奇才,类似中国的诸葛卧龙。从而使的秀吉从众臣中显身而出,鹤立鸡群。半兵卫从一开始就帮秀吉规划好了天下,再临终前,又帮秀吉赢得了官兵卫,官兵卫助其实现战术的胜利。可以说没有半兵卫,秀吉此生最多也就大名了,战死或者被清除掉。
官兵卫,因秀吉受半兵卫建议,救下官兵卫儿子,在救出官兵卫后,得到了官兵卫的感激和追随。虽然半兵卫被描述成先知之谋略,但官兵卫的实际作用丝毫不逊于半兵卫,他是实际帮助秀吉夺取天下的人,真实的水平深藏不露。官兵卫早年信仰基督,晚年改信佛教。
秀长,秀吉的弟弟,能干,但缺乏谋略,是大臣和秀吉之间的缓冲带。50多岁就死了,使的晚年的秀吉缺少了心腹很支持。
将右卫门,秀吉的左膀右臂,没有这个踏实而放心的的伙伴和下属,秀吉的事业轨迹将大大不同,他也是秀吉唯一想说和敢说心里话的干将。事业和心灵的好基友。
小六,跟将右卫门一样,得力干将,但是跟秀吉的关系没有将右卫门近。
秀次,秀吉的侄子,从知识和才干方面,是唯一一个看似能继承秀吉衣钵的人,可惜被陷害,株连。但其和妻女3p的事情很是让人不解。
光秀,信长公的忠臣,因不甘信长暴戾和对天皇的不尊,而想取而代之,却被秀吉渔翁得利。
德川家康,从不多的描写中可以看出其老奸巨猾,能屈能伸,心狠手辣(弑其子)。
宁宁,秀吉正房,忍受秀吉鬼混,终其一生陪伴和追随的人,是一个真正爱着秀吉的女人。宁宁是个下等人的女儿,但秀吉并没有在显赫时休掉宁宁,也没有加以恶意相待和冷落,并且对夫妻关系作必要的经营和维护,可以说是秀吉自己用行动获取了宁宁最终的支持和陪伴。
龙子,极其崇拜秀吉,是其心灵伴侣,在精神和肉体上都得到秀吉的信赖和喜欢。
茶茶,一个淫贱,有手腕,心狠手辣的女人。然而平心而论,最终她也许都只是她母亲的一个工具,是一个悲剧的影子。

和善,不拘小节。不忌主仆繁文缛节,平时打成一片,深受部下拥戴和喜爱。
施财而好才。先后招揽了了半兵卫,官兵卫,将右卫门,小六等当世之才。秀吉不像其主公信长那么吝啬,受封后尽将财物封地分与下属,赢得部下的效力和忠心。在急行军后,开库散银,犒赏全军。懂得用眼前的利益赢得未来的利益。善招揽人才,善用人才,这也是其脱颖而出的关键。
善谋略,不嗜杀。其大多的战术都是围城,水淹威胁,一夜城恐吓等,即使在拥重兵的情况下也不擅使暴力强攻,大部分的城池都是接受投降,甚至城主留任。
好色。以现在的眼光看是很好色,但综观后面的秀次等的表现就可以明白,秀吉的水准也许只是普通水准略高而已,毕竟权高位重嘛,获取资源门槛低。整个当时的日本都是这样的风气,也不能单怪秀吉。
谦虚好学。无论是早年对大家意见的细心听取还是后面的对诗词的虚心学习,都说明真正使秀吉强大而可怕的正是其一生的学习态度和追求。

秀吉把信长作为对手,但在除掉对方后,获得权力后,却心怀愧疚被家康持以把柄以及临死都无法释怀,恰恰是日本家臣以及武士精神的体现,在中国历代的权利斗争中很少有此方面的感情顾虑。

秀吉的整个人生就像一颗坠落的流星,从冉冉发光,到光芒万丈,最后到燃烧殆尽。秀吉用其一生的经历,说明了即使一个出类拔萃的人在面对权力和金钱时,也很难保持自我。你未疯狂,因为你还没有尝过权力的滋味。

追求金钱和权力的过程可以让一个人得到满足感,但金钱和权力本身并不能满足人精神层面的空虚和欲望。

羽柴秀吉,藤原秀吉,后来天皇赐姓丰臣,也就是大家耳熟能详的丰臣秀吉。

如果秀吉可以生育,如果他没有过早的疏远忠臣干将,如果他不是用现在的方式杀死信长,如果他当年追杀了家康,如果他没有杀秀次,如果他没有宠幸茶茶,如果他坚持收养了天皇的弟弟,秀吉的人生又应该是怎么样的呢?

但生活,没有如果。。。

东京散步

作者:藤田宜永
译:兴远

译者的水平还是不错的,让整个故事叙述显得生动,细腻,阅读起来很舒服。
建议阅读时长:1-2天

首封上写的是:幸福来时总是慢得让人难以察觉,而不幸却是毫无预兆地轰然降临。

故事讲述了一个出身悲惨,处境落魄的年轻人突然接到一个诡异而报酬诱人的陪同散步的生意。在散步的过程中双方互相倾诉心境,途中一起经历了种种怪异而有趣的事件。最终在故事的结尾,主人公得知了生意的真相,也在对方的帮助下实现了自己内心的愿望。故事最终得到了一个令人惋惜而不忍的结局,但真实的生活不就是这样吗,不完美才是我们孜孜以求。

故事娓娓道来,看似缓慢的节奏却引人入胜,结局有点想不到,整个故事在高潮中结束,一气呵成。如果你在一个周末的早上开始阅读,也许在你下午会不经意的发现你竟然一口气看完了整个故事。

从故事中你能了解当时的日本的一部分群体的生活状态和社会现状:高利贷,弹子店,情色店,援交,离家出走,婚内出轨等。通过几个相关人物的人生经历和状态,让人感受到日本特定时代下一部分群体真实的生活。

结尾福原有一段的语言显得很生硬,也许是作者构思小说时的最开始提炼的主题,所以硬生的借福原的嘴表达了出来。

推荐:细读

git 403 Forbidden

现象

error: The requested URL returned error: 403 Forbidden while accessing https://github.com/xxx/xxx.git/info/refs

fatal: HTTP request failed

分析

错误是在一个脚本中操作git时的错误提示,看错误提示初步判断是权限问题,继续google之,发现 : http://stackoverflow.com/questions/19722521/error-the-requested-url-returned-error-403-forbidden-while-accessing 。

解决

参照帖子的思路:

  1. 将https改为ssh访问,直接修改repo对应的隐藏文件.git/config 文件中的url即可。
  2. Generating an SSH key

再次运行脚本,执行成功。

hugo生成文章中不能正确显示中文字数的问题定位

问题发现

在刚切换到hugo博客的时候就发现文章的字数统计是错误的,但是因为当时也没发现有额外的副作用,就暂时懒得理了,毕竟这个只是写作工具,web前端也不是本人长项。

今天在使用多说分享文章的时候发现部分文章分享失败,于是根据分享生成的URL中的数据初步判断是获取文章content时出错了。插件获取了超长的content内容,导致接口调用失败。

问题定位

1. 多说

我们看多说插件中的相关代码:

data-content="{{ .Summary }}" 

多说只是使用了模版中的Summary变量值。

2. Summary

关于Summary变量,我们看Hugo的解释

Hugo-defined: automatic summary split

By default, Hugo automatically takes the first 70 words of your content as its summary and stores it into the .Summary variable, which you may use in your templates.

Pros: Automatic, no additional work on your part.
Cons: All HTML tags are stripped from the summary, and the first 70 words, whether they belong to a heading or to different paragraphs, are all lumped into one paragraph. Some people like it, but some people don't.

从以上的内容看到Hugo只是默认最多取70个词(words),所以这里很明显是取70个词时,实际取出了远远多于70个的内容,导致上面的url出错。

3. 长度错误

从上面我们初步判断是长度的问题。初步查看模版的代码,我们知道长度在模版中使用的是WordCount变量。于是我们查看hugo中WordCount相关的代码hugolib/page.go:

if p.isCJKLanguage {
        p.WordCount = 0
        for _, word := range p.PlainWords() {
            runeCount := utf8.RuneCountInString(word)
            if len(word) == runeCount {
                p.WordCount++
            } else {
                p.WordCount += runeCount
            }
        }
    } else {
        p.WordCount = len(p.PlainWords())
    }

从上面isCJKLanguage的变量名我们就能猜到是判断非英文的路径。简单看下代码,Hugo是有考虑CJKLanguage的问题的,也就是说可能是这个分支没有执行。

4. isCJKLanguage

我们搜索工程,找到isCJKLanguage:https://github.com/spf13/hugo/blob/2c5e4f7640e71d2a193a74e6c41109ec40bc0222/docs/content/content/front-matter.md 中的介绍,于是手动在出错的markdown文件中添加此变量,再次重新执行hugo,发现对应生成的网页中的字数统计的值正确了,分享也不出错了,说明就是字数的统计导致了系统错误。

问题验证了,但是因为博客的文章都是用工具自动从wordpress中生成的,不可能手动一个一个修改,修改工具也显得很死板,最好的方法是从Hugo这侧来找方案。

5. hugo配置文件

顺着上面的思路,开始搜索相关的关键字,发现hugo的配置中还真有相关的配置hasCJKLanguage:https://github.com/spf13/hugo/blob/b7efbdc12f0a96639b445f7920b6477d88beb744/docs/content/overview/configuration.md ,喜出望外,赶紧添加到config文件中重新编译文件,编译后发现并没有生效。于是再次查看源码hugolib/page.go:

if isCJKLanguage != nil {
        p.isCJKLanguage = *isCJKLanguage
    } else if viper.GetBool("HasCJKLanguage") {
        if cjk.Match(p.rawContent) {
            p.isCJKLanguage = true
        } else {
            p.isCJKLanguage = false
        }
    }

从上面的代码看isCJKLanguage 是由HasCJKLanguage开关控制的。

6. HasCJKLanguage

继续看代码,我们看到工程中只有一个地方有设置commands/hugo.go:

viper.SetDefault("HasCJKLanguage", false)

于是尝试修改本地的此行代码,重新编译Hugo。再次用hugo生成所有文章,发现所有文章的长度都正确了,甚是高兴。

再次检查今天发现的分享出错的文章,都全部正常了,舒服!

小米路由器R1D刷机手记

update

2016.12.16:由于文章中的360 网盘的资源挂了,重新更新了tomato的刷机包的路径

缘由

小米路由器R1D已经使用了快一年,一直想刷机,主要因为:

  • 后门问题,请搜索知乎“小米路由器 劫持”,虽然严格讲天朝人民是没有什么隐私,但被人强奸总是很郁闷的。
  • 用了半年后,经常会掉网,很快就自动恢复,有时候莫名其妙手机就掉网开始走移动流量,很郁闷。

    小米路由器R1D

    小米路由器R1D是一款基于博通bcm4709( c )和OpenWRT深度定制的智能路由器。

准备工作

  • 了解基本的linux 常用命令,ssh,root权限等。
  • usb一个,用来制作usb刷机工具。
  • 备份你的拨号账号密码。
  • 一个备用的路由器,刷机有风险,一旦出现变砖,备用路由器至少可以让你继续使用网络,不至于影响正常的网络使用。
  • 一米的短网线,用来直连路由器,当然理论上wifi也是可以的,但是某些rom在进入特定的刷机工具状态时wifi是不可用或者启动有延迟的,有线则方便很多。

刷机的ROM备选

小米路由器是深度定制版的OpenWRT,网上大概搜了一下,可选的有OpenWRT原生系统,DD-WRT和Tomato三种系统可以尝试。

OpenWRT

ROM

首先从官方的硬件支持列表我们并没有看到R1D的支持:https://wiki.openwrt.org/toh/start 。其次从小米论坛和google也没有找到明确支持的rom。

刷机

巧妇难为无米之炊。
结果:放弃

DD-WRT

ROM

从官方的支持列表中:http://dd-wrt.com/wiki/index.php/Supported_Devices ,我们并没有看到R1D的支持ROM。但是我们可以找到非官方的rom,例如:http://bbs.xiaomi.cn/t-10691878

刷机

刷机流程参见:http://www.right.com.cn/forum/thread-143121-1-1.html ,本人尝试多次,均在第6步失败,初步判断是tftp server没有正常启动。

分析原因:原博是在0.4.58的rom上做的验证,本人尝试0.4.58版的USB和web页面升级的方式,均失败。web直接校验失败,USB刷机后,路由器无法正常启动,于是只能采用的最新开发版来验证。猜想是小米硬件做了升级限制或者在软件层面有专门对这个漏洞做修复。

结果:失败。如果有最近刷机成功的兄弟,也望留言指教。

Tomato

ROM

AdvancedTomato是一套基于Tomato的路由器管理界面,其核心是tomato。这个是tomato对应R1D的下载路径:

  1. http://tomato.groov.pl/download/K26ARM

  2. https://bitbucket.org/tsynik/tomato-arm/downloads

本次刷机实际使用的是这个:http://bbs.xiaomi.cn/t-11522784 ,因为有人验证过,并且支持中文,相对风险会小些,实际效果有待日后进一步观察。

说明:

因为文章里面的360的网盘挂了,特将rom重新发布:

链接: http://pan.baidu.com/s/1o8TRooE 密码: qe4j

因为本人验证时已经是很久以前的事情了,所以建议看到的朋友在上面的路径下载最新的版本尝试。

刷机

  1. 升级到最新开发版 :http://miwifi.com/miwifi_download.html
  2. 开ssh,http://www1.miwifi.com/miwifi_open.html
  3. ssh 192.168.31.1,ifconfig得到网卡MAC地址(或者刷机之前在web页面记录下MAC)
  4. 下载tomato固件,见前面的rom部分内容 。
  5. 下载tomato刷机包
  6. 打开CFEEdit.exe,通过菜单打开cfe.bin,修改其中的MAC地址为你的小米路由器的MAC地址,改好后覆盖。
  7. 将cfe.bin、backup.sh、flash-cfe.sh这三个文件放入U盘,插入路由U口。
  8. ssh登录路由器,登陆的账户名和密码参见 http://www1.miwifi.com/miwifi_open.html 。执行:
    nvram set boot_wait=on
    nvram set wait_time=10
    nvram commit
    进入U盘目录:cd /extdisks/sdb1 ,此路径可能会不同,请自行确认。执行:
    ./backup.sh
    ./flash-cfe.sh
    先备份系统文件,在必要时可以手动恢复。
  9. 电脑有线ip设置为192.168.1.1段,通过有线连接局域网端口。
  10. 重启路由,浏览器打开192.168.1.1,刷入tomato固件即可。

刷回小米系统

  1. 将miwifi-stock.bin和小米官网下载的小米路由器最新版U盘刷机包(改名我miwifi.bin)放到U盘中
  2. ssh登录小米路由器,执行命令:
    mtd-write2 /mnt/U盘/miwifi-stock.bin linux
  3. 按复位键,重新接电源,等黄灯闪烁后松开复位键
  4. 几分钟后黄灯常亮,拔下U盘
  5. 浏览器登录小米路由器,通过web页面更新最新开发版。

结果:成功。
启示:按照CFE的思路,理论上也可以来刷DD-WRT,但已经折腾一天了,改日有机会再试吧。

AdvancedTomato 基本安全配置

  1. 修改界面语言,修改管理员名称和密码:系统管理 - 管理员访问
  2. 设置宽带拨号,DHCP,wifi的SSID和密码:基础设置 - 网络
    注意:
    WAN设置:类别为PPPoE,不要勾选“使用DHCP”。
    LAN:默认的DHCP是有问题的,要设置正确的值,客户端才能正确使用外网。
    无线网络:设置WPA/WPA2密码。
  3. 如果为了进一步的安全,建议开启无线过滤:基础设置 - 无线过滤 - 允许以下客户端。手动添加需要wifi访问权限的设备,避免有人搞小动作

AdvancedTomato 服务配置

  1. USB&NAS
    通过设置,我们可以外接USB硬盘了,这个对某些人似乎有点用。
  2. 文件共享
    删除默认的共享,设置你要共享的目录,记得设置身份验证。这样你就可以在局域网使用路由器的1T硬盘了
  3. DLNA服务器
    如果你想在bt下完电影后,直接通过电脑或者手机观看,就要设置这个。设置一下媒体文件的目录,设置完后,DLNA客户端就可以直接在局域网发现媒体资源了。如果需要ios的客户端,你可以在APP store搜索Arkuda。
  4. BitTorrent客户端
    开启后,你可以通过路由器页面给路由器下发bt任务了,它会下载到你指定的路径,如果这个路径支持DLNA,那么下载完,你就可以直接观看大片了。
  5. DDNS+端口转发
    如果你要在远程通过互联网来玩bt,那么就需要继续设置DDNS+端口转发。

最后

如果你是其他型号的路由器,只要是刷相同类型的rom,流程基本相似,希望有帮到大家,也欢迎大家交流。
最后,有图有真相:

AdvancedTomato

刷完后,感觉整个世界都平静了。

其他参考

http://blog.icece.tw/Xiaomi-R1D-1TB-Flash-Tomato

补充:

  1. 关于发热的问题,我觉得原版其实就发热,我自己尝试的135的版本,刷机后感觉网速和发热并没有改善,期望新尝试的朋友继续反馈

LoRa技术介绍

目前看,LoRa是一个在IOT方案领域替换GSM的初步可行方案,主要从距离和功耗两个考虑方面。

介绍如下:
http://www.lora-alliance.org/What-Is-LoRa/Technology
http://www.rfwireless-world.com/Terminology/LoRa-technology-basics.html
http://www.rfwireless-world.com/Terminology/IoT-wireless-technologies.html

芯片厂商目前也已经有了量产的芯片可供评估,成本在1-2个美金。
http://www.microchip.com/design-centers/wireless-connectivity/embedded-wireless/lora-technology
http://www.semtech.com/wireless-rf/lora.html

IPSec生成证书

在很多安全服务中,我们都需要签名的安全证书。

注意 如果不生成证书,后面配置部分的配置中用 pubkey 认证的 conn 都不能用,甚至不能保留在配置中。
每一个完整的 ssl 证书都有一个公钥和一个私钥,它们可以在一起也可以分开放(当然如果你要在网络上传输,肯定只能用公钥)。公钥是在网络上传输的,而私钥是藏好用来和接收到的公钥配对的(因此私钥里也有整个公钥,用来配对)。

生成 CA 证书

生成一个私钥:

ipsec pki --gen --outform pem > ca.pem

没什么好解释的,--outform 一共有三个格式可选,但是另外两个是 der 和 pgp...

基于这个私钥自己签一个 CA 证书:

ipsec pki --self --in ca.pem --dn "C=CN, O=strongSwan, CN=strongSwan CA" --ca --outform pem > ca.cert.pem

这里 --self 表示自签证书,--in 是输入的私钥,--dn 是判别名,--ca 表示生成 CA,其它同上。这里需要解释下判别名:

C 表示国家名,同样还有 ST 州/省名,L 地区名,STREET(全大写) 街道名。
O 表示组织名。
CN 为通用名。
具体见微软的文档:Distinguished Names

生成服务器证书

同样生成私钥:

ipsec pki --gen --outform pem > server.pem

用我们刚才自签的 CA 证书给自己发一个服务器证书:

ipsec pki --pub --in server.pem | ipsec pki --issue --cacert ca.cert.pem \
--cakey ca.pem --dn "C=CN, O=strongSwan, CN=forum.suse.org.cn" \
--san="forum.suse.org.cn" --flag serverAuth --flag ikeIntermediate \
--outform pem > server.cert.pem

这条命令的意思解释下:

ipsec pki --pub --in server.pem
是从我们刚生成的私钥里把公钥提取出来,然后用公钥去参与后面的服务器证书签发(这个是 VPN 连接时候要用的,你不想把私钥也给它吧?那样跟没签证书一样...)。

--issue, --cacert 和 --cakey 就是表明要用刚才自签的 CA 证书来签这个服务器证书。

--dn, --san,--flag 是一些客户端方面的特殊要求:

iOS 客户端要求 CN 也就是通用名必须是你的服务器的 URL 或 IP 地址;
Windows 7 不但要求了上面,还要求必须显式说明这个服务器证书的用途(用于与服务器进行认证),--flag serverAuth;
非 iOS 的 Mac OS X 要求了“IP 安全网络密钥互换居间(IP Security IKE Intermediate)”这种增强型密钥用法(EKU),--flag ikdeIntermediate;
Android 和 iOS 都要求服务器别名(serverAltName)就是服务器的 URL 或 IP 地址,--san。

生成客户端证书

依然是生成私钥:

ipsec pki --gen --outform pem > client.pem

然后用刚才自签的 CA 证书来签客户端证书:

ipsec pki --pub --in client.pem | ipsec pki --issue --cacert caCert.pem \
--cakey caKey.pem --dn "C=CN, O=strongSwan, CN=client" \
--outform pem > client.cert.pem

这时命令行会提示你输入两遍密码,这个就是你的客户端证书密码。

看懂了服务器的,客户端的也就不难理解了。除了没有那一堆特殊要求别的都一样。

客户端证书可以每个客户端签一个,也可以让它们公用一个。是否多签看用途,一般用于区分设备(计费是不用这样的,是用账户来区分的)。

生成 pkcs12 证书(可选)

你可能还想生成一个可以直接导入的 pkcs12 证书(用于手机,诺基亚没这东西还不行):

openssl pkcs12 -export -inkey client.pem -in client.cert.pem -name "client" \
-certfile ca.cert.pem -caname "strongSwan CA" -out client.cert.p12

安装证书

cp -r ca.cert.pem /etc/ipsec.d/cacerts/
cp -r server.cert.pem /etc/ipsec.d/certs/
cp -r server.pem /etc/ipsec.d/private/
cp -r client.cert.pem /etc/ipsec.d/certs/
cp -r client.pem /etc/ipsec.d/private/

CA 证书、客户证书(两个)和 .p12 证书用 FTP 复制出来给客户端用。有几种 Android 配置还需要服务器证书(server.cert.pem)。

docker中的”layers”指的是什么东西

在很多的docker的说明中,我们都能看到“layer”的身影,那么到底什么是”layer“?

参见http://docker-doc.readthedocs.org/en/latest/terms/layer.html中的说明:

When Docker mounts the rootfs, it starts read-only, as in a traditional Linux boot, but then, instead of changing the file system to read-write mode, it takes advantage of a union mount to add a read-write file system over the read-only file system. In fact there may be multiple read-only file systems stacked on top of each other. We think of each one of these file systems as a layer.

简单翻译如下:
当docker加载rootfs时,和传统的linux系统中一样,它是只读的,但是随后,不是将当前文件系统变成可读写,而是利用union mount添加一个可读写的文件系统在当前的只读系统上。实际上将会有很多的只读文件系统叠加在彼此之上。我们将这里的每一层文件系统称之为"layer"。

参考阅读:
https://docs.docker.com/engine/understanding-docker/#how-does-a-docker-image-work
https://www.ctl.io/developers/blog/post/caching-docker-images/

使用docker中应避免的10件事

docker好文,短小精炼。水平有限,请不吝指教。

原文

http://developerblog.redhat.com/2016/02/24/10-things-to-avoid-in-docker-containers/

翻译正文

ps:为了保证一致性,文中的容器即container,是docker container的简称

最终,你选择了容器技术并且发现它解决了很多问题并且有着很多的优点:
1. 第一:不可变更-因为OS,库文件,配置参数,文件夹,应用等都包含在容器中,所以只要你保证你生产环境中使用的是和QA一样的镜像,那么它的行为就一定是一样的。
2. 第二:轻量级-容器的内存占用很小。不需要成百上千MB,只需要为主进程分配数十MB的内存就可以。
3. 第三:运行超快-启动容器就像开启一个单进程,不需要几分钟,只要短短几秒就可以完成。

但是很多用户仍然将容器仅仅当作虚拟机使用,而另外的人仅仅知道:容器是一次性的,而忘记了它还有其他很多优点。

关于容器的口头禅:容器是短暂的。

这一特性“本身”迫使用户去改变他们关于如何处理和管理容器的想法。而我也将阐述为了获取最佳性能而需要避免做的事情:

1)不要在容器中存储数据-容器可以被停止,销毁,或者替换。一个1.0版的应用应该很容易的就被替换成1.1而不应有任何影响和数据丢失。基于这个原因,如果你需要存储数据,把它存储在宿主的硬盘上,但是要避免两个容器同时写数据,造成数据异常,确保你的应用是将数据写到共享的数据路径的。

2)不要把你的应用分成两部分-因为很多人把容器当虚拟机使用,所以他们的做法是部署应用到已经在运行的容器中。也许开发中确实如此,因为你要不间断的部署和调试,但是在正式的QA和生产环节流程中,你的应用应该是容器的一部分。请记住:容器应该是不可变更的。

3)不要创建过大的镜像-镜像过大不易分发。请确认你只包含了必要的文件和库,不要包含不必要的开发包,也不要在容器运行时运行"updates"(yum update)。

4)不要使用单层的镜像-合理的使用分层的文件系统,OS是一层,安全和用户定义组件是一层,库文件是一层,配置文件是一层,最后是你的应用独立为一层。这样将使镜像的重建和管理变的容易同时易于分发。

译者:这里的层指得是"layer",关于什么是"layer",请参见docker中的”layers”指的是什么东西

5)不要从运行的容器中创建镜像-换个说法,不要用"docker commit"去创建镜像。这样创建的镜像不可准确重建和版本化管理,应该彻底避免。要使用Dockerfile或者其他可重建的S2I(source to image)方式。

6)不要只使用最新标签的版本-最新的标签就想maven中的"SNAPSHOT"。应该使用特定的标签的版本。我想你应该不想在你2个月后重新编译镜像时,收到惊奇,发现你的应用不能运行是因为顶层的layer使用了新的版本,没有向前兼容或者最新的版本是有问题的。当部署容器到生产环境时也要避免使用新的版本。

7)不要在一个容器中运行多于1个的进程-容积最好是运行单一的进程(http守护进程,应用服务器,数据库)。当你运行多个时,你将会遇到要单独管理,处理日志,以及更新的困难。

8)不要存储身份数据到镜像中,尽量使用环境变量-你应该不希望直接存储你的用户名,密码到镜像中。所以使用容器外的环境变量去获取相关的信息,一个较好的例子是postres image

9)请使用非root权限运行进程-“docker默认是以root身份运行的。当docker运行时,将会获取到默认的安全选项。而被他人获取root是危险的,并且也不是所有环境都支持的。你的镜像应该使用 USER 指令去指定一个非root用户来运行容器”(摘自Guidance for Docker image Authors)。

10)不要依赖特定的IP-每一个容器都有它们自己的内部ip,而且这个ip可能在开启或者停止时变化。假如你的应用或者微服务需要和其他的容器通讯,请使用名称或者环境变量去传递相关的信息。

更多关于容器的信息请参见:http://developers.redhat.com/containers/

通过wordpress钩子实现增删改文章后执行相应动作

How to use wordpress hook functions to follow the file change events

wordpress hook

hook是wordpress实现功能扩展的重要手段,详细的介绍:
hook functions is the important methods of extention of wordpress, reference to:
Plugin API

hook主要分两类:actions 和filters。
There have two catagory of hooks: actions add filters.
对应的注册函数分别为:add_action()和add_filter()。

目前支持的hook类型参见:
The details of hook cattagory refoerence to:
http://adambrown.info/p/wp_hooks

例子(Some examples)

  • 在模板的基础函数文件中(Modify the theme's style file):
    /data/wp/wordpress/wp-content/themes/premium-style/functions.php

  • 新增以下代码( Add some codes as bellow):

    function publish_post_extra($post_ID){
    // 1. init curl object
    $ch = curl_init();
    // 2. settings
    curl_setopt($ch, CURLOPT_URL, "https://xxx" . $post_ID);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    // 3. execute the curl function
    curl_exec($ch);
    }
    add_action('publish_post', 'publish_post_extra');
    add_action('deleted_post', 'publish_post_extra');

以上的代码实现动作发生时, 通过curl访问一个特定url。
The uper codes show the publish and delete hook fuction that triger the specific http get request through the curl lib.

Docker容器的数据管理

这两天开始学习docker,发现docker确实很强大,让网站部署和维护的效率大大提高。遂准备将手头维护的几个小站,全部docker化。整理的过程中感觉到,docker可以以功能或者进程为单位进行部署和维护,不用再花时间在繁琐的配置上面,但是docker和宿主之间的数据共享以及docker间的数据共享仍然是让人头疼和操心的地方。正好翻到官方文档,看到相关内容,遂决定翻译一下,水平有限,欢迎吐槽。

几个基本概念:
docker: 一种容器管理技术,这里也指既有的开发工具链。
container: 容器
image: 镜像
volum:卷 [译者:卷可以理解成计算机中的文件路径]

原文链接:Manage data in containers

翻译正文:

之前我们介绍了docker基本概念,学习了docker 镜像如何工作以及docker之间的网络和联系。这章节我们将继续讨论怎么在docker内和docker之间管理数据。

我们将着重讨论两种你所能管理docker数据的方式

  • 数据卷
  • 数据卷容器

数据卷

数据卷是一种特殊的存在于一个或者多个docker内部的不同于Union File System的目录。数据卷提供多种有用的特性用来持久化和共享数据:

  • 数据卷在docker初始化时创建。如果容器的镜像包含外挂的数据,外挂的数据将在卷初始化时被拷贝到新的本地卷。
  • 数据卷可以被共享和在多个docker间复用。
  • 可以对数据卷直接修改。
  • 更新镜像时数据卷并不受影响。
  • 即使镜像被删除,数据卷也仍然会持久化到本地。

数据卷被设计用来持久化存储数据,独立于容器的生命周期。当你删除容器时,docker并不会自动删除数据卷,不使用的数据卷,也不会替你“垃圾回收”。

增加一个数据卷

你可以通过 -v 标示在 docker create 和 docker run 命令中给容器增加一个数据卷。你可以多次使用 -v 增加多个数据卷。让我们给我们的web应用容器挂载一个单独的数据卷。

$ docker run -d -P --name web -v /webapp training/webapp python app.py

这条指令将会在容器内部的 /webapp 路径下创建一个新卷。

注意:你也可以使用 VOLUME 指令在 Dockerfile 文件中添加一个或者多个卷到容器中。
docker中的卷默认是读写权限,但你也可以设置为只读。

$ docker run -d -P --name web -v /opt/webapp:ro training/webapp python app.py

查看一个卷

你可以使用 ‘docker inspect’ 指令来查看一个卷。

$ docker inspect web

输出将会提供详细的容器配置和卷信息。输出格式类似如下:

Mounts": [
    {
        "Name": "fac362...80535",
        "Source": "/var/lib/docker/volumes/fac362...80535/_data",
        "Destination": "/webapp",
        "Driver": "local",
        "Mode": "",
        "RW": true
    }
]

你将看到‘Source’表示的是宿主路径,‘Destination’表示的是容器路径。 RW 用来标示这个卷的读写属性。

将宿主目录挂载为数据卷

通过 -v 标示你可以挂载一个宿主目录到容器中。

$ docker run -d -P --name web -v /src/webapp:/opt/webapp training/webapp python app.py

这条命令将宿主的 /src/webapp 挂载到容器的 /opt/webapp 卷上。如果 /opt/webapp 已经存在,/src/webapp 将会覆盖但是不会移除已有的文件。当挂载的卷移除后,原先被覆盖的内容将可以再次使用。这个和mount的行为是一致的。

container-dir 必须是绝对路径,比如 /src/docs。host-dir可以是绝对路径,也可以是一个已经声明过的数据卷。如果你指定host-dir为绝对路径,docker将会按你指定的路径挂载,如果你提供的是一个声明的卷,docker将会按照name 指定的名称创建一个声明的卷。

一个 name 声明的卷必须以字母开头,后面跟随z-z0-9,_,. 或者 -。绝对路径都以 / 开始。

例如,你可以用/foo 或者 foo 作为一个 host-dir.如果你使用 /foo ,docker 创建一个挂载点。如果你使用 foo, docker 创建一个声明的卷。

如果你在 Mac 或者 Windows 上使用 docker,你的docker后台只能拥有有限的权限。docker尝试着自动分享你的 /User 或者 C:\Users 目录,所以在OS X上挂载如下:

docker run -v /Users/<path>:/<container path> ...

windows上如下:

docker run -v /c/Users/<path>:/<container path> ...

其他来自虚拟机的目录,比如你想共享virtualbox 中的某些目录,你需要做些额外的工作。在 virtualbox下,你先要使宿主的目录变成共享,然后才能使用 -v 来挂载。

挂载宿主的目录对测试来说很有用。比如你可以在容器中挂载源码,然后修改代码,实时看修改后的执行效果。宿主的路径必须是绝对路径,如果路径不存在,docker就会在本地创建它。这种 auto-creation 将会被移除。

这里我们仍然挂载 /src/webapp 目录,但是增加了ro选项来声明挂载的目录是只读的。

$ docker run -d -P --name web -v /src/webapp:/opt/webapp:ro training/webapp python app.py

注意:宿主目录是主机相关的,所以你不能在Dockerfile中挂载一个宿主目录,因为镜像要可移植,而不同的主机有不同的目录结构。

卷标识

Labeling 系统例如 SELinux 要求数据卷被容器挂载时要标示。如果没有标示,安全系统可能会阻止容器内的进程访问卷的内容。默认情况下,docker不会改变操作系统的卷标识。

要在容器的上下文中修改卷标识,你需要在挂载时增加 :z 或者 :Z 标识。z 表示多个容器共享卷内容,所以docke用共享标识来标示卷内容,共享的标识允许多个容器读写内容。Z 表示docker用私有的标识来标示卷内容,所以只有当前的容器能访问这个私有的卷内容。

挂载宿主的文件作为卷

-v 也可以用来挂载一个单独的文件而不仅仅是目录:

$ docker run --rm -it -v ~/.bash_history:/.bash_history ubuntu /bin/bash

这个指令将会让你进入容器的命令行,能获取宿主的历史纪录,并且退出时,宿主的历史记录能纪录容器的指令操作。

注意:许多编辑工具包括 vi ,sed --in-place 等可能会导致inode数据结构变更。自从docker1.1.0后,此类操作会导致" sed:cannot rename ./sedKdj9Dy:Device or resource busy"。[In the case where you want to edit the mounted file, it is often easiest to instead mount the parent directory.][译者:这句不是很明白什么意思,望看到的网友指点]

创建和挂载一个数据卷容器

如果你有一些希望分享给多个容器的持久化数据,或者想从 non-persistent 容器中使用这些数据,最好的办法是创建一个命名的数据卷容器,然后从从容器中挂载使用数据。

让我们创建一个共享数据卷的容器。这个容器不运行应用,它提供 training/postgres 镜像,这样所有的容器都共用统一的接口,从而节省空间。

$ docker create -v /dbdata --name dbdata training/postgres /bin/true

你可以在其他的容器中用 --volumes-from 标示来挂载 /dbdata 卷。

$ docker run -d --volumes-from dbdata --name db1 training/postgres

$ docker run -d --volumes-from dbdata --name db2 training/postgres

这个例子里,如果 postgres 镜像已经包含一个叫做 /dbdata 的目录,将会隐藏这个目录,只有挂载的可见。

你可以多次使用 --volumes-from 从多个容器中挂载多个卷。

你也可以用 db1 db2 来引用 dbdata。

$ docker run -d --name db3 --volumes-from db1 training/postgres

如果你删除挂载了卷的容器,包括初始的dbdata 或者子序列的 db1 db2等,这些卷将不会被删除。如果你要删除卷,你需要明确的调用 docker rm -v,这个操作可以让你去在容器之间更新,或者高效的迁移数据。

注意:当你删除一个容器不带 -v 时,docker不会警告提醒你。当你不带 -v 删除容器时,将会出现‘挂起’的数据卷,这些卷不再被容器引用。‘挂起’的卷很难避免,并且占用空间。我们在尽力提升卷管理功能,参见pull request #14214

备份,恢复,迁移数据卷

数据卷的另外一个有用的功能是用来备份,恢复和迁移数据。我们使用 --volumes-from 来创建一个新容器并挂载数据卷:

$ docker run --volumes-from dbdata -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

示例中我们启动了一个新容器并且从dbdata 容器挂载了数据卷。我们把宿主本地路径挂载成 /backup.最后,我们用tar命令将dbdata数据卷备份进backup.tar,并存入 /backup目录。当命令结束时,我们将在本地目录下得到一个dbdata的备份。

你可以用它来恢复到之前备份的容器里,或者其他的容器:

docker run -v /dbdata --name dbdata2 ubuntu /bin/bash

然后 un-tar 备份的文件到新的容器数据卷中:

$ docker run --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar"

你可以使用上面的方法来自动化地备份迁移和恢复数据。

使用共享卷的注意点

多个容器可以共享一个或者多个数据卷,但是同时写入的时候会发生冲突。

数据卷在宿主里面是可以直接操作的。你可以使用普通的linux工具操作它们。但是建议你不要这样直接做,因为容器和应用并不知道你的操作,这可能会导致数据操作冲突。

下一步

我们学习了很多关于怎么使用docker,接下来我们将看到怎么将docker和Docker Hub上的服务例如自动编译,创建私有仓库等结合起来使用。

Go to Working with Docker Hub

完!

译者:本文中的部分命令参数是过时或者错误的,但这些可以忽略,并不影响我们理解指令的正确用法。

debian7系统安装docker

起始

最近开始做docker相关的学习和技术准备,准备将目前的一些基础服务,通过docker做同构或者异构分布式调整,提升运维效率和系统稳定性。

安装

我的系统:

Linux chicago1 3.2.0-4-amd64 #1 SMP Debian 3.2.41-2 x86_64 GNU/Linux

Docker要求Kernel 3.8+,幸运的是, wheezy-backports 目前有Kernel 3.16 , 该版本正式支持Docker。

升级系统

  • 从wheezy-backports安装内核

在文件 /etc/apt/sources.list中添加如下行

deb http://http.debian.net/debian wheezy-backports main

然后安装linux-image-amd64包 (注意使用 -t wheezy-backports)

sudo apt-get update
sudo apt-get install -t wheezy-backports linux-image-amd64
  • 重启你的系统。对于Debian来说使用新内核是必要的。

安装Docker

使用get.docker.com 的脚本:

curl -sSL https://get.docker.com/ | sh

卸载

为了卸载Docker包:

$ sudo apt-get purge lxc-docker

为了永久卸载Docker及其依赖包,你应该这样:

$ sudo apt-get autoremove --purge lxc-docker

命令将不会移除镜像,容器,数据卷,或者用户创建的配置文件。如果你希望删除所有镜像,容器,数据卷 ,运行如下命令:

$ rm -rf /var/lib/docker

你必须手动删除用户创建的配置项。

一键迁移wordpress到hugo

网站已经迁移了一个多月,一直想写下,但是因为一个issue的问题,一直不爽快:
the tag format
一直想等待解决再来写。

周末抽空过了一遍代码,代码中是用spyc来生成yaml的。改成[xx, x] 的格式还是要费点神的,而且破坏了原来格式的简单优美,什么意思?看下两种格式的生成就明白了,前者是不用考虑长度的迭代模式,后者要考虑长度,因为要在结尾加后括号']',这样逻辑就不是简单流式的了。为了确认是否值得修改,确认了官方的文档:
yaml官方文档
发现2种格式都是支持的,但是hugo的问题是:
文档的tags和categories本质来讲,都是列表数组,没有道理两种采用不同的格式,详情:
the tag format advice
显然关闭的人并没有理解我的意思,但是还是有其他的开发者明白了我的意思,做了回复,总之此问题目前确认是属于hugo的“不优雅”。不打算勉强,为了修复而修复,静待hugo的变化吧。

hugo

hugo是一个能把markdown转化成静态html网站的工具。

wordpress-to-hugo-exporter

说了这么多,言归正传,一键切换主要是使用这个插件:
wordpress-to-hugo-exporter
这个插件功能是通过提取wp的页面,将所有的可见页面转换成markdown文档。

生成md文件

支持两种方式:
1. 在wp插件页面通过按钮执行
2. 在后台命令行通过cli方式执行:

php hugo-export-cli.php

前者的文件会导出到用户客户端,后者生成的.zip在/tmp目录下。

后继的工作

这个插件只是将wp的内容转化成markdown文档,要创建hugo网站,你需要:

  • 找一个你喜欢的theme,从hugo官网就能找到
  • 根据theme的指导,配置相应的config文件,每一个theme的config会有略微差异
  • 将之前的markdown打包文件根据你自己配置的目录层次解压到hugo的content目录下的对应路径中。
    如果以上你都做对了,你应该已经可以看到你的hugo网站了。
    相关参考:
    一步一步教你用hugo搭建博客

中文支持的PR

拿到插件时,是不支持中文路径的,含有中文路径的文章,都是404。这个对大陆用户来说很不方便的,因为有很多人的文章都是有中文路径的。于是提交了对应的PR:
fix the unicode url issue and markdown format error
作者很勤快,很快就合入了主干,所以现在大家可以直接使用主干的代码,就是支持中文了。

selinux相关的permission-denied问题

起因

最近将系统更新到了centos7,由于centos7 采用systemctl 替代了原有server相关的服务管理接口。当使用

systemctl start nginx.service

时,系统提示出现错误,请运行:

systemctl status nginx.service -l

-l 表示显示完整的提示日志,否则会出现提示日志缩略的问题。运行status指令时,出现下面日志:

[root@server]# systemctl status nginx.service -l
nginx.service - The nginx HTTP and reverse proxy server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled)
   Active: failed (Result: exit-code) since Fri 2016-01-08 11:56:09 EST; 30min ago
  Process: 704 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=1/FAILURE)
  Process: 698 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)

Jan 08 11:56:09 chicago1 nginx[704]: nginx: [emerg] BIO_new_file("/xxx/xxx.crt") failed (SSL: error:0200100D:system library:fopen:Permission denied:fopen('/xxx/xxx.crt','r') error:2006D002:BIO routines:BIO_new_file:system lib)
Jan 08 11:56:09 chicago1 nginx[704]: nginx: configuration file /etc/nginx/nginx.conf test failed
Jan 08 11:56:09 chicago1 systemd[1]: nginx.service: control process exited, code=exited status=1
Jan 08 11:56:09 chicago1 systemd[1]: Failed to start The nginx HTTP and reverse proxy server.
Jan 08 11:56:09 chicago1 systemd[1]: Unit nginx.service entered failed state.

分析

因为看到'Permission denied',所以先检查文件权限,ll指令查看,文件的权限是没有问题。想起的了之前遇到的部署nginx时,部署在自定义目录下不行,部署在nginx默认安装目录'/usr/share/nginx/html' 下就可以的问题。当时也是直接ls查看文件属性,两边的文件权限和归属都是相同的。

今天再次遇到这个问题是,觉得两次应该是同一个问题,所以有必要彻搞清楚,因为是权限问题,所以还是要从权限策略方面入手,于是想到selinux,在之前的系统中默认都是关闭的,但在新系统中我们并没有关闭。于是确认下:

[root@server]# sestatus
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Max kernel policy version:      28

于是我们查看下selinux策略配置:

ls -lrtZ /usr/share/nginx/html

显示如下:

-rw-r--r--. root root system_u:object_r:usr_t:s0

而我们自己的目录:

-rw-r--r--. root root unconfined_u:object_r:default_t:s0

解决

于是修改策略:

chcon -R -u system_u /xxx/
chcon -R -t usr_t /xxx/

重新执行,发现还是失败,最终确认,需要重启后,才能生效。

一线之间

英文名: "A Fine Line"

应了那句“书非借不能读也”的俗语,本人很少涉猎非计算机书籍,但是因为是朋友的推荐,竟然很快地读完了此书。
作者是一位有着多年设计经验参加过无数伟大公司经典产品设计的设计师和创业者,创立了著名的“青蛙”公司。

觉得值得推荐,主要因为以下几点:

  • 翻译很到位,阅读很流畅舒服。
  • 作者的经验总结都来源于实际的案例,涉及国际一线公司,从而不会让人觉得空洞而无趣。
  • 作者的境界很高,将创意设计和商业成功,公司竞争力,外包模式,全球环境等联系在了一起,描述了其关联和相互作用,是一本让人很惊醒和受启发的好书。

此书适合的群体对象:设计师,产品经理,决策管理层,创业厂长们。

更多参见:
豆瓣地址:http://book.douban.com/subject/3865302/
其他:http://www.madisonboom.com/2012/11/10/book-of-a-fine-line-by-hartmut-esslinger/

参考封面:

正则学习笔记

简介

如果做文本处理,日志运维,数据分析,爬虫等相关工作的同学,一定离不开的一个工具就是正则表达式。正则可以从一个完整的字符串中查找或者提取到具有相同“模式”的字符串子串。

什么是正则表达式

正则就是描述文本规则的代码,例如: *.doc 表示所有以doc后缀结束的文件名,这就是一个简单的正则。

基本模式

正则 含义
[abc] A single character of: a, b or c
[^abc] Any single character except: a, b, or c
[a-z] Any single character in the range a-z
[a-zA-Z] Any single character in the range a-z or A-Z
^ Start of line
$ End of line
\A Start of string
\z End of string
. Any single character
\s Any whitespace character
\S Any non-whitespace character
\d Any digit
\D Any non-digit
\w Any word character (letter, number, underscore)
\W Any non-word character
\b Any word boundary
(...) Capture everything enclosed
(a/b) a or b
a? Zero or one of a
a* Zero or more of a
a+ One or more of a
a{3} Exactly 3 of a
a{3,} 3 or more of a
a{3,6} Between 3 and 6 of a

元字符

元字符是正则中的具有独立完整意义的字符,类似变成语言中的保留字,只不过保留字一般是多字符的。

代码/语法 说明
. 匹配除换行符以外的任意字符
w 匹配字母或数字或下划线或汉字
s 匹配任意的空白符
d 匹配数字
b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束

例子说明:
元字符 ^(和数字6在同一个键位上的符号)和 $ 都匹配一个位置,这和 \b 有点类似。^ 匹配你要用来查找的字符串的开头,$ 匹配结尾。这两个代码在验证输入的内容时非常有用,比如一个网站如果要求你填写的 QQ 号必须为 5 位到 12 位数字时,可以使用:^\d{5,12}$ 。
这里的 {5,12} 和前面介绍过的 {2} 是类似的,只不过 {2} 匹配只能不多不少重复 2 次,{5,12} 则是重复的次数不能少于 5 次,不能多于 12 次,否则都不匹配。
因为使用了 ^ 和 $,所以输入的整个字符串都要用来和 \d{5,12} 来匹配,也就是说整个输入必须是 5 到 12 个数字,因此如果输入的QQ号能匹配这个正则表达式的话,那就符合要求了。

字符转义

如果你想查找元字符本身的话,比如你查找 . 或者 *,就出现了问题:你没办法指定它们,因为它们会被解释成别的意思。这时你就得使用\ 来取消这些字符的特殊意义。因此,你应该使用 \. 和 \* 。当然,要查找 \ 本身,你也得用 \\。
例如:unibetter\.com 匹配 unibetter.com,C:\\Windows 匹配 C:\Windows。

重复

常用的表示符

代码/语法 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次

字符类

要想查找数字,字母或数字,空白是很简单的,因为已经有了对应这些字符集合的元字符,但是如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办?
很简单,你只需要在方括号里列出它们就行了,像 [aeiou] 就匹配任何一个英文元音字母,[.?!] 匹配标点符号 (. 或 ? 或 !)。
我们也可以轻松地指定一个字符范围,像 [0-9] 代表的含意与 \d 就是完全一致的:一位数字;同理 [a-z0-9A-Z_] 也完全等同于 \w(如果只考虑英文的话)。

下面是一个更复杂的表达式:

\(?0\d{2}[)-]?\d{8}  

英文括号 ( 和 ) 也是元字符,后面的分组节里会提到,所以在这里需要使用转义。

这个表达式可以匹配几种格式的电话号码,像 (010)88886666,或 022-22334455,或 02912345678 等。我们对它进行一些分析吧:
首先是一个转义字符 (,它能出现 0 次或 1 次(?),然后是一个 0,后面跟着 2 个数字 (\d{2}),然后是)或 - 或 空格 中的一个,它出现 1 次或不出现(?),最后是 8 个数(\d{8})。

分支条件

正则1|正则2:从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。

分组

我们已经提到了怎么重复单个字符(直接在字符后面加上限定符就行了);但如果想要重复多个字符又该怎么办?你可以用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了,你也可以对子表达式进行其它一些操作(后面会有介绍)。

(\d{1,3}.){3}\d{1,3} 是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:\d{1,3} 匹配 1 到 3 位的数字,(\d{1,3}.){3} 匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复 3 次,最后再加上一个一到三位的数字 (\d{1,3})。

不幸的是,它也将匹配256.300.888.999这种不可能存在的IP地址。如果能使用算术比较的话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:

((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)  

理解这个表达式的关键是理解 2[0-4]\d|25[0-5]|[01]?\d\d?,具体含义参看后面的注释章节

反义

有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义:

代码/语法 说明
\W 匹配任意不是字母,数字,下划线,汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结束的位置
[^x] 匹配除了 x 以外的任意字符
[^aeiou] 匹配除了 aeiou 这几个字母以外的任意字符

例子:
\S+ 匹配不包含空白符的字符串。
\<a[^>]+> 匹配用尖括号括起来的以 a 开头的字符串。

后向引用

使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。

呃……其实,组号分配还不像我刚说得那么简单:
分组0对应整个正则表达式
实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组号
你可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权.

后向引用用于重复搜索前面某个分组匹配的文本。例如,\1 代表分组 1 匹配的文本。难以理解?请看示例:

\b(\w+)\b\s+\1\b 可以用来匹配重复的单词,像 go go, 或者 kitty kitty。这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字 (\b(\w+)\b),这个单词会被捕获到编号为 1 的分组中,然后是 1 个或几个空白符 (\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词)(\1)。

你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?\\w+) (或者把尖括号换成 ‘ 也行:(?’Word’\w+)),这样就把 \w+ 的组名指定为 Word了。要反向引用这个分组捕获的内容,你可以使用 \k\,所以上一个例子也可以写成这样:\b(?\\w+)\b\s+\k\\b。

使用小括号的时候,还有很多特定用途的语法。下面列出了最常用的一些:

常用分组语法

分类 代码/语法 说明
捕获 (exp) 匹配exp,并捕获文本到自动命名的组里
(?< name>exp) 匹配exp,并捕获文本到名称为name的组里,也可以写成(?’name’exp)
(?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号
零宽断言 (?=exp) 匹配exp前面的位置
(?<=exp) 匹配exp后面的位置
(?!exp) 匹配后面跟的不是exp的位置
(?<!exp) 匹配前面不是exp的位置
注释 (?#comment) 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读

我们已经讨论了前两种语法。第三个 (?:exp) 不会改变正则表达式的处理方式,只是这样的组匹配的内容不会像前两种那样被捕获到某个组里面,也不会拥有组号。“我为什么会想要这样做?”——好问题,你觉得为什么呢?

零宽断言

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像 \b , ^ , $ 那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧:

断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配。

(?=exp) 也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式 exp。比如 \b\w+(?=ing\b),匹配以 ing 结尾的单词的前面部分(除了ing以外的部分),如查找 I’m singing while you’re dancing. 时,它会匹配 sing 和 danc。

(?<=exp) 也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如 (?<=\bre)\w+\b 会匹配以 re 开头的单词的后半部分(除了re以外的部分),例如在查找 reading a book 时,它匹配 ading。

假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了),你可以这样查找需要在前面和里面添加逗号的部分:((?<=\d)\d{3})+\b,用它对 1234567890 进行查找时结果是 234567890。

下面这个例子同时使用了这两种断言:

(?<=\s)\d+(?=\s)

匹配以空白符间隔的数字 (再次强调,不包括这些空白符)。

负向零宽断言

前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果我们想查找这样的单词–它里面出现了字母 q,但是 q 后面跟的不是字母 u,我们可以尝试这样:

\b\w*q[^u]\w*\b 匹配包含后面不是字母 u 的字母 q 的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词的结尾的话,像 Iraq,Benq,这个表达式就会出错。这是因为 [^u] 总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的 [^u] 将会匹配 q 后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的 \w*\b 将会匹配下一个单词,于是 \b\w*q[^u]\w*\b 就能匹配整个 Iraq fighting。负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题:\b\w*q(?!u)\w*\b。

零宽度负预测先行断言 (?!exp),断言此位置的后面不能匹配表达式exp。例如:\d{3}(?!\d) 匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。

同理,我们可以用 (?<!exp),零宽度负回顾后发断言来断言此位置的前面不能匹配表达式 exp:(?<![a-z])\d{7} 匹配前面不是小写字母的七位数字。

请详细分析表达式 (?<=<(\w+)>).*(?=<\/\1>),这个表达式最能表现零宽断言的真正用途。

一个更复杂的例子:
(?<=<(\w+)>).*(?=<\/\1>) 匹配不包含属性的简单 HTML 标签内里的内容。(?<=<(\w+)>) 指定了这样的前缀:被尖括号括起来的单词(比如可能是 < xxx> ),然后是 ".*" (任意的字符串),最后是一个后缀 (?=<\/\1>)。注意后缀里的 \/,它用到了前面提过的字符转义;\1 则是一个反向引用,引用的正是捕获的第一组,前面的 (\w+) 匹配的内容,这样如果前缀实际上是 < xxx> 的话,后缀就是 < /xxx> 了。整个表达式匹配的是 < xxx> 和 < /xxx> 之间的内容(再次提醒,不包括前缀和后缀本身)。

注释

小括号的另一种用途是通过语法 (?#comment) 来包含注释。例如:
2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)

要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样:

(?<=    # 断言要匹配的文本的前缀
<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
)       # 前缀结束
.*      # 匹配任意文本
(?=     # 断言要匹配的文本的后缀
<\/\1>  # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
)       # 后缀结束

贪婪与懒惰匹配

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b,它将会匹配最长的以 a 开始,以 b 结束的字符串。如果用它来搜索 aabab 的话,它会匹配整个字符串 aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号 ? 。这样 .*? 就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b 匹配最短的,以 a 开始,以 b 结束的字符串。如果把它应用于 aabab的话,它会匹配 aab(第一到第三个字符)和 ab(第四到第五个字符)。

为什么第一个匹配是 aab(第一到第三个字符)而不是 ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。

懒惰限定符

代码/语法 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

处理选项

正则支持设置匹配的选项,比如忽略大小写,忽略空白,单行模式,多行模式等

其他语法

代码/语法 说明
\a 报警字符(打印它的效果是电脑嘀一声)
\b 通常是单词分界位置,但如果在字符类里使用代表退格
\t 制表符,Tab
\r 回车
\v 竖向制表符
\f 换页符
\n 换行符
\e Escape
\0nn ASCII代码中八进制代码为nn的字符
\xnn ASCII代码中十六进制代码为nn的字符
\unnnn Unicode代码中十六进制代码为nnnn的字符
\cN ASCII控制字符。比如\cC代表Ctrl+C
\A 字符串开头(类似^,但不受处理多行选项的影响)
\Z 字符串结尾或行尾(不受处理多行选项的影响)
\z 字符串结尾(类似$,但不受处理多行选项的影响)
\G 当前搜索的开头
\p{name} Unicode中命名为name的字符类,例如\p{IsGreek}
(?>exp) 贪婪子表达式
(?< x>-< y>exp) 平衡组
(?im-nsx:exp) 在子表达式exp中改变处理选项
(?im-nsx) 为表达式后面的部分改变处理选项
(?(exp)yes/no) 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式;否则使用no
(?(exp)yes) 同上,只是使用空表达式作为no
(?(name)yes/no) 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用no
(?(name)yes) 同上,只是使用空表达式作为no

在线测试

以上这么多知识,边学边练是很有必要的。这里推荐两个有趣的在线网站:
http://rubular.com/
https://www.debuggex.com/

参考

http://blog.jobbole.com/96708/

通过源码将git升级到最新版

因为go-get下载hugo失败,需要升级git到最新版本。

下载最新git代码

wget -O git-master.zip https://github.com/git/git/archive/master.zip

编译

unzip git-master.zip
cd git-master

yum install openssl-devel curl-devel expat-devel perl-ExtUtils-MakeMaker gettext gettext-libs gettext-devel asciidoc xmlto docbook2X

ln -s /usr/bin/db2x_docbook2texi /usr/bin/docbook2x-texi

错误

libgit.a(utf8.o): In function `reencode_string_iconv':
/usr/local/git/utf8.c:463: undefined reference to `libiconv'
libgit.a(utf8.o): In function `reencode_string_len':
/usr/local/git/utf8.c:502: undefined reference to `libiconv_open'
/usr/local/git/utf8.c:521: undefined reference to `libiconv_close'
/usr/local/git/utf8.c:515: undefined reference to `libiconv_open'
collect2: ld returned 1 exit status
make: *** [git-credential-store] Error 1

从错误提示看,是缺少libiconv。

安装libiconv

  1. 下载编译

    cd /usr/local
    wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.14.tar.gz
    tar -zxvf libiconv-1.14.tar.gz
    cd libiconv-1.14
    ./configure –prefix=/usr/local/libiconv && make && make install

  2. 创建一个软链接到/usr/lib

    ln -s /usr/local/lib/libiconv.so /usr/lib
    ln -s /usr/local/lib/libiconv.so.2 /usr/lib

  3. 然后回到git目录继续编译

    cd /usr/local/git
    make configure
    ./configure --prefix=/usr/local --with-iconv=/usr/local/libiconv
    make
    make install

  4. 此时,git应该编译安装成功了,测试如下:

    $ git --version
    git version 2.5.0

centos6下go get卡停问题

go get可以通过选项,直接下载所有依赖项,非常方便。所以在安装hugo时,我们也通过go get 下载源码:

go get -u -v github.com/spf13/hugo 

下载时突然卡住,没有任何反应和提示,日志停留在:

Fetching https://gopkg.in/fsnotify.v1?go-get=1
Parsing meta tags from https://gopkg.in/fsnotify.v1?go-get=1 (status code 200)
get "gopkg.in/fsnotify.v1": found meta tag main.metaImport{Prefix:"gopkg.in/fsnotify.v1", VCS:"git", RepoRoot:"https://gopkg.in/fsnotify.v1"} at https://gopkg.in/fsnotify.v1?go-get=1
gopkg.in/fsnotify.v1 (download) 

根据缺失的库逐一排查,后来根据缺失 gopkg.in/yaml.v1 为线索,网上说是原因git版本太低,需>= 1.7.9.5,而Centos6.7 自带的git 是1.7.1的。

升级到git最新版后,go get 成功!

一步一步教你用hugo搭建博客

Hugo 是一个轻量级的静态网站生成工具,是基于GO语言的模版技术开发而成,因为最近在学习go,就花了时间研究了下,一研究就喜欢上了。
再加上最新wordpress版本有严重的问题,在文章发表后或者再次编辑时,编辑框会丢失所有的格式,这个让使用Markdown的人无法接受。

安装hugo

Hugo官方主页:HUGO
hugo托管在github上,我们可以直接二进制安装也可以源码安装。
这里我们演示源码安装。

  1. 源码安装
    在go里面我们可以直接通过get安装:
    go get -u -v github.com/spf13/hugo
    或者直接git下载
    git clone https://github.com/spf13/hugo.git

  2. 编译
    go build -o hugo main.go
    mv hugo $GOPATH/bin
    终端查看是否成功,mac下可能出现路径没找到的问题,要重新开终端
    $ hugo version
    Hugo Static Site Generator v0.16-DEV BuildDate: 2015-12-14T16:07:24+08:00

生成静态站点

  1. 创建网站
    我们先创建一个空网站.
    $ hugo new site localhost
    $ tree
    .
    ├── archetypes
    ├── config.toml
    ├── content
    ├── data
    ├── layouts
    ├── public
    ├── static
    └── themes  

默认情况下这些目录都是空的,直接运行的话会有ERROR提示

    ERROR: 2015/12/14   =============================================================
    ERROR: 2015/12/14 Your rendered home page is blank: /index.html is zero-length
    ERROR: 2015/12/14  * Did you specify a theme on the command-line or in your
    ERROR: 2015/12/14    "config.toml" file?  (Current theme: "")
    ERROR: 2015/12/14  * For more debugging information, run "hugo -v"
    ERROR: 2015/12/14 =============================================================

看提示说是没有指定theme导致,我们需要下载一个theme。

  1. 安装theme
    我们可以从hugo的网站下载自己喜欢的theme
    $ cd themes
    $ git clone https://github.com/spf13/hyde.git

  2. 测试框架
    安装完theme后,我们体验下效果,使用 hugo server就可以起一个http server,默认监听在1313端口,如果没有在config中配置theme,就要指定theme。

    $ hugo server -t hyde
    0 draft content
    0 future content
    0 pages created
    0 paginator pages created
    0 tags created
    0 topics created
    in 24 ms
    Watching for changes in /Users/alex/run/localhost/{data,content,layouts,static,themes}
    Serving pages from memory
    Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
    Press Ctrl+C to stop            

hyde 界面

  1. 发表文章
    hugo里面写文章其实就是写markdown文档了。写好文档,hugo会给你自动转成html静态文件。我们通过Hugo创建一个md文档。
    $ hugo new first.md
    /Users/alex/run/localhost/content/first.md created
    运行时在网站根目录下运行,创建的文件默认创建在content目录下。
    +++
    date = "2015-12-15T22:35:22+08:00"
    draft = true
    title = "first"
    
    +++

我们从内容看默认创建的是草稿类型,需要将draft改为true才能看到页面。正常情况下我们会通过Mou或者github编辑文档,只要文件头符合hugo的规范就可以。
第一篇文章

调试部署

  1. 调试
    在开发的过程中,我们需要不断的修改验证,所以hugo支持LiveReload功能,用户修改后,可以实时看到效果。执行hugo server命令时加上-w选项,hugo就可以自动检测本地站点文件的变更。
    $ hugo server -w -t hyde
    注意:在使用server命令时,hugo并没有在public目录下产生相应的静态页面。

  2. 部署
    部署时,我们需要生成静态页面文件,然后就可以随便部署在自己的空间上了。转化时,一个hugo命令就搞定:

    $ hugo -t hyde
    0 draft content
    0 future content
    1 pages created
    0 paginator pages created
    0 topics created
    0 tags created
    in 38 ms

我们看到有一个页面生成了,默认在public目录,实际一起生成的还有其他文件:

    $ ls
    404.html                first
    apple-touch-icon-144-precomposed.png    index.html
    css                 index.xml
    favicon.png             sitemap.xml

把这些文件放到你的空间,你就可以看见你的页面和theme了。

到这里,我们已经有了一个基本的能创建文章并且显示的网站了。

have fun!

记一次移动端frame注入事件

起因

在用手机调试网站效果时,偶尔发现底部出现广告,于是就有了以下的内容。

界面表现


停留一会后自动消失

代码表现

通过提取,我们可以拿到广告出现时和消失后的代码。
出现广告时页面被插入的frame代码
<iframe src="http://i.dreamfull.cn/api/my.jsp?sid=320418129&amp;pn=_QWERJAD_274192119_320418129_1_&amp;sd=api.dreamfull.cn#_maerd_dnegel_=1" style="display: none; border: 0px; width: 0px; height: 0px;"></iframe><div style="display: block; visibility: visible; overflow: hidden; width: 375px; height: 56.25px; margin: 0px; padding: 0px; border: 0px; box-sizing: border-box; z-index: 2147483647; position: fixed; bottom: 0px; left: 0px;"><div style="position: relative; z-index: 0;"><a style="width: 100%; height: 59px; display: none; text-decoration: none; -webkit-box-align: center; color: rgb(0, 0, 0); overflow: hidden; line-height: 59px; font-size: 10px;"></a><script src="http://c3.moogos.com/js/_jssdk.js?aid=s4b7b3f9" type="text/javascript" async="async"></script><div style="left: 0px; font-size: 0px; z-index: 2147483583; position: fixed; bottom: 0px; display: block; width: 100%; height: 59px;" csstext="display:block;left:0;font-size:0;z-index:2147483583;position:fixed;bottom:0;display:block;width:100%;height:59px;"><iframe style="border: 1px; bottom: 0px; display: block; width: 375px; height: 56.25px;" frameborder="0" scrolling="no" border="0" src="http://api.moogos.com/js/index.html?_ts=1450470254933&amp;info=%7B%22domain%22:%22www.goodmemory.cc%22,%22urls%22:%22www.goodmemory.cc/%25E9%2580%259A%25E8%25BF%2587event-hook%25E5%25B0%2586github%25E8%2587%25AA%25E5%258A%25A8%25E9%2583%25A8%25E7%25BD%25B2%25E8%2587%25B3hugo%25E7%25BD%2591%25E7%25AB%2599/%22,%22adslot%22:%22s4b7b3f9%22,%22version%22:%7B%22major%22:2,%22minor%22:26%7D,%22prod%22:1,%22adParentId%22:%22moogos_s4b7b3f9%22,%22inittime%22:1450470254923,%22duration%22:10,%22appId%22:%2202eddf5b%22%7D"></iframe><div style="position:absolute; width:30px; height:30px;top:0;right:0;z-index:2147483584;background:rgba(255,255,255,0);" onclick="var p = this.parentNode;p.parentNode.removeChild(p);p.setAttribute(&quot;close&quot;, &quot;0&quot;);var b = document.getElementById(&quot;blankDivs4b7b3f9&quot;);b &amp;&amp; b.parentNode.removeChild(b);"><i style="position:absolute; width:1px; height:18px; background:#000; top:6px;left:6px; -ms-transform:rotate(45deg); -webkit-transform:rotate(45deg); -moz-transform:rotate(45deg); transform:rotate(45deg); left:15px;"></i><i style="position:absolute; width:1px; height:18px; background:#000; top:6px;left:6px; -ms-transform:rotate(-45deg); -webkit-transform:rotate(-45deg); -moz-transform:rotate(-45deg); transform:rotate(-45deg); left:15px;"></i><i style="position:absolute; width:24px; height:24px; top:3px; left:3px; background:rgba(255,255,255,0.4); border-radius:50%;"></i></div></div><div style="position: absolute; top: -20px; right: -10px; z-index: 99; width: 50px; height: 60px; overflow: hidden; display: block; left: auto !important; background: url(&quot;http://api.dreamfull.cn/s/images/none.png&quot;) repeat transparent;"></div></div><div style="position: absolute; cursor: pointer; bottom: 0px; right: 0px; z-index: 100; width: 35px; height: 20px; overflow: hidden; display: block; left: auto !important; background: url(&quot;http://api.dreamfull.cn/s/images/logo_mini.gif&quot;) repeat transparent;"></div><div style="position: absolute; cursor: pointer; top: 0px; right: 0px; z-index: 100; width: 35px; height: 20px; overflow: hidden; display: block; left: auto !important; background: url(&quot;http://api.dreamfull.cn/s/images/none.png&quot;) repeat transparent;"></div><div style="position: absolute; cursor: pointer; top: 0px; right: 0px; z-index: 100; display: none; width: 86px; height: 20px; overflow: hidden; left: auto !important; background: url(&quot;http://api.dreamfull.cn/s/images/close_long.gif&quot;) repeat transparent;"></div></div>
广告自动消失后的代码变成:
<img src="http://rcv.moogos.com/rtsdk?type=show&amp;version=2.26&amp;urls=www.goodmemory.cc/%E9%80%9A%E8%BF%87event-hook%E5%B0%86github%E8%87%AA%E5%8A%A8%E9%83%A8%E7%BD%B2%E8%87%B3hugo%E7%BD%91%E7%AB%99/&amp;adslot=s4b7b3f9&amp;_ts=1450467804078" style="display:none;">

分析

通过以上的信息,我们得到如下信息:
* http://dreamfull.cn 是广告平台方,根据域名信息查询:


* 广告投放方为http://moogos.com
* 广告代码样本
https://hiproz.github.io/goodmemory.cc/blog/images/2015/12/frame1.txt
https://hiproz.github.io/goodmemory.cc/blog/images/2015/12/frame2.txt
* 其中的js脚本:http://c3.moogos.com/js/_jssdk.js?aid=s4b7b3f9,搜索这个脚本中的".com"和".png"能看到更多细节。
为了避免被清理,做了备份,方便后面举证:
https://hiproz.github.io/goodmemory.cc/blog/images/2015/12/jssdk.js-bak
* 搜索了以下,有很多dreamfull.cn的案例,多和联通有关:

安全

因为注入的原理不是在服务器修改源代码的,可能是在运营商的路由环节,或者我们使用的所谓智能路由器,或者第三方动态加载时被劫持,所以很难从根源上消除,目前能想到的就是先把https做了,还有就是加载安全js插件,用户加载时动态触发检测,不过这个只是计划,目前本人能时间上和能力还做不到。

最后

以上做了这么多细致的工作,是为了拿到更多的证据,方便更多的人去投诉和举报,创造健康的网络环境。

通过event-hook将Github自动部署至Hugo网站

WHAT

hugo是一个轻量高效的博客系统,很适合个人博客。使用hugo,我们只要写作完markdown文档,就可以利用hugo工具,自动生成网页,
变成我们的网站。

我们理想的步骤:

  • 在github上写完markdown文章
  • 提交完后,数秒后就看见了我们的网站页面
  • 在github上修改完网站的配置文件,数秒后我们的网站就变化和更新了。

好爽!

流程

为了完成上面的效果,我们大概分为几步:

  1. 设置github的webservice hook。当完成一篇新的文章或者修改旧的文章后,github就会向目标网站发webservice hook消息。
  2. 目标网站收到消息后git pull,解析消息特征,更新相关的文档,最后调用hugo
  3. hugo将markdown文章转化成html静态页面
  4. 将html页面部署到目标web服务器

HOW

详细请移步:
https://github.com/hiproz/hugo-sync

分分钟就搞定了,一劳永逸!

have fun!

hugo-md文档书写时的格式说明

前言

在决定使用github-hugo自动化流程后,第一个面临的问题就是,这个文档头的格式到底是什么样的,因为现在你要手写,就需要彻底搞清楚, 以便达到正确和高效。

阅读查找了gohugo.io的所有相关页面,找到以下两个相关的内容基本上说的很清楚了:
https://gohugo.io/content/front-matter/
https://gohugo.io/content/archetypes/

头格式

md只是文档的类型,但是里面的内容遵循怎样的格式呢? hugo目前支持3中toml,yaml,json,三种格式的文件头。识别符号分别如下

  • toml: +++
  • yaml: ---
  • json: {}

toml示例:

+++
title = "spf13-vim 3.0 release and new website"
description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
tags = [ ".vimrc", "plugins", "spf13-vim", "vim" ]
date = "2012-04-06"
categories = [
  "Development",
  "VIM"
]
slug = "spf13-vim-3-0-release-and-new-website"
+++

Content of the file goes Here

yaml示例:

---
title: "spf13-vim 3.0 release and new website"
description: "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
tags: [ ".vimrc", "plugins", "spf13-vim", "vim" ]
date: "2012-04-06"
categories:
  - "Development"
  - "VIM"
slug: "spf13-vim-3-0-release-and-new-website"
---

Content of the file goes Here

json示例:

{
    "title": "spf13-vim 3.0 release and new website",
    "description": "spf13-vim is a cross platform distribution of vim plugins and resources for Vim.",
    "tags": [ ".vimrc", "plugins", "spf13-vim", "vim" ],
    "date": "2012-04-06",
    "categories": [
        "Development",
        "VIM"
    ],
    "slug": "spf13-vim-3-0-release-and-new-website",
}

Content of the file goes Here

字段变量

文档可以包含很多变量。

必要字段

  • tile: 内容的标题
  • description: 内容的描述
  • date:日期
  • taxonomies:分类字段,包括tag和categories

可选字段

  • aliases: 别名
  • draft: 是否是草稿
  • publishdate: 定时未来发布的时间
  • type: 内容的格式,可以从内容自动识别
  • isCJKLanguage:是否时CJK
  • weight: 排序的权重
  • markup: 时markdown格式还是reStructuredText
  • slug: url尾部的token
  • url: 完整的url

hugo markdown引擎的配置

finish!

mac下安装php56

今天调试php的代码,提示需要PHP56,于是看了下php在github上的指导文档

Installation

Setup the homebrew/dupes tap which has dependencies we need:

$ brew tap homebrew/dupes

Setup the homebrew/versions tap which has dependencies we need:

$ brew tap homebrew/versions

Then, run the following in your command-line:

$ brew tap homebrew/homebrew-php

Usage

Note: For a list of available configuration options run:

$ brew options php56

Once the tap is installed, you can install php53, php54, php55, php56, php70, or any formulae you might need via:

$ brew install php56

按照上面的指导操作,出现如下的告警,导致tap失败 ,后继无法install成功:

$ brew tap homebrew/homebrew-php

Warning: Tap homebrew/php already tapped.

解决:先untap,然后重新tap:

$ brew untap homebrew/php

done!

go get引起的terminal prompts disabled错误

执行go get命令时,出现如下的错误提示:

fatal: could not read Username for 'https://github.com': terminal prompts disabled

两种解决方案:

  1. 先通过ssh成功登陆一次git,正确获取到key缓存。
  2. 手动添加key:https://help.github.com/articles/generating-ssh-keys/

然后就可以正常get了。

文章参考:
http://stackoverflow.com/questions/32232655/go-get-results-in-terminal-prompts-disabled-error-for-github-private-repo

centos7的mariadb相关配置

centos下面的repo源默认是支持mariadb的,因为是和mysql兼容的,所以如果特定需求,可以直接使用mariadb。

安装

yum -y install MariaDB-server MariaDB-client
or
yum -y install mysql

启动/停止/重启/重新加载配置

systemctl start|stop|restart|reload mariadb.service

修改密码

mysqladmin -u root password 'root'

自动启动

systemctl enable mariadb.service

初次设置(密码/权限/库)

mysql_secure_installation

Primary script unknown

nginx运行时的error log:

FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream 原因在于nginx 默认的脚本配置中

location ~ \.php$ { 
    root html; fastcgi_pass 127.0.0.1:9000; 
    fastcgi_index index.php; 
    fastcgi_param SCRIPT_FILENAME /script$fastcgi_script_name; 
    include fastcgi_params; 
} 

SCRIPT_FILENAME 需要更新成实际的目录,修改如下:

$document_root$fastcgi_script_name;

c++中的extern “c” {}

讲的很清晰,透彻,特此存档 原文:[C++项目中的extern "C" {}](http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html)
以下正文:

引言

在用C++的项目源码中,经常会不可避免的会看到下面的代码:

#ifdef __cplusplus
extern "C" {
#endif

/*...*/

#ifdef __cplusplus
}
#endif

它到底有什么用呢,你知道吗?而且这样的问题经常会出现在面试or笔试中。下面我就从以下几个方面来介绍它:

  • 1、#ifdef _cplusplus/#endif _cplusplus及发散
  • 2、extern "C"
    • 2.1、extern关键字
    • 2.2、"C"
    • 2.3、小结extern "C"
  • 3、C和C++互相调用
    • 3.1、C++的编译和连接
    • 3.2、C的编译和连接
    • 3.3、C++中调用C的代码
    • 3.4、C中调用C++的代码
  • 4、C和C++混合调用特别之处函数指针

1、#ifdef _cplusplus/#endif _cplusplus及发散

在介绍extern "C"之前,我们来看下#ifdef _cplusplus/#endif _cplusplus的作用。很明显#ifdef/#endif、#ifndef/#endif用于条件编译,#ifdef _cplusplus/#endif _cplusplus——表示如果定义了宏_cplusplus,就执行#ifdef/#endif之间的语句,否则就不执行。

在这里为什么需要#ifdef _cplusplus/#endif _cplusplus呢?因为C语言中不支持extern "C"声明,如果你明白extern "C"的作用就知道在C中也没有必要这样做,这就是条件编译的作用!在.c文件中包含了extern "C"时会出现编译时错误。

既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。还记得腾讯笔试就考过这个题目,给出类似下面的代码(下面是我最近在研究的一个开源web服务器——Mongoose的头文件mongoose.h中的一段代码):

#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

/*.................................
 * do something here
 *.................................
 */

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* MONGOOSE_HEADER_INCLUDED */

然后叫你说明上面宏#ifndef/#endif的作用?为了解释一个问题,我们先来看两个事实:

  • 这个头文件mongoose.h可能在项目中被多个源文件包含(#include "mongoose.h"),而对于一个大型项目来说,这些冗余可能导致错误,因为一个头文件包含类定义或inline函数,在一个源文件中mongoose.h可能会被#include两次(如,a.h头文件包含了mongoose.h,而在b.c文件中#include a.h和mongoose.h)——这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。
  • 从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际,最重要的是有时候又需要这种冗余来保证各个模块的独立

为了解决这个问题,上面代码中的

#ifndef MONGOOSE_HEADER_INCLUDED
#define    MONGOOSE_HEADER_INCLUDED
/*……………………………*/
#endif /* MONGOOSE_HEADER_INCLUDED */

就起作用了。如果定义了MONGOOSE_HEADER_INCLUDED,#ifndef/#endif之间的内容就被忽略掉。因此,编译时第一次看到mongoose.h头文件,它的内容会被读取且给定MONGOOSE_HEADER_INCLUDED一个值。之后再次看到mongoose.h头文件时,MONGOOSE_HEADER_INCLUDED就已经定义了,mongoose.h的内容就不会再次被读取了。

2、extern "C"

首先从字面上分析extern "C",它由两部分组成——extern关键字、"C"。下面我就从这两个方面来解读extern "C"的含义。

2.1、extern关键字

在一个项目中必须保证函数、变量、枚举等在所有的源文件中保持一致,除非你指定定义为局部的。首先来一个例子:

//file1.c:
    int x=1;
    int f(){do something here}
//file2.c:
    extern int x;
    int f();
    void g(){x=f();}

在file2.c中g()使用的x和f()是定义在file1.c中的。extern关键字表明file2.c中x,仅仅是一个变量的声明,其并不是在定义变量x,并未为x分配内存空间。变量x在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。但是可以声明多次,且声明必须保证类型一致,如:

//file1.c:
    int x=1;
    int b=1;
    extern c;
//file2.c:
    int x;// x equals to default of int type 0
    int f();
    extern double b;
    extern int c;

在这段代码中存在着这样的三个错误:

  1. x被定义了两次
  2. b两次被声明为不同的类型
  3. c被声明了两次,但却没有定义

回到extern关键字,extern是C/C++语言中表明函数全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

与extern对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

2.2、"C"

典型的,一个C++程序包含其它语言编写的部分代码。类似的,C++编写的代码片段可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。

为了使它们遵守统一规则,可以使用extern指定一个编译和连接规约。例如,声明C和C++标准库函数strcyp(),并指定它应该根据C的编译和连接规约来链接:

extern "C" char* strcpy(char*,const char*);

注意它与下面的声明的不同之处:

extern char* strcpy(char*,const char*);

下面的这个声明仅表示在连接的时候调用strcpy()。

extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortran、assembler等。

还有要说明的是,extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则。

再看下面的一个例子,为了声明一个变量而不是定义一个变量,你必须在声明时指定extern关键字,但是当你又加上了"C",它不会改变语义,但是会改变它的编译和连接方式。

如果你有很多语言要加上extern "C",你可以将它们放到extern "C"{ }中。

2.3、小结extern "C"

通过上面两节的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言)

3、C和C++互相调用

我们既然知道extern "C"是实现的类C和C++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。首先要明白C和C++互相调用,你得知道它们之间的编译和连接差异,及如何利用extern "C"来实现相互调用。

3.1、C++的编译和连接

C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:

void print(int i);
void print(char c);
void print(float f);
void print(char* s);

编译为:

_print_int
_print_char
_print_float
_pirnt_string

这样的函数名,来唯一标识每个函数。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用print(3)时,它会去查找_print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。

C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。

3.2、C的编译和连接

C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了。

3.3、C++中调用C的代码

假设一个C的头文件cHeader.h中包含一个函数print(int i),为了在C++中能够调用它,必须要加上extern关键字(原因在extern关键字那节已经介绍)。它的代码如下:

#ifndef C_HEADER
#define C_HEADER

extern void print(int i);

#endif C_HEADER

相对应的实现文件为cHeader.c的代码为:

#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}

现在C++的代码文件C++.cpp中引用C中的print(int i)函数:

extern "C"{
#include "cHeader.h"
}

int main(int argc,char** argv)
{
    print(3);
    return 0;
}

执行程序输出:

image 

3.4、C中调用C++的代码

现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。如下在cppHeader.h头文件中定义了下面的代码:

#ifndef CPP_HEADER
#define CPP_HEADER

extern "C" void print(int i);

#endif CPP_HEADER

相应的实现文件cppHeader.cpp文件中代码如下:

#include "cppHeader.h"

#include <iostream>
using namespace std;
void print(int i)
{
    cout<<"cppHeader "<<i<<endl;
}

在C的代码文件c.c中调用print函数:

extern void print(int i);
int main(int argc,char** argv)
{
    print(3);
    return 0;
}

注意在C的代码文件中直接#include "cppHeader.h"头文件,编译出错。而且如果不加extern int print(int i)编译也会出错。

4、C和C++混合调用特别之处函数指针

当我们C和C++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一中语言定义的函数。如果C和C++共享同一中编译和连接、函数调用机制,这样做是可以的。然而,这样的通用机制,通常不然假定它存在,因此我们必须小心地确保函数以期望的方式调用。

而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:

typedef int (*FT) (const void* ,const void*);//style of C++

extern "C"{
    typedef int (*CFT) (const void*,const void*);//style of C
    void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
}

void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C

//style of C
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);

int compare(const void*,const void*);//style of C++
extern "C" ccomp(const void*,const void*);//style of C

void f(char* v,int sz)
{
    //error,as qsort is style of C
    //but compare is style of C++
    qsort(v,sz,1,&compare);
    qsort(v,sz,1,&ccomp);//ok
    
    isort(v,sz,1,&compare);//ok
    //error,as isort is style of C++
    //but ccomp is style of C
    isort(v,sz,1,&ccopm);
}

注意:typedef int (*FT) (const void* ,const void*),表示定义了一个函数指针的别名FT,这种函数指针指向的函数有这样的特征:返回值为int型、有两个参数,参数类型可以为任意类型的指针(因为为void*)。

最典型的函数指针的别名的例子是,信号处理函数signal,它的定义如下:

typedef void (*HANDLER)(int);
HANDLER signal(int ,HANDLER);

上面的代码定义了信函处理函数signal,它的返回值类型为HANDLER,有两个参数分别为int、HANDLER。 这样避免了要这样定义signal函数:

void (*signal (int ,void(*)(int) ))(int)

比较之后可以明显的体会到typedef的好处。

Go中struct{}{}的解读

新人刚开始看到go中的struct{}{}作为interface的返回值时,一定很费解,怎么理解呢?看下面的例子
关于空接口,我们有如下的用法:

var v1 interface{} = 1      // 将int类型赋值给interface{}
var v2 interface{} = "abc"    // 将string类型赋值给interface{}
var v3 interface{} = &v2    // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}

看到4,5行的用法了嘛,其实 struct{}{}只是一个空的特例而已。也就是一个空的struct实例对象而已。

Go编程tip-4

从Panic中恢复

recover()的调用仅当它在defer函数中被直接调用时才有效。在defer中和直接调用这两个是必要条件

在Slice, Array, and Map "range"语句中更新引用元素的值

在“range”语句中生成的数据的值是真实集合元素的拷贝。它们不是原有元素的引用。这意味着更新这些值将不会修改原来的数据。同时也意味着使用这些值的地址将不会得到原有数据的指针。

如果你需要更新原有集合中的数据,使用索引操作符来获得数据。
package main

import "fmt"

func main() {
    data := []int{1, 2, 3}
    for i, _ := range data {
        data[i] *= 10
    }

    fmt.Println("data:", data) //prints data: [10 20 30]
}

如果你的集合保存的是指针,那规则会稍有不同。如果要更新原有记录指向的数据,你依然需要使用索引操作,但你可以使用for range语句中的第二个值来更新存储在目标位置的数据。
package main

import "fmt"

func main() {
    data := []*struct{ num int }{{1}, {2}, {3}}

    for _, v := range data {
        v.num *= 10
    }

    fmt.Println(data[0], data[1], data[2]) //prints &{10} &{20} &{30}
}

slice中的隐藏数据

因为slice是一个指针引用,所以如果引用原有数据的一小部分,实际指向的还是原来的整个内存对象。

对slice截断对象的append操作

因为slice很像指针,整个操作很容易产生“越界覆盖”

类型声明和方法

当你通过把一个现有(非interface)的类型定义为一个新的类型时,新的类型不会继承现有类型的方法。
Fails:
package main

import "sync"

type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock()   //error
    mtx.Unlock() //error
}

如果你确实需要原有类型的方法,你可以定义一个新的struct类型,用匿名方式把原有类型嵌入其中。
package main

import "sync"

type myLocker struct {
    sync.Mutex
}

func main() {
    var lock myLocker
    lock.Lock()   //ok
    lock.Unlock() //ok
}

defer的函数什么时候执行?

被defer的调用会在包含的函数的末尾执行,而不是包含代码块的末尾。对于Go新手而言,一个很常犯的错误就是无法区分被defer的代码执行规则和变量作用规则。如果你有一个长时运行的函数,而函数内有一个for循环试图在每次迭代时都defer资源清理调用,那就会出现问题。
解决办法就是将相关的逻辑封装成内部匿名函数,这样在函数结束时,就会调用defer指定的函数

更新map的值

如果你有一个struct值的map,你无法更新单个的struct值。这个操作无效是因为map元素是无法取址的。
但是slice元素是可以取址的。

栈和堆变量

你并不总是知道变量是分配到栈还是堆上。在C++中,使用new创建的变量总是在堆上。在Go中,即使是使用new()或者make()函数来分配,变量的位置还是由编译器决定。编译器根据变量的大小和“泄露分析”的结果来决定其位置。这也意味着在局部变量上返回引用是没问题的,而这在C或者C++这样的语言中是不行的。

如果你想知道变量分配的位置,在“go build”或“go run”上传入“-m“ gc标志(即,go run -gcflags -m app.go)。

多goroutine的读写顺序可能被重排

多个goroutine运行时,顺序的语句执行次序可能会发生调换。

Go lang Slice截取时指定最大容量以及append函数的使用说明

slice是一个三元组对象
* 一个内容指针
* 有效内容长度
* 最大长度,也就是容量。

示例:

package main

import (
    "bytes"
    "fmt"
)

func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path, '/')
    fmt.Println(sepIndex)
    dir1 := path[:sepIndex:sepIndex] //full slice expression
    fmt.Println(&path[0], &dir1[0])
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>", string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>", string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1, "suffix"...)
    fmt.Println(&dir1[0])
    path = bytes.Join([][]byte{dir1, dir2}, []byte{'/'})

    fmt.Println("dir1 =>", string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>", string(dir2)) //prints: dir2 => BBBBBBBBB (ok now)

    fmt.Println("new path =>", string(path))
}
  • 第12行的代码就是一个指定容量的截取。
  • 如果增加后不回超出slice的最大空间,slice是不会重新分配对象的。但是如果空间不够,从13行和19行的地址打印输出我们可以看到,因为append超出了指定的空间容量,系统动态重新分配了空间,地址发生了变化。

运行结果:

4
0xc82000a3b0 0xc82000a3b0
dir1 => AAAA
dir2 => BBBBBBBBB
0xc82000a450
dir1 => AAAAsuffix
dir2 => BBBBBBBBB
new path => AAAAsuffix/BBBBBBBBB
成功: 进程退出代码 0.

Go编程tips-3

String在“range”语句中的迭代值

package main

import "fmt"

func main() {  
    data := "A\xfe\x02\xff\x04"
    for _,v := range data {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok)

    fmt.Println()
    for _,v := range []byte(data) {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfe 0x2 0xff 0x4 (good)
}

"switch"

分支默认不是继续下一个分支,默认是'break';
支持条件列表,通过逗号隔开
如果要强制进入下一分支,可以使用"fallthrough"

自增自减

不支持前置,只支持后置,也就是只支持 "i++"的用法

位运算

取反:^ XOR
与: & AND
与非:&^ AND NOT

操作符优先级

&优先于+
<<优先于+
|优先于^

未导出的结构不会被编码

[{ loading ... }]将会得到零值。下面的示例中,成员two没有被编码,所以再反序列化时,就变成空值了。

package main

import (  
    "fmt"
    "encoding/json"
)

type MyData struct {  
    One int
    two string
}

func main() {  
    in := MyData{1,"two"}
    fmt.Printf("%#v\n",in) //prints main.MyData{One:1, two:"two"}

    encoded,_ := json.Marshal(in)
    fmt.Println(string(encoded)) //prints {"One":1}

    var out MyData
    json.Unmarshal(encoded,&out)

    fmt.Printf("%#v\n",out) //prints main.MyData{One:1, two:""}
}

有coroutines的应用的退出

  • 应用是不会等待所有goroutines都完成才退出的,主应用和goroutine之间默认是不交互和感知的。

向关闭的chnnel发送数据会panic

操作没有初始化的"nil"的channel

在一个nil的channel上发送和接收操作会被永久阻塞。这个行为可以在select声明中用于动态开启和关闭case代码块的方法。

package main

import "fmt"
import "time"

func main() {
    inch := make(chan int)
    outch := make(chan int)

    go func() {
        var in <-chan int = inch
        var out chan<- int
        var val int
        for {
            select {
            case out <- val:
                out = nil
                in = inch
            case val = <-in:
                out = outch
                in = nil
            }
        }
    }()

    go func() {
        for r := range outch {
            fmt.Println("result:", r)
        }
    }()

    time.Sleep(0)
    inch <- 1
    inch <- 2
    time.Sleep(3 * time.Second)
}

HTTP响应的关闭

当你使用标准http库发起请求时,你得到一个http的响应变量。如果你不读取响应主体,你依旧需要关闭它。注意对于空的响应你也一定要这么做。对于新的Go开发者而言,这个很容易就会忘掉。
大多数情况下,当你的http响应失败时,resp变量将为nil,而err变量将是non-nil。然而,当你得到一个重定向的错误时,两个变量都将是non-nil。这意味着你最后依然会内存泄露。
通过在http响应错误处理中添加一个关闭non-nil响应主体的的调用来修复这个问题。另一个方法是使用一个defer调用来关闭所有失败和成功的请求的响应主体。

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
)

func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
    _, err = io.Copy(ioutil.Discard, resp.Body)
}

resp.Body.Close()的原始实现也会读取并丢弃剩余的响应主体数据。这确保了http的链接在keepalive http连接行为开启的情况下,可以被另一个请求复用。最新的http客户端的行为是不同的。现在读取并丢弃剩余的响应数据是你的职责。如果你不这么做,http的连接可能会关闭,而不是被重用。这个小技巧应该会写在Go 1.5的文档中。

关闭HTTP连接

一些HTTP服务器保持会保持一段时间的网络连接(根据HTTP 1.1的说明和服务器端的“keep-alive”配置)。默认情况下,标准http库只在目标HTTP服务器要求关闭时才会关闭网络连接。这意味着你的应用在某些条件下消耗完sockets/file的描述符。
你可以通过设置请求变量中的Close域的值为true,来让http库在请求完成时关闭连接。
另一个选项是添加一个Connection的请求头,并设置为close。目标HTTP服务器应该也会响应一个Connection: close的头。当http库看到这个响应头时,它也将会关闭连接。
当然是否需要关闭,是由你的应用场景决定的。

package main

import (  
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {  
    req, err := http.NewRequest("GET","http://golang.org",nil)
    if err != nil {
        fmt.Println(err)
        return
    }

    req.Close = true
    //or do this:
    //req.Header.Add("Connection", "close")

    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(len(string(body)))
}

比较Structs, Arrays, Slices, and Maps

只有每个成员本身能用"=="比较时,对象才可以用"=="比较。

DeepEqual()函数

对于不能直接比较的,可以使用DeepEqual函数
package main

import (
    "fmt"
    "reflect"
)

type data struct {
    num    int               //ok
    checks [10]func() bool   //not comparable
    doit   func() bool       //not comparable
    m      map[string]string //not comparable
    bytes  []byte            //not comparable
}

func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:", reflect.DeepEqual(v1, v2)) //prints: v1 == v2: true

    m1 := map[string]string{"one": "a", "two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:", reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:", reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}

DeepEqual()不会认为空的slice与“nil”的slice相等。这个行为与你使用bytes.Equal()函数的行为不同。bytes.Equal()认为“nil”和空的slice是相等的。

如果你的byte slice(或者字符串)中包含文字数据,而当你要不区分大小写形式的值时(在使用==,bytes.Equal(),或者bytes.Compare()),你可能会尝试使用“bytes”和“string”包中的ToUpper()或者ToLower()函数。对于英语文本,这么做是没问题的,但对于许多其他的语言来说就不行了。这时应该使用strings.EqualFold()和bytes.EqualFold()。

如果你的byte slice中包含需要验证用户数据的隐私信息(比如,加密哈希、tokens等),不要使用reflect.DeepEqual()、bytes.Equal(),或者bytes.Compare(),因为这些函数将会让你的应用易于被定时攻击。为了避免泄露时间信息,使用'crypto/subtle'包中的函数(即,subtle.ConstantTimeCompare())。

[转]Golang 中的格式化输入输出

待整理验证。
原文:Golang 中的格式化输入输出

正文:

【简介】

  fmt 包实现了格式化 I/O 函数,类似于 C 的 printf 和 scanf。格式“占位符”衍生自 C,但比 C 更简单。

【打印】

占位符:

[一般]

  %v 相应值的默认格式。在打印结构体时,“加号”标记(%+v)会添加字段名

  %#v 相应值的 Go 语法表示

  %T 相应值的类型的 Go 语法表示

  %% 字面上的百分号,并非值的占位符

[布尔]

  %t 单词 true 或 false。

[整数]

  %b 二进制表示

  %c 相应 Unicode 码点所表示的字符

  %d 十进制表示

  %o 八进制表示

  %q 单引号围绕的字符字面值,由 Go 语法安全地转义

  %x 十六进制表示,字母形式为小写 a-f

  %X 十六进制表示,字母形式为大写 A-F

  %U Unicode 格式:U+1234,等同于 "U+%04X"

[浮点数及其复合构成]

  %b 无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 的 'b' 转换格式一致。例如 -123456p-78

  %e 科学计数法,例如 -1234.456e+78

  %E 科学计数法,例如 -1234.456E+78

  %f 有小数点而无指数,例如 123.456

  %g 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的 0)输出

  %G 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的 0)输出

[字符串与字节切片]

  %s 字符串或切片的无解译字节

  %q 双引号围绕的字符串,由 Go 语法安全地转义

  %x 十六进制,小写字母,每字节两个字符

  %X 十六进制,大写字母,每字节两个字符

[指针]

  %p 十六进制表示,前缀 0x

[注意]

  这里没有 'u' 标记。若整数为无符号类型,他们就会被打印成无符号的。类似地, 这里也不需要指定操作数的大小(int8,int64)。

  宽度与精度的控制格式以 Unicode 码点为单位。(这点与 C 的 printf 不同, 它以字节数为单位。)二者或其中之一均可用字符 '*' 表示, 此时它们的值会从下一个操作数中获取,该操作数的类型必须为 int。

// 宽度与精度的控制以 Unicode 码点为单位
fmt.Printf("\"%8s\"\n", "123456") // 最大长度为 8
// " 123456"
fmt.Printf("\"%8s\"\n", "你好") // 最大长度为 8
// " 你好"

// 宽度与精度均可用字符 '' 表示
fmt.Printf("%0
.*f \n", 8, 3, 13.25) // 总长度 8,小数位数 3
fmt.Printf("%08.3f \n", 13.25) // 总长度 8,小数位数 3
// 0013.250

  对数值而言,宽度为该数值占用区域的最小宽度;精度为小数点之后的位数。 但对于 %g/%G 而言,精度为所有数字的总数。例如,对于 123.45,格式 %6.2f 会打印 123.45,而 %.4g 会打印 123.5。%e 和 %f 的默认精度为 6;但对于 %g 而言,它的默认精度为确定该值所必须的最小位数。

  对大多数值而言,宽度为输出的最小字符数,如果必要的话会为已格式化的形式填充空格。对字符串而言,精度为输出的最大字符数,如果必要的话会直接截断。

// 宽度与精度标记字符串
fmt.Printf("%8q", "ABC") // 最小长度为 8(包括 %q 的引号字符)
// "ABC"
fmt.Printf("%.8q", "1234567890") // 最大长度为 8(不包括 %q 的引号字符)
// "12345678"

[其它标记]

  + 总打印数值的正负号;对于 %q(%+q)保证只输出 ASCII 编码的字符。

  - 在右侧而非左侧填充空格(左对齐该区域)

  # 备用格式:为八进制添加前导 0(%#o),为十六进制添加前导 0x(%#x)或

  0X(%#X),为 %p(%#p)去掉前导 0x;如果可能的话,%q(%#q)会打印原始(即反引号围绕的)字符串;如果是可打印字符,%U(%#U)会写出该字符的 Unicode 编码形式(如字符 x 会被打印成 U+0078 'x')。

  ' ' (空格)为数值中省略的正负号留出空白(% d);以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开:

fmt.Printf("% x\n", "Hello")
// 48 65 6c 6c 6f

  0 填充前导的 0 而非空格;对于数字,这会将填充移到正负号之后

[注意]

  标记有时会被占位符忽略,所以不要指望它们。例如十进制没有备用格式,因此 %#d 与 %d 的行为相同。

  对于每一个 Printf 类的函数,都有一个 Print 函数,该函数不接受任何格式化, 它等价于对每一个操作数都应用 %v。另一个变参函数 Println 会在操作数之间插入空白, 并在末尾追加一个换行符。

  不考虑占位符的话,如果操作数是接口值,就会使用其内部的具体值,而非接口本身。 因此:

var i interface{} = 23
fmt.Printf("%v\n", i)
// 会打印 23

  若一个操作数实现了 Formatter 接口,该接口就能更好地用于控制格式化。

  若其格式(它对于 Println 等函数是隐式的 %v)对于字符串是有效的(%s %q %v %x %X),以下两条规则也适用:

  1、若一个操作数实现了 error 接口,Error 方法就能将该对象转换为字符串,随后会根据占位符的需要进行格式化。

  2、若一个操作数实现了 String() string 方法,该方法能将该对象转换为字符串,随后会根据占位符的需要进行格式化。

  为避免以下这类递归的情况:

  type X string
  func (x X) String() string { return Sprintf("<%s>", x) }

  需要在递归前转换该值:
  func (x X) String() string { return Sprintf("<%s>", string(x)) }

[格式化错误]

  如果给占位符提供了无效的实参(例如将一个字符串提供给 %d),所生成的字符串会包含该问题的描述,如下例所示:

  类型错误或占位符未知:%!verb(type=value)

Printf("%d", hi)
// %!d(string=hi)

  实参太多:%!(EXTRA type=value)

Printf("hi", "guys")
// hi%!(EXTRA string=guys)

  实参太少:%!verb(MISSING)

Printf("hi%d")
// hi %!d(MISSING)

  宽度或精度不是 int 类型:%!(BADWIDTH)或 %!(BADPREC)

Printf("%*s", 4.5, "hi")
// %!(BADWIDTH)hi

Printf("%.*s", 4.5, "hi")
// %!(BADPREC)hi

  所有错误都始于“%!”,有时紧跟着单个字符(占位符),并以小括号括住的描述结尾。

【扫描】

  一组类似的函数通过扫描已格式化的文本来产生值。Scan、Scanf 和 Scanln 从 os.Stdin 中读取;Fscan、Fscanf 和 Fscanln 从指定的 io.Reader 中读取; Sscan、Sscanf 和 Sscanln 从实参字符串中读取。Scanln、Fscanln 和 Sscanln 在换行符处停止扫描,且需要条目紧随换行符之后;Scanf、Fscanf 和 Sscanf 需要输入换行符来匹配格式中的换行符;其它函数则将换行符视为空格。

  Scanf、Fscanf 和 Sscanf 根据格式字符串解析实参,类似于 Printf。例如,%x 会将一个整数扫描为十六进制数,而 %v 则会扫描该值的默认表现格式。

  格式化行为类似于 Printf,但也有如下例外:

  %p 没有实现
  %T 没有实现
  %e %E %f %F %g %G 都完全等价,且可扫描任何浮点数或复合数值
  %s 和 %v 在扫描字符串时会将其中的空格作为分隔符
  标记 # 和 + 没有实现

  在使用 %v 占位符扫描整数时,可接受友好的进制前缀 0(八进制)和 0x(十六进制)。

  宽度被解释为输入的文本(%5s 意为最多从输入中读取 5 个符文来扫描成字符串),而扫描函数则没有精度的语法(没有 %5.2f,只有 %5f)。

  当以某种格式进行扫描时,无论在格式中还是在输入中,所有非空的连续空白字符 (除换行符外)都等价于单个空格。由于这种限制,格式字符串文本必须匹配输入的文本,如果不匹配,扫描过程就会停止,并返回已扫描的实参数。

  在所有的扫描参数中,若一个操作数实现了 Scan 方法(即它实现了 Scanner 接口),该操作数将使用该方法扫描其文本。此外,若已扫描的实参数少于所提供的实参数,就会返回一个错误。

  所有需要被扫描的实参都必须是基本类型或实现了 Scanner 接口的类型。

  注意:Fscan 等函数会从输入中多读取一个字符(符文),因此,如果循环调用扫描函数,可能会跳过输入中的某些数据。一般只有在输入的数据中没有空白符时该问题才会出现。若提供给 Fscan 的读取器实现了 ReadRune,就会用该方法读取字符。若此读取器还实现了 UnreadRune 方法,就会用该方法保存字符,而连续的调用将不会丢失数据。若要为没有 ReadRune 和 UnreadRune 方法的读取器加上这些功能,需使用 bufio.NewReader。

Go编程tips-2

未使用的imports

可以注释掉或者加下划线'_',否则,编译报错

package main

import (  
    - "fmt"
    "log"
    "time"
)

var _ = log.Println

func main() {  
    _ = time.Now
}

使用简式声明重复声明变量

你不能在一个单独的声明中重复声明一个变量,但在多变量声明中这是允许的,其中至少要有一个新的声明变量。

重复变量需要在相同的代码块内,否则你将得到一个隐藏变量。

Fails:

package main

func main() {  
    one := 0
    one := 1 //error
}

Compile Error:

/tmp/sandbox706333626/main.go:5: no new variables on left side of :=

Works:

package main

func main() {  
    one := 0
    one, two := 1,2

    one,two = two,one
}

用 nil 初始化slice和map

在一个“nil”的slice中添加元素是没问题的,但对一个map做同样的事将会生成一个运行时的panic。

Works:

package main

func main() {  
    var s []int
    s = append(s,1)
}

Fails:

package main

func main() {  
    var m map[string]int
    m["one"] = 1 //error

}

map 的容量

你可以在map创建时指定它的容量,但你无法在map上使用cap()函数。
package main

func main() {  
    m := make(map[string]int,99)
    cap(m) //error
}

Compile Error:

/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap

字符串不能初始化为 "nil"

这对于经常使用“nil”分配字符串变量的开发者而言是个需要注意的地方。

二维数组的创建

你可以使用纯一维数组、“独立”切片的切片,“共享数据”切片的切片来构建动态的多维数组。

如果你使用纯一维的数组,你需要处理索引、边界检查、当数组需要变大时的内存重新分配。

使用“独立”slice来创建一个动态的多维数组需要两步。首先,你需要创建一个外部的slice。然后,你需要分配每个内部的slice。内部的slice相互之间独立。你可以增加减少它们,而不会影响其他内部的slice。

package main

func main() {  
    x := 2
    y := 4

    table := make([][]int,x)
    for i:= range table {
        table[i] = make([]int,y)
    }
}

使用“共享数据”slice的slice来创建一个动态的多维数组需要三步。首先,你需要创建一个用于存放原始数据的数据“容器”。然后,你再创建外部的slice。最后,通过重新切片原始数据slice来初始化各个内部的slice。

package main

import "fmt"

func main() {  
    h, w := 2, 4

    raw := make([]int,h*w)
    for i := range raw {
        raw[i] = i
    }
    fmt.Println(raw,&raw[4])
    //prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>

    table := make([][]int,h)
    for i:= range table {
        table[i] = raw[i*w:i*w + w]
    }

    fmt.Println(table,&table[1][0])
    //prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}

判断map中节点是否存在

检测给定map中的记录是否存在的最可信的方法是,通过map的访问操作,检查第二个返回的值。

package main

import "fmt"

func main() {  
    x := map[string]string{"one":"a","two":"","three":"c"}

    if _,ok := x["two"]; !ok {
        fmt.Println("no entry")
    }
}

string的修改

如果是ASCII码,要转换成[]byte,如果是unicode,要转化成rune byte
[]byte的方法

package main

import "fmt"

func main() {  
    x := "text"
    xbytes := []byte(x)
    xbytes[0] = 'T'

    fmt.Println(string(xbytes)) //prints Text
}

string 与 rune byte 的关系

在Go当中 string底层是用byte数组存的,并且是不可以改变的。

例如 s:="Go编程" fmt.Println(len(s)) 输出结果应该是8因为中文字符是用3个字节存的。

len(string(rune('编')))的结果是3

如果想要获得我们想要的情况的话,需要先转换为rune切片再使用内置的len函数

fmt.Println(len([]rune(s)))

结果就是4了。

所以用string存储unicode的话,如果有中文,按下标是访问不到的,因为你只能得到一个byte。 要想访问中文的话,还是要用rune切片,这样就能按下表访问。

判断字符串是否为UTF8

为了知道字符串是否是UTF8,你可以使用“unicode/utf8”包中的ValidString()函数。

求字符串长度

ASCII:len();
unicode:RuneCountInString()。实际上,RuneCountInString()求的是rune的个数,不是绝对的字符个数,因为一个字符可能占多个rune

在多行的Slice、Array和Map语句中遗漏逗号

Fails:

package main

func main() {  
    x := []int{
    1,
    2 //error
    }
    _ = x
}

Works:

package main

func main() {  
    x := []int{
    1,
    2,
    }
    x = x

    y := []int{3,4,} //no error
    y = y
}

当你把声明折叠到单行时,如果你没加末尾的逗号,你将不会得到编译错误

log.Fatal和log.Panic不仅仅是Log

Logging库一般提供不同的log等级。与这些logging库不同,Go中log包在你调用它的Fatal()和Panic()函数时,可以做的不仅仅是log。当你的应用调用这些函数时,Go也将会终止应用 🙂

Go面向对象

本来想写下相关的内容,找材料时,发现了这篇不错的内容,强烈推荐,关于面向对象的基本用法都讲到了。

原文:Go面向对象
原英文:Go Object Oriented Design

正文:

Go是一个完全面向对象的语言。例如,它允许基于我们定义的类型的方法,而没有像其他语言一样的装箱/拆箱操作。

Go没有使用classes,但提供很多相似的功能:

·通过嵌入实现的自动消息委托

·通过接口实现多台

·通过exports实现的命名空间

Go语言中没有继承。忘记is-a的关系,而是就组合而言的面向对象设计。

“使用经典的继承始终是可选的;每个问题都可以通过其他方法得到解决” - Sandi Metz

通过例子说明组合

最近阅读了一篇Ruby的面向对象编程实践, 我决定使用Go语言翻译这个例子。

Chapter 6说明了一个简单的问题。维修工需要知道自行车出行需要带上的备件,决定于哪一辆自行车已经被租出去。问题可以通过经典的继承来解决,山地车和公路自行车是自行车基类的一个特殊化例子。Chapter 8使用组合改写了同一个例子。我很高兴这个例子翻译成Go。让我们看看。

Packages(包)

  1. package main
  2. import "fmt"

包提供了命名空间概念. main() 函数是这个包的入口函数. fmt包提供格式化功能

Types(类型)

  1. type Part struct {
  2.     Name        string
  3.     Description string
  4.     NeedsSpare  bool
  5. }

我们定义了一个新的类型名为Part, 非常像c的结构体

  1. type Parts []Part

Parts类型是包含Part类型的数组切片, Slice可以理解为动态增长的数组, 在Go中是很常见的.

我们可以在任何类型上声明方法,  所以我们不需要要再去封装 []Part, 这意味着 Parts 会拥有slice的所有行为, 再加上我们自己定义的行为方法.

方法

  1. func (parts Parts) Spares() (spares Parts) {
  2.     for _, part := range parts {
  3.         if part.NeedsSpare {
  4.             spares = append(spares, part)
  5.         }
  6.     }
  7.     return spares
  8. }

Go中定义方法就像一个函数,除了它有一个显式的接收者,紧接着func之后定义。这个函数利用命名返回变量,并为我们初始化备件。

方法的主体十分简单。我们重复parts,忽略索引的位置(_),过滤parts后返回。append builtin 需要分配和返回一个大的切片,因为我们并没有预先分配好它的容量。

这段代码没有ruby代码来得优雅。在Go语言中有过滤函数,但它并非是builtin.

内嵌

  1. type Bicycle struct {
  2.     Size string
  3.     Parts
  4. }

自行车由Size和Parts组成。没有给Parts指定一个名称,我们是要保证实现 内嵌。这样可以提供自动的委托,不需特殊的声明,例如bike.Spares()和bike.Parts.Spares()是等同的。

如果我们向Bicycle增加一个Spares()方法,它会得到优先权,但是我们仍然引用嵌入的Parts.Spares()。这跟继承十分相似,但是内嵌并不提供多态。Parts的方法的接收者通常是Parts类型,甚至是通过Bicycle委托的。

与继承一起使用的模式,就像模板方法模式,并不适合于内嵌。就组合和委托而言去考虑会更好,就如我们这个例子一样。

Composite Literals(复合语义)

  1. var (
  2.     RoadBikeParts = Parts{
  3.         {"chain""10-speed"true},
  4.         {"tire_size""23"true},
  5.         {"tape_color""red"true},
  6.     }
  7.     MountainBikeParts = Parts{
  8.         {"chain""10-speed"true},
  9.         {"tire_size""2.1"true},
  10.         {"front_shock""Manitou"false},
  11.         {"rear_shock""Fox"true},
  12.     }
  13.     RecumbentBikeParts = Parts{
  14.         {"chain""9-speed"true},
  15.         {"tire_size""28"true},
  16.         {"flag""tall and orange"true},
  17.     }
  18. )

Go提供优美的语法,来初始化对象,叫做 composite literals。使用像数组初始化一样的语法,来初始化一个结构,使得我们不再需要ruby例子中的Parts工厂。

  1. func main() {
  2.     roadBike := Bicycle{Size: "L", Parts: RoadBikeParts}
  3.     mountainBike := Bicycle{Size: "L", Parts: MountainBikeParts}
  4.     recumbentBike := Bicycle{Size: "L", Parts: RecumbentBikeParts}

Composite literals(复合语义)同样可以用于字段:值的语法,所有的字段都是可选的。

简短的定义操作符(:=)通过Bicycle类型,使用类型推论来初始化roadBike,和其他。

输出

  1. fmt.Println(roadBike.Spares())
  2. fmt.Println(mountainBike.Spares())
  3. fmt.Println(recumbentBike.Spares())

我们将以默认格式打印 Spares 的调用结果:

  1. [{chain 10-speed true} {tire_size 23 true} {tape_color red true}]
  2. [{chain 10-speed true} {tire_size 2.1 true} {rear_shock Fox true}]
  3. [{chain 9-speed true} {tire_size 28 true} {flag tall and orange true}]

组合 Parts

  1.     comboParts :Parts{}
  2.     comboParts = append(comboParts, mountainBike.Parts...)
  3.     comboParts = append(comboParts, roadBike.Parts...)
  4.     comboParts = append(comboParts, recumbentBike.Parts...)
  5.     fmt.Println(len(comboParts), comboParts[9:])
  6.     fmt.Println(comboParts.Spares())
  7. }

Parts 的行为类似于 slice。按照长度获取切片,或者将数个切片结合。Ruby 中的类似解决方案就数组的子类,但是当两个 Parts 连接在一起时,Ruby 将会“错置” spares 方法。

“……在一个完美的面向对象的语言,这种解决方案是完全正确的。不幸的是,Ruby语言并没有完美的实现……”
—— Sandi Metz

在 Ruby 中有一个那看的解决方法,使用 Enumerable、forwardable,以及 def_delegators。 Go有没有这样的缺陷。 []Part 正是我们所需要的,且更为简洁(更新:Ruby 的 SimpleDelegator 看上去好了一点)。

接口 Interfaces

Go的多态性由接口提供。不像JAVA和C#,它们是隐含实现的,所以接口可以为不属于我们的代码定义。

和动态类型比较,接口是在它们声明过程中静态检查和说明的,而不是通过写一系列响应(respond_to)测试完成的。

“不可能不知不觉的或者偶然的创建一个抽象;在静态类型语言中定义的接口总是有倾向性的。” - Sandi Metz

给个简单的例子,假设我们不需要打印Part的NeedsSpare标记。我们可以写这样的字符串方法:

  1. func (part Part) String() string {
  2.     return fmt.Sprintf("%s: %s", part.Name, part.Description)
  3. }

然后对上述Print的调用将会输出这样的替代结果:

  1. [chain: 10-speed tire_size: 23 tape_color: red]
  2. [chain: 10-speed tire_size: 2.1 rear_shock: Fox]
  3. [chain: 9-speed tire_size: 28 flag: tall and orange]

这个机理是因为我们实现了fmt包会用到的Stringer接口。它是这么定义的:

  1. type Stringer interface {
  2.     String() string
  3. }

接口类型在同一个地方可以用作其它类型。变量与参数可以携带一个Stringer,可以是任何实现String() string方法签名的接口。

Exports 导出

Go 使用包来管理命名空间, 要使某个符号对其他包(package )可见(即可以访问),需要将该符号定义为以大写字母开头,  当然,如果以小写字母开关,那就是私有的.包外不可见.

  1. type Part struct {
  2.     name        string
  3.     description string
  4.     needsSpare  bool
  5. }

为了对Part类型应用统一的访问原则(uniform access principle), 我们可以改变Part类型的定义并提供setter/getter 方法,就像这样:

  1. func (part Part) Name() string {
  2.     return part.name
  3. }
  4. func (part *Part) SetName(name string) {
  5.     part.name = name
  6. }

这样可以很容易的确定哪些是public API, 哪些是私有的属性和方法, 只要通过字母的大小写.(例如(part.Name()vs.part.name)

注意 我们不必要对 getters 加前Get, (例如.GetName),Getter不是必需,特别是对于字符串,当我们有需要时,我们可以使用满足Stringer 类型接口的自定义的类型去改变Name 字段。

找到一些私有性

私有命名(小写字母)可以从同一个包的任何地方访问到,即使是包含了跨越多个文件的多个结构。如果你觉得这令人不安,包也可以像你希望的那么小。

可能的情况下用(更稳固的)公共API是一个好的实践,即使是来自经典语言的同样的类中。这需要一些约定,当然这些约定可以应用在GO中。

最大的好处

组合,内嵌和接口提供了Go语言中面向对象设计的强大工具。继承概念的思想真的不起什么作用。相信我,我尝试了

习惯Go需要思维的改变,当触及到Go对象模型的力量时,我非常高兴的吃惊于Go代码的简单和简洁。

Go编程tips-1

结构体的初始化

示例代码中,前者是创建普通对象,后者是创建指针

package main

import (
    "fmt"
)

type Rect struct {
    x, y          float64
    width, height float64
}

func main() {
    // common object
    rect2 := Rect{}
    rect3 := Rect{0, 0, 100, 200}
    rect4 := Rect{width: 100, height: 200}
    fmt.Println(rect2, rect3, rect4)

    /*
        // pointer objects
        rect1 := new(Rect)
        rect2 := &Rect{}
        rect3 := &Rect{0, 0, 100, 200}
        rect4 := &Rect{width: 100, height: 200}
        fmt.Println(rect1, rect2, rect3, rect4)
    */
}

变参

go支持变参,变参中所有参数的类别必须是同一种,且必须是最后一个形参。使用方法如下:“…type”表示具有不定个type类型的参数,不定参数实质上是一个slice类型,故可以使用range对其参数进行取值。如下例子。

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

defer 与return执行顺序

当函数执行到最后时,先执行defer语句,然后才执行return语句.所以可以利用这个来进行资源安全关闭,解加锁,记录执行情况等。defer是采用先进后出的模式的,这种情形与栈的情况一致。注意:定义的defer延迟操作,如有提供参数会发生值的拷贝,尽管这些函数在退出时才执行,但所使用的参数是在定义时就进行拷贝,拷贝的原则和变量的赋值原则一个,slice,map,channel是指针拷贝.如下例子:

package main

import (
    "fmt"
)

func main() {
    x := 1
    defer func(a int) { //直接将x=1赋值给a,虽然他在后面才执行.
        fmt.Println("a=", a)
    }(x)
    defer func() {
        fmt.Println("x=", x) //经过x++后,x值变为2
    }()
    x++
}

运行结果:
x= 2
a= 1

临时变量的作用域

go语言中对于堆和栈的内存分配没有严格区分,在go中返回一个局部变量的地址是绝对没有问题的,变量关联的存储在函数返回后依然存在.(注:尤其对由C/C++转过来的程序员,开始肯定不是很适应,但是go这种内存分配方式解放了程序员,使得程序员能够专注做事情,而不用花费太多的时间在堆和栈的内存分配上).更直接的说,在go语言中,如果一个局部变量在函数返回后仍然被使用,那么这个变量会在堆heap,而不是栈stack中进行内存分配.详情参考How do I know whether a variable is allocated on the heap or the stack?

Mac下零基础学习go语言-2-开发环境的搭建

环境介绍

  • 系统:OS X EI Capitan 10.11.1
  • go version:1.5.1

安装

下载最新的安装包 https://golang.org/dl/

mac下安装

直接点击,按照引导就可以完成安装。

linux下安装

linux下实际就是手动设置文件路径了,大概的动作如下:

#!/bin/bash
tar -C /usr/local -xzf go*.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile

最后重启,或者:

source /etc/profile

验证版本

终端验证,输入

go version
go version go1.11.2 linux/amd64

配置环境变量

设置环境变量,go默认安装在/usr/local/go 下面,参见:Mac OS X 配置环境变量

所以我们修改 .bash_profile:

export GOPATH=/Users/alex/dev/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN

注意:

  • 默认情况下GOROOT已经在安装时指定为安装目录了,是不需要设置的
  • GOPATH 设置成你的本地开发路径,而不是安装路径

设置完成后可以用 go env 命令检测

测试

在 GOPATH下 的src目录下创建项目demo。注意这个将是默认的bin档名。 然后在demo目录下创建main.go

package main

import (
  "fmt"
)

func main() {
  fmt.Println("hello world");
}

然后运行 go build demo,如果在demo项目目录下,直接 go build,完成后,在项目目录下生成 demo

直接运行 ./demo,就会输出 :hello world

DEBUG

初学者建议在LiteIDE下面debug,因为IDE直接UI支持,熟悉了以后也可以在命令行下面直接debug。第一次试用时要解决GDB的证书签名问题,可以参看网上同学给出的操作流程。

下面终于可以开始coding了!

Mac下零基础学习go语言-1-开篇

缘由

一直以来都在慢慢悠悠地关注各种go的相关主题,因为本职还是做c/c++相关的工作,所以一致以来都没有系统的对go进行系统的学习,仅局限于对相关的知识点进行进行简单的了解和学习。最近由于工作变动原因,稍息赋闲,打算对go做一个系统的学习,所以开此主题,希望能详细记录下学习的细节,能对后来的新人有个引路的作用

为什么学习go

这个应该是最先要回答的问题,也是群里经常争论的问题,为什么要选go,而不是php,java。
首先说下go的几个特点

  • 静态语言
  • 有高级语法,又不是非常复杂晦涩
  • 高并发,非常小的协程开销,非常适合服务器场景
  • 其他的网友自行google,这里不详细展开

我自己的观点是,如果你是c,c++的背景,要提高后台开发的效率,go是不二的选择,go的语法复杂度,介于c和c++之间,功能上长于后端开发,也有完备的库,开发效率上会有很大提高。
如果你原来就是php,java,node的背景,你所做的工作都是基于接口的数据开发,也建议尝试go。go天生适合做后台接口,开发效率高,性能也不错。
但如果你想要做一个web系统,其实go就目前的情况看,并不比php,java,node更合适,不必盲目跟风。
最后,建议初学者读一下这篇
Donovan/Kernighan大神们关于go语言的问答
其中关于go的特点,潜力,为什要选go等都有涉及。
其他的问题参见go的FAQ

环境

mac air
go version go1.5.1 darwin/amd64

主要学习材料

  1. google go官网教程
  2. 谢大的书籍:
    go web编程
    go实践开发
  3. 无闻的<<go编程基础>>视频教程
  4. 雨痕的笔记
  5. 其他:go学习资料链接汇总

最后,由于本人在go方面也是小白,难免水平有限,如果有大牛路过,一定不要怜惜您的键盘,狠狠的拍吧

OOSMOS介绍

原英文 oosmos 官网
无意中看见大牛的微博提及,正好物联网也是本人感兴趣的领域,正好周末闲暇,翻译下文章,切换下心情。水平有限,还请路过的大侠多指点。

正文:

  1. 什么是OOSMOS
    OOSMOS(Object-Oriented State Machine Operating System)代表面向对象的状态机操作系统。它是一个以基本的上下文为对象的操作系统,而不象传统的以线程为对象。

    因为没有线程,所以没有线程栈(译者:消耗较小资源),所以OOSMOS是一个理想的用于那些传统的基于线程的操作系统无法满足的内存紧张的平台环境。

    OOSMOS 有着强大的分级状态机引擎可以管理非常复杂的事件驱动系统。

  2. OOSMOS的优点

  • 很适合:
    物联网(IOT)
    教育-STEM
    创客活动
    医疗设备
    自动化设备
    控制系统
    航天系统
    企业
    逻辑控制/工作流
    各种界面切换控制
    机器人
    基础的只要是事件驱动的系统
  • 免费
  • 适合 c 甚至 c++
  • 封装简单
  • 对CPU消耗低
  • 没有线程,也就没有线程栈
  • 特别的进行了面相对象的封装和无关信息隐藏
  • 灵活扩展
  • 高效,对象的时间和内存消耗固定
  • 强大的状态机引擎
    支持不限数量的当前对象超时。
  • 支持状态查询
  • 支持异步函数
  • 支持正交区域
  • 支持状态机调试
  • 支持事件管理
    事件码通过对象的类来管理
    事件支持参数传递
    事件支持发布/订阅模式
    事件对象有自己的事件队列
  • 移植方便,基于c 89,还在以下的环境测试:
    MPLABX(包含ChipKit的PIC32系列)
    Arduino(AVR,ARM)
    ChipKit(PIC32,使用类Arduino或者MPLAB X)
    Energia(MSP430,使用类Arduino IDE)
    Intel Galileo
    mbed(ARM,使用web IDE)
    MSP430
    LightBlue Bean
    ESP8266
    Linux(树莓派)
    Windows(Visual Studio)
  1. OOSMOS是怎么工作的-简介
    OOSMOS由对象组成,每一个对象可以有一个状态机能反映对象当前的状态。
    工作时,最重要的是可视化,所以让我们看一些已经运行一段时间的状态机(图 1)

    图1 两个运行态的对象状态切换图
    上图中,红框代表对象的当前状态,我们看到A的状态是On,B的状态是Off。
    OOSMOS循环地一个一个运行每一个对象。
    当OOSMOS反转对象A时,A就变成On状态,唯一退出On状态的方式是tm(m_TimeOnMS)超时,超时前,每一次都运行A。OOSMOS会检查对象是否超时,如果超时,将会向对象发送TIMEOUT事件,对象将会调用oosmos_Transition方法,状态由On切换为Off。在切换的同时,OOSMOS将会在On状态调用EXIT事件,然后调用ENTER事件,变成Off状态。如果没有超时,切换就不会发生,对象会一直保持当前状态。

  2. 学习OOSMOS的最好方式
    学习OOSMOS最好的方式是下载和解压项目,然后编译其中的例子,选择你最合适和熟悉的平台。修改下代码,看下新的项目行为,从而通过比较学习。
    开始学习时,Windows或者Linux例子是不错的选择,你可以用源码级别的调试工具去设置断点和现实变量。
    OOSMOS用户手册涵盖所有的概念和技术点。OOSMOS API参考手册提供快速的函数和宏查看。

over,have fun!

go基础笔记

本文不讲解详细的语法,详细的知识请参见 tour.go-zh.org,只是纪录本人学习中觉得需要注意或者有意思的点

  • switch 可以不带条件,用来简化冗长的 if-else if-else
  • 结构体初始化参数可变,缺省有零值。支持 Name:特定字段赋值
  • slice 区间表示时左闭右开,可以省略上标或下标
  • _ 用来在 slice 值对中忽略序号和值
  • make 用来创建slice map channel ,其他的用new
  • map 可以同时获取值和行为结果(true/falses)
  • 值可以隐式表示函数
  • 闭包的概念

MAC下go code安装问题

在安装go lang时,已经设置过GOPATH,go 也运行正常,但是安装 gocode时提示:

Agreeing to the Xcode/iOS license requires admin privileges, please re-run as root via sudo.
package github.com/nsf/gocode: exit status 69

当我们使用sudo 时,提示:

package github.com/nsf/gocode: cannot download, $GOPATH not set. For more details see: go help gopath

虽然之前已经设置,但是当切换权限时,也切换了环境变量,所以当用sudo管理员权限时提示没有设置环境变量。

解决:

sudo env GOPATH=/Users/alex/dev/go 
go get -u github.com/nsf/gocode

在命令行里临时指定env变量,完成

You have not agreed to the Xcode license agreements

在安装gocode时,提示You have not agreed to the Xcode license agreements 相关的错误提示:
You have not agreed to the Xcode license agreements, please run 'xcodebuild -license' (for user-level acceptance) or 'sudo xcodebuild -license' (for system-wide acceptance) from within a Terminal window to review and agree to the Xcode license agreements.
根据提示,执行 sudo xcodebuild -license 或者直接通过系统运行 xcode,会出现 license界面,点击同意,再次安装就ok了

Mac OS X 配置环境变量

/etc/profile;/etc/bashrc 是针对系统所有用户的全局变量,只有root用户才能修改这两个文件,对一般用户来说是他们是只读的。一般用户要想修改它们,可以在命令前加sudo,意思是以Root身份执行,比如:sudo vi /etc/profile ,然后按照提示输入密码即可。

Mac OS X 10.3之后默认的是Bourne Shell。因此,对于一般用户而言,通常我们建议去修改~/.bash_profile来设置环境变量,它是用户级的设置,只对当前用户有效。

增加环境变量

操作步骤:
1. 打开Terminal(终端)
2. 输入:vi ~/.bash_profile
3. 设置PATH:export PATH=/usr/local/mysql/bin:$PATH
4. 输入::wq //保存并退出vi
5. 修改立即生效:source ~/.bash_profile
6. 查看环境变量的值:echo $PATH

删除环境变量

unset DYLD_LIBRARY_PATH,其中DYLD_LIBRARY_PATH(环境变量名)
删除环境变量之后需要先logout,关闭terminal,再重新打开terminal

恢复系统默认环境变量

PATH=$(getconf PATH),执行此命令将环境变量恢复到系统初始值

爬虫(spider/crawl)原理

学习了很多爬虫的原理和模块划分后,发现很多项目的原理以及组件划分似乎都是类似的,所以虽然这篇文章的原名不是介绍爬虫原理,但内容时机上就是介绍通用爬虫的基本原理和流程以及工作组件划分 - SpiderDuck:Twitter的实时URL抓取器
英文原文已经被twitter移除了,大家且看且珍惜吧。

正文:

推文中常常含有指向 web 各种内容的链接,包括图像、视频、新闻文章以及博客帖。SpiderDuck 是Twitter 的一项用于对这些链接进行实时抓取、解析下载内容并提取有趣的元数据的服务,并且,它使得其它的 Twitter 服务能够在数秒钟内即可使用这些数据。

Twitter 的许多的团队都需要访问链接的内容,尤其是需要用实时内容来改进 Twitter 的产品。比如:
搜索:需要对解析后的 URL 建立索引并改进搜索质量;
客户端:需要在推文旁边同时显示某些类型的媒体内容,比如照片;
发推按钮:用以统计每条 URL 到底被共享了多少次;
信任与安全部门:用以帮助检测恶意软件和垃圾信息;
分析:发掘关于 Twitter 上被共享的链接的统计汇总信息。
背景

SpiderDuck 出现之前,Twitter 有一个用发送 HEAD 请求和跟踪跳转来解析所有推文中的 URL 的服务。这个服务很简单,也满足了公司当时的需求,但是它有几项限制:
它解析 URL,但是并不真正的下载内容。解析信息被存在内存中缓存里但是并没有永久地保存在磁盘上。这意味着如果内存中的缓存实例被重启的话,数据就都会丢失。
它没有实现现代抓取机器人的“礼貌性”,比如访问频率限制和遵守 robots.txt 的指示等等。
显然,我们需要建造一个能够克服上述限制并满足公司的长远目标的真正的 URL 抓取器。我们最初的想法使用某个开源的抓取代码,或以其为基础,但是我们意识到几所有能用的开源抓取器都有两个我们不需要的特征:
它们都是递归抓取器。也就是说,它们是被设计来抓取页面并递归抓取从页面里面提取出来的所有链接的。递归抓取给爬虫的调度和长期队列的维护带来了很多复杂性,在我们情况下并不必要。
它们被优化来进行大批量抓取。我们需要的是快速的、实时的 URL 抓取。
因此,我们决定设计一个能够满足 Twitter 的实时需要的新系统,并能够随其增长水平扩展。为了避免重新发明轮子,我们把新系统大部分建立在了开源的模块之上,从而可以继续利用开源社区的贡献。

这是 Twitter 的工程问题里面很典型的一个——它们和其它大型互联网公司的问题很类似,但是要求所有东西都能够实时工作又带来了独特而有趣的挑战。

系统概览

这里是讲述 SpiderDuck 如何工作的概览。下图画出了它的主要部件。

SpiderDuck:Twitter的实时URL抓取器
Kestrel:这是一个在 Twitter 广泛使用的,用以对新进推文进行排队的消息队列系统。

Scheduler:这些工作单元决定是否要抓取一个 URL,计划抓取时间,并跟踪重定向跳转(如果有的话)。抓取之后,它会解析下载的内容,提取元数据(metadata),并把元数据写回 Metadata Store,把原始数据写入 Content Store。每个 scheduler 都独立工作;也就是说,我们可以把任意数量的 scheduler 加入到系统中,随推文和 URL 的数量增加水平地扩展系统。

Fetcher:这些是用于维护短期 URL 抓取队列的 Thrift 服务器,它们发送实际的 HTTP 抓取请求并实现速率控制和处理 robots.txt。就像 scheduler 一样,它们可以随抓取速率水平扩展。

Memcached:这是 fetcher 使用的分布式缓存,用以临时存储 robots.txt 文件。

Metadata Store:这是一个基于 Cassandra 的分布式散列表,用以存储网页的元数据和以 URL 索引的解析信息,以及系统最近遇到的每个 URL 的抓取状态。这个存储为 Twitter 所有的需要实时访问 URL 数据的客户服务。

Content Store:这是一个 HDFS 集群,用以存储下载的内容和所有的抓取信息。

现在我们将更详细地介绍 SpiderDuck 的两个主要部件——URL Scheduler 和 URL Fetcher。

URL Scheduler-URL 调度器)

下面的图表画出了 SpiderDuck Scheduler 里面的几个处理阶段。
SpiderDuck:Twitter的实时URL抓取器
就像 SpiderDuck 的大部分一样,Scheduler 也是建立在一个 Twitter 开发的开叫做 Finagle 的开源异步 RPC 框架之上。(实际上,这是最早的一个利用 Finagle 的项目)。上图里面的每一个方块,除了 Kestrel Reader,都是一个 Finagle Filter —— 一个允许把一系列处理阶段连接成一个完全异步流水线的抽象概念。完全异步则允许 SpiderDuck 以较少的、固定数量的线程处理很高的流量。

Kestrel Reader 会不断地询问是否有新的推文出现。当推文进来时,它们被发送到 Tweet Processor,它从其中提取 URL。每条 URL 然后就会被送到 Crawl Decider 阶段。该阶段从 Metadata Store 读取 URL 的抓取状态,以确定 SpiderDuck 是否之前已经见过了这个 URL。Crawl Decider 然后根据一个预先制定的抓取策略(就是如果 SpiderDuck 在过去 X 天内已经见过了此 URL 则不再重复抓取)来决定是否该 URL 应该被抓取。如果 Decider 决定不抓取该 URL,它会记录状态以表示处理完成。如果它决定要抓取这个 URL,它就会把 URL 送到 Fetcher Client 阶段。

Fetcher Client 阶段使用客户端库和 Fetcher 交谈。客户端库实现了逻辑用以够决定哪个 Fetcher 会被用来抓取该 URL;它也能够处理重定向跳转。(重定向跳转链非常普遍,因为 Twitter 上的贴的 URL 多数都被缩短了)经过 Scheduler 的每个 URL 都有一个相关的上下文对象。Fetcher Client 会把包括状态、下载的头以及内容的抓取信息添加到上下文对象中,并将其传递给 Post Processor。Post Processor 把下载的数据交给元数据提取器,它会检测页面的编码,并使用一个开源的 HTML5 解析器解析页面的内容。提取库实现了一系列启发式算法,用于提取诸如标题、简介、以及代表图片等元数据。Post Processor 然后把所有的元数据和抓取信息写入 Metadata Store。如果需要的话,Post Processor 还会调度一系列相关抓取。相关抓取的例子之一就是嵌入的媒体内容,比如图片。

后期处理(post processing)结束之后,URL 上下文对象被交给下一个阶段,其会使用一个叫做 Scribe 的开源日志聚集器在 Content Store (HDFS)的日志中记录所有信息,包括完整的内容。该阶段还通知所有的感兴趣的监听者 URL 处理结束了。通知使用了一个简单的发布者-订阅者模型,用 Kestrel 的分散队列实现。

所有的处理阶段都是异步运行的 —— 没有任何线程会等待一个阶段完成。和每个正在处理中的 URL 相关的状态都保存在相关的上下文对象中,所以线程模型也非常简单。异步实现也受益于 Finagle 和 Twitter Util 库提供的方便的抽象和构件。

URL Fetcher-URL 抓取器

让我们来看看 Fetcher 如何处理一条 URL。
SpiderDuck:Twitter的实时URL抓取器
Fetcher 通过 Thrift 界面接收到 URL。经过一些简单的确认之后,Thrift 处理器把 URL 传递给 Request Queue Manager (请求队列管理器),其把 URL 指定给某个合适的请求队列。一个调度了的任务会按照固定的速率从请求队列中读取。一旦 URL 被从队列中取出来了,它就会被送到 HTTP Service 处理。建造在 Finagle 上面的 HTTP Service 首先检查 URL 相关的主机是否已经在缓存中了。如果没有,那么它会为它创建一个 Finagle 客户,并调度好 robots.txt 文件的抓取。在 robots.txt 被下载之后,HTTP Service 会抓取许可的 URL。robots.txt 文件本身是被缓存的,在进程中的 Host Cache 和 Memcached 里面各一份,以防止每次有该主机新的 URL 进来时重复抓取。

一些叫做 Vulture (秃鹫)的任务周期性地检查 Request Queue (请求队列)和 Host Cache (主机缓存)以寻找有一段时间都没有被使用的队列和主机;如果找到了,它们就会被删除。Vulture 还会通过日志和 Twitter Commons 状态输出库报告有用的统计信息。

Fetcher 的 Request Queue 还有一个重要的目标:速率限制。SpiderDuck 限制对每个域名发出的 HTTP 抓取请求,以保证不会使得 web 服务器过载。为了准确地限制速率,SpiderDuck 保证每一个 Request Queue 在任一时刻都被指定到刚好一个 Fetcher,并且能够在 Fetcher 失效的时候自动重新指定到另一个 Fetcher 上。一个叫做 Pacemaker 的机群软件包会把 Request Queue 指定给 Fetcher 并管理失效转移。Fetcher 客户库根据 URL 的域名把它们分配到不同的 Request Queue。对于整个 web 设置的默认速率限制也能够根据需要被对于具体的域名设置的速率限制取代。也就是说,如果 URL 进来的速度比处理它们的速度还要快,它们就会拒绝请求,以告诉客户端应该收敛,或者采取其它合适措施。

为了安全,Fetcher 被部署到了 Twitter 数据中心里面的一个特殊区域 DMZ。这意味着 Fetcher 不能访问 Twitter 的产品机群和服务。所以,确保它们的轻量级设计和自力更生非常重要,这也是一条指导很多方面的设计的原则。

Twitter 如何使用 SpiderDuck

Twitter 服务以很多方式使用 SpiderDuck 的数据。大部分会直接查询 Metadata Store 以获取 URL 的元数据(比如,标题)以及解析信息(所有重定向跳转之后的最终规范化 URL)。Metadata Store 是实时填充的,一般是在 URL 在推文中发布后的几秒钟内。这些服务并不直接和 Cassandra 交谈,而是通过一个代理这些请求的 Spiderduck Thrift 服务器。这个中间层为 SpiderDuck 提供了灵活性,使其能够透明地切换存储系统,如有需有。它同时也支持了比直接访问 Cassandra 更高级的 API 抽象。

其它服务会周期性的处理 SpiderDuck 在 HDFS 上的日志以生成聚合统计信息,用以 Twitter 的内部测量仪表板或者进行其它批量分析。仪表板帮助我们回答诸如“每天 Twitter 上有多少图片被共享?”、“Twitter 用户最经常链接到什么新闻网站?”以及“我们昨天从某个网站抓取了多少网页?”之类的问题。

需要注意的是,这些服务一般不会告诉 SpiderDuck 需要抓取什么东西;SpiderDuck 已经抓取了进入 Twitter 的所有 URL。取而代之,这些服务在 URL 可用之后询问它们的相关信息。SpiderDuck 也允许这些服务直接请求 Fetcher 通过 HTTP 抓取任意内容,(这样它们就能受益于我们的数据中心设置、速率限制、robot.txt 支持等功能),但这种用法并不普遍。

性能数据

SpiderDuck 每秒处理数百条 URL。这中间的大部分都是在 SpiderDuck 的抓取策略所定义的时间窗口里独一无二(unique)的,所以它们会被抓取。对于抓取了的 URL,SpiderDuck 处理延迟中值在 2 秒以下,99% 的处理延迟低于 5 秒。该延迟是基于推问发布时间测量的,也就是说在用户点击“发推”按钮后 5 秒内,推文中的 URL 就被提取出来,做好了抓取准备,获取了所有的重定向跳转,下载并解析了内容,并提取了元数据,并且它们已经通过 Metadata Store 对于客户可用了。这中间大部分的时间要么花在了 Fetcher Request Queue (因为速率限制)中,或者花在了从外部 web 服务器实际获取该 URL 上。SpiderDuck 本身只增加了几百毫秒的额外处理时间,大部分都花在 HTML 解析上。

SpiderDuck 的基于 Cassandra 的 Metadata Store 能够处理接近每秒 10,000 个请求。这些请求一般是针对单独或者小批次(小于 20 个)URL 的,但是它也能够处理大批次(200~300 个 URL)的请求。这个存储系统的读取延迟中值在 4 ~ 5 秒左右,第 99 百分区间在 50 ~ 60 毫秒左右。

致谢

SpiderDuck 的核心团队包括以下成员:Abhi Khune,Michael Busch,Paul Burstein,Raghavendra Prabhu,Tian Wang 以及 Yi Zhuang。此外,我们希望对遍布全公司的以下人员表示感谢,他们要么直接为该项目做出了贡献,帮助设置了 SpiderDuck 直接依赖的部件(比如 Cassandra、Finagle、Pacemaker 以及 Scribe),要么帮助建立了 SpiderDuck 独特的数据中心设置: Alan Liang, Brady Catherman, Chris Goffinet, Dmitriy Ryaboy, Gilad Mishne, John Corwin, John Sirois, Jonathan Boulle, Jonathan Reichhold, Marius Eriksen, Nick Kallen, Ryan King, Samuel Luckenbill, Steve Jiang, Stu Hood and Travis Crawford。我们也要感谢整个 Twitter 搜索团队提供的宝贵的设计反馈和支持。如果你也想参与这样的项目,和我们一起飞吧!

原文地址:http://engineering.twitter.com/2011/11/spiderduck-twitters-real-time-url.htmlhttp://engineering.twitter.com/2011/11/spiderduck-twitters-real-time-url.html

2015.10.27补充: - 爬虫技术浅析

苹果“查找我的iphone”功能的地图纠偏问题

今天无意研究了下苹果的“查找我的iphone”功能,发现网页中的地图跟手机实际位置偏差很大,因为之前研究过国内地图的纠偏问题,所以本能就想到是不是纠偏造成的。

于是将地图位置(国内只能显示平面地图,不能显示卫星图,原因你懂的)放在google地图上,通过maps.google.com 中的平面图和卫星图拟合,发现拟合后的微信图的位置跟实际位置相同,跟手机自己显示的位置一样,就此确认。

简单的解决方法:vpn后,就能显示正确位置了,原因就不多说了,自己google吧。