丰臣秀吉

加藤广 著
曹宇 译
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生成所有文章,发现所有文章的长度都正确了,甚是高兴。

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

go编译中出现的link signal killed问题

在vps中编译hugo代码时出现:
tool/linux_amd64/link: signal: killed

经过搜索查到类似的场景:
https://github.com/beego/wetalk/issues/32

不同的是这个是beego,而我编译的是hugo,所以初步断定应该是跟go的环境有关,而不是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中应避免的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钩子接口实现新增修改删除文章后执行相应动作

wordpress hook介绍

hook是wordpress实现功能扩展的重要手段,详细的介绍:
Plugin API

hook主要分两类:actions 和filters。
对应的注册函数分别为:add_action()和add_filter()。

目前支持的hook类型参见:http://adambrown.info/p/wp_hooks

例子

在模板的基础函数文件中:
/data/wp/wordpress/wp-content/themes/premium-style/functions.php

新增以下代码:

function publish_post_extra($post_ID){
    // 1. 初始化
    $ch = curl_init();
    // 2. 设置选项,包括URL
    curl_setopt($ch, CURLOPT_URL, "https://xxx" . $post_ID);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    // 3. 执行
    curl_exec($ch);
}
add_action('publish_post', 'publish_post_extra');
add_action('deleted_post', 'publish_post_extra');

以上的代码实现动作发生时,发送一个特定的通知url。

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插件,用户加载时动态触发检测,不过这个只是计划,目前本人能时间上和能力还做不到。

最后

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