拆解国产 Phylian H8 电动牙刷,并估算物料成本

前段时间在亚马逊上买了一个中国品牌的Phylian H8型电动牙刷,约25加币,用了2个月后,马达故障,停止工作。向厂家保修后,厂家寄回一套全新的,于是我闲着无聊,拆解了一下。

先说结论:这玩意的物料价格低到离谱,20元人民币都富余。

首先是外观。一个白色的刷头插座,下面是黑色主机,有一个大按钮,和几个LED指示灯。和博朗等品牌的金属震动插座不同,Phylian是塑料的,而且很粗,我之前不明白为什么这么设计,但下面会解释。

拆解思路很简单,把牙刷底部封口四周用工具刀划一遍、破坏掉防水胶,然后取下封口。

此时用改锥稍微一撬,就能拔出这个塑料封口。

封口下面有两根电线连着内部电路,因为两边有两个塑料卡扣,必须进行破坏性拆除,否则无法取出。

将尖嘴钳将卡扣掰弯,见上图红圈处,即可顺利取出内部组件。

取出后,可见到牙刷主要分为三个部分,上部的白色刷头插座,中部的牙刷主板、电池,下部的充电电路(也就是刚才拆了半天的塑料底座)。

电路部分很有趣,直接就是个14500电池,500mAh,这块电池应该是低端电动牙刷的标配了吧?取下来也很容易,和5号电池差不多大,用手一扣就下来。

拆掉14500电池以后,我想起来自己还曾把旧牙刷快递给第三方维修,花费上百元人民币只为了换个电池,突然感到损失一个亿。

接下来是牙刷主板部分。

从上到下,分别是开关按钮,5个模式切换LED,以及一个充电指示灯。模式切换LED附近,覆盖了一层黑色海绵,如下图。

这层海绵的作用,就是为了防止LED之间「串色」,例如第一个LED亮起时产生的光晕,让第二个灯看起来也亮了。粗糙的产品一般不加这玩意,对产品核心功能没有任何影响,但如果对用户体验有点追求,还是会对这种隐形Bug做一些防护。此处应该给好评。

我的牙刷故障原因大概率是马达出了问题,但我拆了半天,并未找到马达在哪,所以继续拆解上面的白色刷头插座。

用胶封住了!显然里面藏着什么电子原件,那肯定要破拆!

终于在这里发现了震动马达!原来把刷头插座做这么大,是为了直接把马达塞进去。而博朗、飞利浦等,则统一将马达放在主机里,看来这是两种基于不同成本的差异很大的设计。

零售价人民币1.8元。难怪这么容易坏,草。

最后来个全家福。充电电路实在懒得拆了,也没什么新鲜的,就到这里了。

估算一下这个零售价25加币、约合人民币130元的电动牙刷的物料成本:电池6元,马达1.8元,估计整个电路板不会超过5元,外壳10元。不包含研发、开模、营销、运营等费用,单纯物料(按零售价计算)只有不到20元人民币。以后兄弟们如果电动牙刷不好使了,直接拆开底部、掏出来换个电池就行了,耗时10分钟。

Tagged : /

字节跳动和 Go 语言

字节跳动

2022年1月5日,我正式入职字节跳动,高级测试工程师,负责一个字节非常底层的网络库的质量保障工作。这次找工作没有选择开发岗位的一个原因是,开发工程师对于整个工程的控制力是比较弱的,一般来说基本相当于一个纯「产出单元」,没有宏观调度的能力与意愿。我想要做更多宏观的工作、加强自己对于工程的把握感,就只有QA、项目经理、产品经理这几个岗位,后两个岗位对我吸引力很低,QA 则刚好和我的个人履历与兴趣相结合。

绝大部分大厂对于 QA 的运用,基本上还是作为测试部门,以调研各种测试流程和技术为主,为「产出单元」们提供后勤保障工作。换言之,说是 QA,其实就是测试+一些 bug 数据统计+打包发版。

当然,并不是说,QA 应该彻底脱离测试、成为一个飘浮在半空中的岗位。但如果 QA 部门自己都不能深刻的理解「何为质量」这个命题,如何能带领整个团队进入下一个质量高潮呢?

以手工回归测试来说,这项工作用来了解产品特性还凑合,但了解产品最好的方式不是使用产品,而是开发产品——买的永远没有卖的精明。但业内的 QA 或测试工程师对产品源码一般来说没什么兴趣,精力都放在测试流程和技术上。国内高强度的工作方式,的确会抑制工程师们的想象力。

在和开发工程师的一次午餐中,我向大家阐述了自己的职业目标,以及我对「质量」二字的理解:

我个人永恒的职业目标,就是把自己当下的岗位「干没」。「干没」,意味着永久性的、终极的解决了这个岗位面临的一切问题——如果我是一个开发,那么我的目标就是减少掉自己这个开发;如果我是测试,就要让团队不再需要测试;如果我是 QA,就要让开发过程不再需要 QA 的校准;如果我是团队 leader,就要让团队不需要我这个 leader 也同样能高效运转。

这是非常终极的目标,有可能永远不可能达到,但应该有这样的追求。作为工匠,不必担心自己未来会失业,如果一个职场人能达到以上高度,那么一定有更重要的岗位等着你。

我对质量的理解,则是——「质量」不应该成为一个岗位,而是一种思想、一种意识形态。因为质量诞生于开发部门,而 QA 永远只能「揭示」质量、而非产出质量。既然「质量」是开发工程师的产出,那么应该让开发工程师来负责质量,才是最正确的事情。就像打铁铸剑,一个产品在诞生之时,其品质已经确定,后期再修补也无济于事。

在传统的「开发-测试-发版」流程中,如果测试发现质量有问题,就应该将之返回给开发团队,由开发团队去内部处理一切有关「质量」的问题。试想一下,这个世界上存在与质量相关、却不应该由开发亲自处理的事情吗?

换言之,「质量」是一个开发问题,而不是测试或 QA 问题。

那么 QA 这个岗位,还有存在的意义吗?

QA 仅存的意义,我认为是一个类似战锤40K中的「牧师」角色。

这就是战锤40K中的牧师,勇武更超普通星际战士

在战锤40K的作战小队中,队长负责战术指挥,牧师负责思想和信仰工作。当士气低落、信仰垂危时,牧师要首当其冲的站出来,挽狂澜于既倒,扶大厦于将倾,即便为了一线胜利而牺牲自己,也在所不惜。

这个牧师不是躲在堡垒中对战士们指指点点的文官,他本身就是一个和兄弟们冲锋在前的顶尖士兵。只是相比于其他人,他的信仰更坚定更执着,也更有大局观、更优秀。

战锤40K 的牧师,实际上就类似项目组中的 QA 角色,这样的 QA 拥有如下特点:

  1. 战斗于一线,而非文官(本身就是开发,而不是开发团队之外的「指导者」)
  2. 更具宏观思维,能超脱于实际战斗以外(视野超出普通开发,更关注整个团队的产出质量)
  3. 来自于团队内部,而非外部(从开发中遴选一个开发人员成为QA,而非在团队外部设置QA岗位)

在这几个原则的规范下,我们很容易确定团队中谁来成为 QA,同时将团队外部的 QA 岗位直接取消。

目前微软已经取消了测试和 QA 岗位,全部测试工作都由开发工程师来做。这种意义上,字节还相当于普通星界军、而微软已经人均星际战士了。

Go语言

早在几年前,我就开始关注 Go 语言,只是耽于舒适区,一直没有向前一步。最近做了下调研,发现除了 Shopee 之外,字节也大量使用 Go 作为后端开发语言;另外作为区块链开发的重要技术栈,Go 语言未来也可能越来越重要。

所以从本周开始,我利用工作和业余的一些零散时间,开始逐渐学习 Go 语法,了解生态现状。

和我最熟悉(应该是唯一熟悉)的语言 Python 相比,Go 的生态要小很多,但是他们的特点都非常显著:Python 生态强大,几乎可以做一切事情,代码编写灵活;Go 的目标就是一个轻量版 C 语言,在「灵活快捷」和「高性能」之间尽量寻找平衡点。

中国互联网企业,实在是没什么技术含量,如果硬说,那就剩下一个「高并发」独步全球。在解决这种并发问题上,Go 有天然优势,goroutine 的内存占用非常小,同时通过 Channel 进行安全的数据处理,避免了 Python 那种一团乱麻式的异步和锁。所以新兴互联网企业喜欢用 Go 来构造后端,是非常理性的。

Python 是当之无愧的王者,但其中势必包含大量的非专业工程师 Python 开发者(例如科学家,量化交易员,等等)

区块链同样面临性能问题,传统比特币、以太坊性能都令人无法接受,所有新型公链一定会号称解决了性能问题。毕竟,以「代币」为核心机制的新型互联网,如果不能解决性能问题,则毫无未来。Go 语言在这个领域可能是最优选择。未来如果有机会进入这个行业,Go 肯定是必修课。

Go 位列13名,已经是非常好的成绩,仅次于 SQL/Swift/PHP/R。

以目前的发展趋势来看,Go 断然不会成为 Python 这样的全能语言,但其后端领域的影响力很可能会在未来几年达到甚至超过 Python。所以对于有 Python 基础的人来说,应该尽快学会 Go 并掌握开发技巧,不仅会带来收入上的提升,还能更好的了解到技术界的新趋势。

所以,这篇文章也算是一个节点,看看我下一次更新博客的时候,能用 Go 写出什么样的东西来了。

祝我的读者们新年快乐,虎虎生威(下面弄两个小老虎凑合凑合吧)!

虎虎生威,新年快乐!

Tagged : / / / / /

成长,就是持续创造价值

我时不时会收到一些小朋友们的私信,问是否招实习生或者「程序员学徒」的,可以不要薪水。我一般回复「没有」,就不再多说什么了。并不是我高冷,而是觉得这样的小朋友恐怕并没有什么潜力值得去挖掘。
这么冷酷的结论,并不是拍脑门想出来的。经历了多次打脸,我最终明白一个道理——如果一个人把「廉价」甚至「不要工资」作为核心竞争力,那么他不仅对自己没有信心,其实也不会有任何主动性去发展自身。他们只期待一个武林高手从一招一式开始教起,像妈妈带娃一样照顾始终,而自己只需负责饭来张口即可,希望在人畜无害的气氛中,无能的成长起来。
显然这是做梦。即便这样一位「程序员学徒」不要工资,其他人为了培养他而投入的巨大精力和时间难道不值钱?武林高手有这个时间干点什么不好,不能去争一争武林至尊吗?为什么要把时间浪费在一个连学习知识技能都需要他人喂食的巨婴身上呢?这些幻想成为「程序员学徒」的小朋友明显算错了数,他们以为「不要工资」是自己的巨大优势,实际上这样的人带来的往往是负价值,对团队和项目都有巨大损害。
举一个我自己的例子。我曾经在一个水平相当低的工程师身上耗费了近半年时间,从 Google 搜索引擎和 stackoverflow 的用法,到代码规范,到 Python 各个标准库的用法,再到各类 Pythonic 代码的写法。之所以耗费这么大精力,是因为我是一个不错的老师,能把很多道理讲的深入浅出(例如我可以在 1 分钟时间里让任意一个无技术背景的人理解金融量化交易引擎的技术原理),经过我的细致培训,他一定有机会成为一个代码细腻而简洁的工程师。
直到被我劝退前,他都一直在用百度搜中文关键词并跳转到 CSDN 上看技术文。
这是什么概念呢?就是一个数学老师教了你半年的二项式展开,最终面对最简单的二项式题,你依然在掰手指数数。
这样的程序员带给同事的往往是噩梦。在他离职之后,我们依然时不时的从他代码中发现骇人的 bug,然而数据已经被污染,重新修正几乎不可能。由他引入的问题,很有可能直到公司破产都不会有人愿意去解决。这样巨大的负担,当年的我如果更成熟一点,是万万不会惹祸上身的。所以现在收到各类的「不要工资」的申请,心里都会默默的想,「你当我是傻子么」。
每个人都渴望成长,但成长不是商品,不是你用「不要工资」就能够换回来的。成长源于你一步一个脚印的学习,在学习过程中,你又创造出了价值。本质上,只有当你持续创造价值的时候,你才会成长;而不是反过来。
如果你技术不好、没公司要你,能否在力所能及的范围内,对开源社区做一点贡献?
如果你技术不好,能否仔细研究业务代码,试着花一个月甚至两个月的时间写一个简单但核心的模块?
如果你技术不好、又想找个老师,能否先帮这个老师做一点事情、哪怕只是帮他写单元测试?
……
可做的事情太多了,简直数都数不过来,而它们的共同点就是——持续创造价值,而不是持续索取关照。
所以,当我们对一件事情有了强烈的渴求时,最好先问问自己,能不能用已有的技能先创造一些价值出来。如果不能,说明你其实并没有那么渴求。那么,请不要再去骚扰那些工作量已经很饱和的工程师了,码农滞销,可怜可怜老乡们吧。

Tagged : / / /

给马蜂窝支招

互联网运营中,存在一种非常隐晦却不可不谈的手段,谓之「虚假运营」。虚假运营指用假数据,给用户或者竞品造成某种错觉,从而达到运营目标。从马化腾在 OICQ 上装女生陪用户聊天,到 Airbnb 将 Craiglist 的数据塞到自家网站上,再到新手玩吃鸡手游会匹配到人机、更容易获得胜利,都体现了这是一种隐匿而重要的互联网运营手法。
然而运营者必须对数据负责,对结果负责,对用户和产品负责。这次马蜂窝被爆 UGC 数据造假,其曝光方式异常简单,甚至无需专业人士出马,普通用户稍加观察,都能轻易发现端倪,体现出其运营团队低下的运营能力。
换句话说,造假都造不好,还能指望它「造真」么?
针对马蜂窝拙劣的运营手法,我提供一些技术方案,用开发手段解决运营问题,估算一下工程量:一个后端开发工程师 + 2 个运营专员,可以在一个月时间内搞定开发、测试、上线。如实在无能为力,也可请我作咨询,连开发带运营,我一个人就够了,给你们省点钱。
长话短说,解决马蜂窝的虚假数据问题,可分五步走:
  1. 对马甲和内容数据分类、打标签
  2. 对「有毒内容」进行过滤
  3. 调研真实用户行为特征
  4. 开发自动化内容发布系统
  5. 优化运营人力
 

一、对马甲和内容数据分类、打标签

在今天曝光的蚂蜂窝数据造假证据中,马甲人设来回变换、点评内容忽男忽女,即便是普通用户,只要稍微细心,都可以发现其造假实锤(例如刚和老婆在上海喝完咖啡,就带老公去深圳吃面)。
首先,批量生成小号,并对其打标签(如 25 岁白领单身女性,32 岁企业管理层已婚男性,19 岁广东省大学女生等等),其属性划分应该符合网站真实用户的正态分布(例如 80 岁老爷爷应该不太会是蚂蜂窝的主力用户吧)。
其次,对爬来的点评数据进行类似处理,按作者性别、年龄、婚姻状态等进行标签归类,便于未来发布内容时,「女性大学生」人设的账号不至于发布出「携老婆和丈母娘旅行」的内容。其中,最重要的属性是性别,在爬取数据应顺便解决性别数据,搞定性别,大约 90% 的点评数据不会出现严重错位。
这个步骤的意义在于,将马甲账号与点评内容关联起来,营造出真实用户的氛围。当马甲账号发布消息时,可通过相应的匹配规则获取符合其人物设定的内容,被现场抓包的问题基本可以解决。
 

二、对「有毒内容」进行过滤

把爬取到的评论中附带的竞品名字(大众点评/美团/携程等),按照渠道来源,直接替换成自己的产品名,(例如从大众点评爬来的数据,统一把「大众」「大众点评」「点评网」等关键词替换为「马蜂窝」),虽然有可能产生内容不精确问题,但是远好于被读者及同行抓现行。
同理,爬虫在爬取数据时被对方以「插入推广链接」等方式「反爬」、导致存在部分脏数据,应及时清理。
运营人员还需要定期抽样检查数据,对存在的问题进行跟踪,发现问题后可以制定数据修改规则,使用脚本对数据进行批量修改,定期对规则进行迭代。
 

三、调研真实用户行为特征

暂停所有马甲账号发布,对真实用户进行为期一个月的行为观测,统计网站用户的真实活跃时间、频率和点评数量。
 

四、开发自动化内容发布系统

按照已被揭露的马蜂窝虚假内容发布时间来看,大概率是运营团队花费了大量人力在其工作时间段内发布,而这毫无必要。后端提供写入接口,并使用脚本或服务定期定量的写入数据,可以将大量人力工作直接转化为自动化任务。
后端开发工程师应该为运营部门提供两个组件:按标签获取点评内容的消息队列,以及写入内容的接口。然后设置定时任务进行批量写入操作,时间规则参考上一条用户真实行为特征,且写入时间可随机做小幅波动,尽量贴近真实用户行为。
 

五、优化运营人力

通过上述方法解放出来的运营人力,应该全力负责下述方面:
1. 如何用更好的方法将马甲账号与爬取到的内容匹配起来?
2. 怎样持续优化点评内容质量、以及迭代检验内容合格与否的标准?
3. 持续观察真实用户行为,要对自己的产品和用户有更更更更深入的理解。
4. 与工程团队对接,寻找优化这套「自动化发布系统」的方法,应有运营专员负责该系统的需求与迭代。
 
做到以上几个方面,我相信很少有用户用户或机构可以再通过运营数据观测到虚假运营。因为虚假运营的本质就是模拟用户真实行为,面面俱到,则可最大限度减少破绽。当然虚假运营的手段多处于灰色地带,而一家正经企业也不能一辈子鸡鸣狗盗,其最终目标依然是获取更多「真实用户」并提高其活跃度和消费额,将「假数据」转变为「真数据」。广大互联网运营人士,应以蚂蜂窝为鉴,时刻提醒自己,在实战中要更专业、更专注。
Tagged : / / / /

程序员年龄增大后的职业出路是什么?

这篇文章是我在知乎上的的一个回答,讲的略微有点深。另外感觉自己这两年越来越左翼,越来越觉得马克思不愧是20世纪最伟大的思想家之一。

今年 30 了,也开始考虑年龄问题了。
不过虽然 30 岁,但我实际作为程序员、以编程为业,也只是最近两年的事情(关于我的编程职业经历,在这个回答里写的比较详细:知乎用户:自学编程的人,都是怎么找到第一份软件开发工作的?)。因此对编程这个职业的理解,可能还不如一些 90 后小朋友深,写的不对的地方贻笑大方,请见谅了。

注:以下叙述,不局限于程序员,实际上包含所有职业。

中国的程序员在市场中的地位,不客气的说,随便翻开《1844经济学哲学手稿》里描写产业工人的段落,把这些一百年前的论断放到程序员身上,都言之凿凿:
例如:关于程序员的先进生产力与其对应的可怜回报

「的确,今天由于有了新的动力和完善的机器,棉纺织场的一个工人往往可以完成早先 100 甚至 250-350 个工人的工作。在一切生产部门中都有类似的结果,因为外部自然力日益被用来加入〔X〕人类劳动。如果说为了满足一定量的物质需要必须耗费的时间和人力比现在比过去减少了一半,那末,与此同时,在不损害物质福利的情况下,给精神创造和精神享受提供的余暇也就增加一臂。但是,在我们甚至从老克伦纳士自己领域中夺得的虏获物的分配方面,仍然取决于像掷骰子那样盲目的、不公正的偶然性。法国人有计算过,在目前生产状况下,每个有劳动能力的人平均每日劳动五小时,就足以满足社会的一切物质利益……尽管因机器改进而节省了时间,工厂中奴隶劳动的时间对多数居民说来却有增无已。」

例如:关于「越工作越贫穷」这一现象的分析

「工人生产的财富越多,他的产品力量和数量越大,他就越贫穷。工人创造的产品越多,他就变成廉价的商品。物的世界的增值同人的世界的贬值成正比.劳动不仅生产商品,它还生产作为商品的劳动自身和工人,而且是按它一般生产商品的比例生产的。」

例如:关于程序员异化为程序的奴隶

「因此,工人在这两方面成为自己的对象的奴隶:首先,他得到劳动的对象,也就是得到工作:其此,他得到生存资料。因而,他首先是作为工人,其次作为肉体的主体,才能够生存。这种奴隶状态的顶点就是:他只有作为工人才能维持作为肉体的主体的生存,并且只有作为肉体的主体才能是工人。」

以上,并不是呼吁大家都去读马克思,我也无意挑起关于马克思的政治经济学的争论(也希望大家尊重题主和读者们,不要做跑题辩论),只是希望各位静下心来想一想,我们和百多年前的纺织工人的区别,到底是什么?
伴随一个产业工人或程序员的老去,不可避免的是工作时长与强度的降低、工资的刚性提升(正常来说只能升不能降)、对家庭福利需求的增高(例如需要公司为家人购买保险)等等。而普通工人(程序员)的工作效率增长速度一旦无法达到其福利需求的增长速度,在公司里即成为累赘。换句话说,辞退你、换新人,对公司有很大好处,而这对资方来说是一个巨大的诱惑。
多亏劳动法和近二十年来的互联网产业红利,程序员的处境已经远远优于当年的工人,不用时刻担心被辞退,但是当到达了一定年龄后,大多数程序员的个人价值已到峰值,收入封顶,但生活压力却越来越重。这是「年龄-收入」的悲剧冲突核心,也是大量程序员夙夜忧叹的唯一原因。
而这种悲剧,集中式的出现在中国,其直接表现就是程序员的薪酬过低(例如在美国,普通工程师拿个十几万刀的 package 并不算天方夜谭,这种问题也就并不严重。)。
所以程序员老了怎么办?唯一靠谱的解答,就是脱离自己的产业工人身份,直面残酷的市场食物链,尽一切可能向上爬,脱离这个「产业工人诅咒」。
那么程序员以上的食物链有哪些?最直接的就是技术管理层、公司中高层,再次是资本方,最后是权力方。故而其方法也分外直接:要么争做管理层并获取股份、股权激励等非劳动收入,要么以资本进行投资、让别人为你打工,要么当个省委副书记、起码乡镇干部之类(这一条过于奇幻,当做笑话吧)。
但凡符合上述逻辑的职业规划,都是靠谱的,区别仅在于风险大小。
至于计算机领域的、乃至其他各行各业的技术专家,这是一类我非常崇敬和羡慕的人群,他们往往已经获得了超额收益,故而不在我上述论证范围内。我所说的一切,仅仅针对你我这样的普通职员。
所以,程序员们,不要为自己掌握先进的知识而沾沾自喜,这个世界并不属于知识劳动者,所有利润最终将归属资本和权力……
……和猫。

Tagged : / / /

树莓派3初体验之一:搭建 Python 开发环境

最近新买了一个树莓派3(购买前还考虑了 Orange Pi 等 Linux 开发板,但最终仍然选择了树莓派),主要目标是想通过折腾这个信用卡大小的 Linux 电脑,学习一下 Linux 系统知识,熟悉服务器的各项操作,顺便给自己搭建个 Web Server。
WechatIMG17
安装步骤很顺利,通过官网下载了 NOOBS 版的 Raspbian 系统,并复制到 TF 卡中,然后启动树莓派,一路 Next,很快就可以看到图形界面。然后看了一下树莓派内置的 Python 版本,发现是 2.7.9 以及 3.4.2,于是机智的我马上制定了第一个任务——把树莓派的系统自带 Python 3.4 升级到最新的 Python 3.6.2。
升级的过程是这样的:
1. 从 Python 官网下载 Python 3.6.2 的压缩包。
wget https://www.python.org/ftp/python/3.6.2/Python-3.6.2.tgz
2. 解压编译
cd ./Python-3.6.2
./configure
make
sudo make install

3. 将 /usr/bin/python 的原文件删除,然后link到刚编译好的Python3命令上
rm /usr/bin/python
ln -s /usr/bin/python ~/Python-3.6.2/python

结果是虽然我把原本是2.7.9版本的python命令变成了3.6.2,但是配套的pip等工具却乱套了,一运行就会报语法错误,看来这样生变过去是不行的,于是想恢复回去,几经尝试后失败,遂格式化TF卡,重装系统。
第二次我不再 link /usr/bin/python, 而是机智的换成了/usr/bin/python3,但是依然出错,具体什么错误已经不记得了。再次格盘重装。
第三次终于成功,在命令行输入python3指向了/usr/local/bin/python3,我还没搞明白是怎么弄得,反正搞定了,明天去问问同事在哪里设置……

后来才想起来,要改环境变量里面的$PATH……这是刚学 Python 时候就遇到的问题,现在居然都想不起来了……

WechatIMG16
此时又遇到了第二个坑——Rsapbian没有安装若干必要的库,需要手动安装。
顺利配置好 Python3.6 后,我试着pip3 install requests,结果出现ssl module in Python is not available的报错,几经搜寻,发现是由于 OpenSSL 等库没有预置在 Raspbian 中,而 pip 则需要访问 https 加密的地址,故而报错。
首先,apt-get 安装这些库:
sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev
然后重新编译一次 Python 即可解决问题:
cd ~/Python-3.6.2
./configure
make
sudo make install

于是经过不知多少次编译,我终于装好了 Python3.6。后来又安装了 PyCharm,但实际运行中感觉延迟严重,所以这样大型的 IDE 可能无法在树莓派上顺畅运行,如果需要写代码,可以考虑轻量级编辑器(同时还有一个问题没有解决,就是如何卸载 PyCharm ……)。
最后说一下树莓派的整体感觉。
开箱之后的树莓派需要自己安装散热片、风扇、外壳,当然这些配件都需要另外购买。组装过程很简单,把散热片粘到处理器上,然后一层一层安装外壳,最后在外壳上方固定和连接风扇。如果有过装机经验,那么整个过程即为简单。组装完成后,我通过一根 HDMI 转 VGA 线,将树莓派连接至一个古董19寸显示器。
我(也是大多数树莓派素人)选择的是官方推荐的 Raspbian 系统,是 Debian 的树莓派分支。其实树莓派有大量的系统版本可以选择,例如 CentOS 的 ARM 版、Ubuntu Mate等等,可按需选择。Raspbian 的 GUI 没有 Ubuntu 那么炫酷,但是能在这么小的板子上运行如此完整而流畅的桌面版 Linux,也让人很欣喜。
目前我已经把若干连接线(视频线、鼠标、键盘)从树莓派上移除,仅剩供电线路并连上 WiFi,通过 Mac 远程登录进行管理。本来想设置一下 DNS 实现通过 hostname 直接访问主机,结果家里没有多余的机器做 DNS Server,华硕路由器的官方系统又不原生支持 DNS、我还懒得再折腾路由器,所以只是通过改本地的 hosts 来实现 hostsname 登录(我给树莓派起的名字叫做 footboy)。以后可能会在树莓派上搭建几个服务,通过 supervisor 管理;或者做几个 Cronjob 的定时任务。
WechatIMG15

Tagged : / / / /

关于开发流程的一些体会

工作近5周,共完成了2个项目(第二个项目已经基本完成测试,准备收尾中)。第一个项目,是用爬虫抓取数据,然后做好 API 供用户使用。第二个项目,是扫描僵尸用户,发邮件唤醒,如未唤醒则关停僵尸用户的进程。
做这两个项目的过程中各有各的体会。
首先,是关于解决问题的方式。做第一个项目时,刚刚进入公司,很难适应工程师文化,加上和同事不熟悉,脸皮薄,不愿意问问题,甚至没有仔细确认需求和工程方式就动手开搞,最终耽误了不少时间。在工程上,沟通极为重要,工程师不是低着头闭门造车的,恰恰相反,工程师们是用集体合作的方式共同搭建一个或繁或简的系统,最终完成个体无法完成的大规模工程。这应该是工程师们在一起工作的最重要意义之一吧。
工程被分割成一张张工单,并不意味着其整体被切割成无关联的个体。在完成工作的时候需要尽可能多的理解工程的有机性。例如我将爬下来的时间序列存入数据库时,每条数据的时间都被我存成了str,于是在后期制作 API 的时候就遇到了一些问题,最后需要通过将其转成datetime来解决。类似的问题说明我在完成某件工作时,并不知道这件工作在工程中的位置与意义,这就需要多问和多做了,在对整体熟悉了以后,自然会逐渐清晰。
其次,是关于单元测试的重要性。其实『单元测试』这四个字,有弱化其意义的副作用。我在做第二个项目的时候,由于内部处理数据的逻辑比较复杂,导致大大小小的 Bug 一大堆,每次提交都认为自己已经做得差不多了,但还是在 code review 时被打脸,来来回回提交了若干次,花费大量时间,甚至同事成了我的人肉 Debug 处理器。最后同事说,你还是写一些测试用例吧,覆盖的情况越多越好。
果然,写了一百多行的测试用例后,将单元测试完成,自然发现了一些之前很难发现的 Bug,无论对我还是其他工程师来说,都节省了大量调试改错的时间,多灾多难的第二个任务也随之迎刃而解。经此一役,我对单元测试的意义恍然大悟,其实单元测试并不仅仅是『测试』这么简单。在『测试』的背后,其实是将代码化整为零、各个击破的写作过程,因此单元测试的写作时间,应当与程序本体同步,也即『写一个函数,就写一个测试』,二者几乎是同步完成的。这样看上去花了很多时间在设计测试用例上,其实是规范了自己的思路和代码,同时大大提高了后期的可维护性。
在明白了第二点(也就是单元测试的真正意义后),觉得自己仿佛突破了一个模糊边界——一个软件工程师和业余代码爱好者的界限。虽然现在代码依然很烂,但是本着对代码负责的态度和对自己职业的尊重,我会把以后写下来的所有代码都同步加上单元测试,配得上一个专业人士应有的严谨。
以上就是我在完成了两个项目后的些微体会。工程师的快乐,是由一个个微小的痛苦组成的。此生竟有幸成为工程师,真是一件幸福的事情。

Tagged : / /

Python 中 list 的传值问题

我在做一个小程序,需要生成一个随机数列表,然后将之赋值给2个 list,并需要这两个列表完全独立(即列表值指向不同的内存地址)。最开始是这样的:

>>> a = [5,4,3,2,1]
>>> b = a
>>> print(a,'-',id(a)) # id是 python 查看对象地址的方法
[5, 4, 3, 2, 1] - 4516275720
>>> print(b,'-',id(b))
[5, 4, 3, 2, 1] - 4516275720 # 显然,此时a和b指向同一个地址
>>> b.sort() # sort()是 python 的内置排序函数
>>> print(b,'-',id(b))
[1, 2, 3, 4, 5] - 4516275720
>>> print(a,'-',id(a))
[1, 2, 3, 4, 5] - 4516275720 # 我去,怎么回事???a 怎么也变了?

Python 的变量赋值机制并不是真的给变量赋值(实际上从头到尾都不存在 list 的容器),只是把变量名当做一个标签,贴在内存地址上,当值发生变化的时候,并不是改变值本身,而是将内存地址对应的标签『唰』的一下撕下来,然后贴到另一个地址上——此所谓『铁打的内存,流水的变量名』。
对于复制 list,按照官方文档的说法,应该这样操作:

b = a[:]
# https://docs.python.org/3/faq/programming.html#how-do-i-copy-an-object-in-python

此时是这样的:

>>> a = [5,4,3,2,1]
>>> b = a[:]
>>> print(a,'-',id(a))
[5, 4, 3, 2, 1] - 4516276488
>>> print(b,'-',id(b))
[5, 4, 3, 2, 1] - 4516276232

可见在 b = a[:] 这一过程中,Python 在内存中创建了一个新值,并将 b 的标签贴到了上面,迥异于我们最开始的过程。
其实上述过程并非发生在所有数据类型中。

>>> A = 5
>>> B = A
>>> id(A)
4540535120
>>> id(B)
4540535120
>>> B = 3
>>> A
5
>>> id(A)
4540535120
>>> id(B)
4540535056

关于这个问题,我还没有研究明白,还没搞懂到底哪些变量是这样传值的,算是一个小小的坑吧。
参考资料:

  1. http://stackoverflow.com/questions/8744113/python-list-by-value-not-by-reference
  2. https://docs.python.org/3/faq/programming.html#how-do-i-copy-an-object-in-python
Tagged : /

几种排序算法的比较

整个十一假期就在折腾这几个算法,这篇总结性文章就是简要的介绍了几个基础算法的特性,并附带了 Python 的实现。
不同算法适合不同情况的数组,但在不知道输入规律的时候,使用时间复杂度低算法的比较保险。
Big O:上界
Big Ω:下界
Big Θ:确界

Big O 比较

big_o_complexity
O(n²):冒泡排序(稳定)/选择排序(不稳定)/插入排序(稳定)
O(nlgn)~O(n²):希尔排序(不稳定)
O(nlgn):堆排序(不稳定)/归并排序(稳定)/快速排序(不稳定)

Bubble Sort 冒泡排序

def bubble_sort(arry):
    n = len(arry)                   #获得数组的长度
    for i in range(n):
        for j in range(1,n-i):
            if  arry[j-1] > arry[j] :       #如果前者比后者大
                arry[j-1],arry[j] = arry[j],arry[j-1]      #则交换两者
    return arry

时间复杂度、稳定性

平均情况:O(n²)
最坏情况:O(n²)
最好情况:O(n)
稳定
辅助空间O(1)

特点

最优情况为全部正序时经过 n-1 次比较即可完成排序,最差情况是倒序。因此冒泡算法对数组的有序性很敏感,适合对规模较小、且比较有序的数据进行排序。[1]

优化方案

  1. 如果某一次遍历没有发生数据交换,则代表已完成排序,可停止迭代。
  2. 记录遍历时最后一次数据交换的位置,后面的数据已经有序,因此可以缩小下次循环的范围。

Selection Sort 选择排序

def select_sort(ary):
    n = len(ary)
    for i in range(0,n):
        min = i                             #最小元素下标标记
        for j in range(i+1,n):
            if ary[j] < ary[min] :
                min = j                     #找到最小值的下标
        ary[min],ary[i] = ary[i],ary[min]   #交换两者
    return ary

时间复杂度、稳定性

平均情况:O(n²)
最坏情况:O(n²)
最好情况:O(n²)
不稳定
辅助空间O(1)

特点

运行时间与输入状态无关(随机排列与正序排列消耗相同的时间)。同时移动次数最少(与数组大小呈线性关系)。[1]

Insertion Sort 插入排序

def insert_sort(ary):
    n = len(ary)
    for i in range(1,n):
        if ary[i] < ary[i-1]:
            temp = ary[i]
            index = i           #待插入的下标
            for j in range(i-1,-1,-1):  #从i-1 循环到 0 (包括0)
                if ary[j] > temp :
                    ary[j+1] = ary[j]
                    index = j   #记录待插入下标
                else :
                    break
            ary[index] = temp
    return ary

时间复杂度、稳定性

平均情况:O(n²)
最坏情况:O(n²)
最好情况:O(n)
稳定
辅助空间:O(1)

特点

输入状态会影响运行效率,对有序数组排序要比对随机排列、逆序排列的数组快,因此适用于部分有序的非随机数组。当『数组中每个元素距离它的最终位置都不愿』『一个有序的大数组接一个小数组』『数组中只有几个元素的位置不正确』时,插入算法的速度很可能比其他算法都快。[2]

Shell Sort 希尔排序

def shell_sort(ary):
    n = len(ary)
    gap = round(n/2)       #初始步长 , 用round四舍五入取整
    while gap > 0 :
        for i in range(gap,n):        #每一列进行插入排序 , 从gap 到 n-1
            temp = ary[i]
            j = i
            while ( j >= gap and ary[j-gap] > temp ):    #插入排序
                ary[j] = ary[j-gap]
                j = j - gap
            ary[j] = temp
        gap = round(gap/2)                     #重新设置步长
    return ary

时间复杂度、稳定性

平均情况:O(nlgn)~O(n²)
最坏情况:O(n²)
最好情况:O(n1.3)
不稳定
辅助空间:O(1)

特点

希尔排序将数组分成较短的子数组、并使之部分有序,很适合插入排序。希尔排序对中等大小数组的排序时间可以接受,代码量小,且不需要额外内存空间。当没有系统排序函数可用时,可以考虑先用希尔排序,再考虑是否值得替换为更复杂的排序算法。[3]

Merge Sort 归并排序

def merge_sort(ary):
    if len(ary) <= 1 : return ary
    num = int(len(ary)/2)       #二分分解
    left = merge_sort(ary[:num])
    right = merge_sort(ary[num:])
    return merge(left,right)    #合并数组
def merge(left,right):
    '''合并操作,
    将两个有序数组left[]和right[]合并成一个大的有序数组'''
    l,r = 0,0           #left与right数组的下标指针
    result = []
    while l<len(left) and r<len(right) :
        if left[l] < right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    result += left[l:]
    result += right[r:]
    return result

时间复杂度、稳定性

平均情况:O(nlgn)
最坏情况:O(nlgn)
最好情况:O(nlgn)
稳定
辅助空间:O(n)

特点

归并排序在最坏的情况下复杂度为O(nlgn),和其他基于比较的排序算法所需的最小比较次数相同。

Quick Sort 快速排序

用递归在 Python 中实现快速排序会遇到 RuntimeError: maximum recursion depth exceeded 的错误提示,原因是 Python 的递归深度默认为1000(可以用 sys.getrecursionlimit() 来查看),有两种方式解决为题。一种是用循环重写算法,另一种比较简单粗暴,直接用 sys.setrecursionlimit(99999) 把递归深度设置为 99999 这种大数字,更详细可参见这里

def quick_sort(ary):
    return qsort(ary,0,len(ary)-1)
def qsort(ary,left,right):
    #快排函数,ary为待排序数组,left为待排序的左边界,right为右边界
    if left >= right : return ary
    key = ary[left]     #取最左边的为基准数
    lp = left           #左指针
    rp = right          #右指针
    while lp < rp :
        while ary[rp] >= key and lp < rp :
            rp -= 1
        while ary[lp] <= key and lp < rp :
            lp += 1
        ary[lp],ary[rp] = ary[rp],ary[lp]
    ary[left],ary[lp] = ary[lp],ary[left]
    qsort(ary,left,lp-1)
    qsort(ary,rp+1,right)
    return ary

时间复杂度、稳定性

平均情况:O(nlgn)
最坏情况:O(n²)
最好情况:O(nlgn)
不稳定
辅助空间:O(nlgn)~O(n)

特点

在实际应用中,一般比其他算法快很多,内循环很小,原地排序(仅需要很小的辅助栈),且将长度为 N 的数组排序的时间与 NlgN 成正比。但很脆弱,实际性能会因某些错误变成平方级。[4]
快速排序和归并排序使用分治法和递归进行排序,但快排在合并子数组后是自然有序的大数组;归并在合并阶段则繁琐一些,还要再次进行比较。
在对数组进行切分不平衡时,会导致性能低效(例如第一次从最小元素切分、第二次从第二小的元素切分……)。解决该问题,可以对数组进行随机排序,避免性能下降到极低。

优化

  1. 快排在小数组中比插入排序慢,因此在排序小数组时使用插入排序。
  2. 三取样切分。
  3. 熵最优排序[5]

以上三种优化是《Algorithms 4th》中对于快排性能的优化,都是对算法本身做了一些改进。而最著名的优化则是《算法导论》中提到的『随机化快速排序』,与上述三种性能优化有很大不同。
随机化快速排序,一般来说都是将取主元的过程随机化。随机化快速排序的『优化』,并没有提高快排的性能,而是避免了某些序列使快排性能大幅降低到O(n²)的可能性、防止他人设计一个序列对服务器发起DoS攻击,使排序更加稳定和安全(我发现还有很多人有类似的误解)。事实上随机化快速排序和普通快速排序在时间复杂度上同为O(nlgn),实际实验中也没有显著差异。

知乎上一个用户对快排和随机快排的性能做了测试,但我猜他可能也理解错了,他所做的只是在优化随机快排的代码,但并没有显著提高算法性能或降低复杂度(知乎链接)。Thomas Cormen 在 Quora 中也回答了关于随机化快排和普通快排性能差异的问题(Quora链接),但是他本人在《算法导论》(中文版100页,英文版179页)中并没有把这个问题讲的很透彻,也许算是一个微小的失误。

Heap Sort 堆排序

def heap_sort(ary) :
    n = len(ary)
    first = int(n/2-1)       #最后一个非叶子节点
    for start in range(first,-1,-1) :     #构造大根堆
        max_heapify(ary,start,n-1)
    for end in range(n-1,0,-1):           #堆排,将大根堆转换成有序数组
        ary[end],ary[0] = ary[0],ary[end]
        max_heapify(ary,0,end-1)
    return ary
#最大堆调整:将堆的末端子节点作调整,使得子节点永远小于父节点
#start为当前需要调整最大堆的位置,end为调整边界
def max_heapify(ary,start,end):
    root = start
    while True :
        child = root*2 +1               #调整节点的子节点
        if child > end : break
        if child+1 <= end and ary[child] < ary[child+1] :
            child = child+1             #取较大的子节点
        if ary[root] < ary[child] :     #较大的子节点成为父节点
            ary[root],ary[child] = ary[child],ary[root]     #交换
            root = child
        else :
            break

时间复杂度、稳定性

平均情况:O(nlgn)
最坏情况:O(nlgn)
最好情况:O(nlgn)
不稳定
辅助空间:O(1)
2. 特点
目前唯一的能最优利用时间与空间的算法,最坏情况下也能保证 2NlgN 次比较和恒定的辅助空间。当空间紧张时(例如嵌入式),堆排序用几行代码就能实现实现较好性能。
缺点是无法利用缓存,缓存未命中的次数远高于大多数在相邻元素间比较的算法。
sort

通过倍率实验预测程序的增长数量级

可以使用倍率实验来预测任意程序的增长数量级:
每次实验时使输入翻倍,计算时间,并求两次运行时间的比值。反复运行直到比值趋近于2的b次方。[6]

算法的『稳定性』是什么

稳定性指,当排序的元素中有相同的值时,这些具有相同值的元素在排序后的前后位置是否发生变化的性质。如果变化,则不稳定;如果不变化,则稳定。在 Bubble Sort 中,相邻元素互相交换,如果两个相邻元素相等则不需交换;如果两个相等的元素彼此之间有间隔,那么即便它们和相邻元素交换后,其相对的前后位置也不会发生变化,所以是稳定的。而在 Selection Sort 中,第一个元素与最小的元素交换、第二个元素与第二小元素交换等等,间隔有可能很大从而在交换时跳过了相同值的元素,进而造成相同值元素前后位置改变,因此是不稳定的。
排序的键值有可能只是元素的一个属性,如果元素本身还具有其他属性,那么键值相同的元素排序后的相对顺序还与其他属性有关。例如用稳定的算法对员工按工资排序,假如原数组是按年龄排序的,那么月薪同为7000元的3个人在按工资排序后仍然可以保持年龄正序,即最初的相对位置不变。

参考资料:

[1]《Algorithms 4th》 P248
[2]《Algorithms 4th》 P252
[3]《Algorithms 4th》 P262
[4]《Algorithms 4th》 P288
[5]《Algorithms 4th》 P296
[6]《Algorithms 4th》 P121

Tagged : / / / /

万用骰子脚本(跑团专用)

几个月前,我写过一个专门用来玩卡坦岛的命令行骰子工具,里面的骰子函数都是写死的,只能选2d6/2d10/3d4这几个,够用,但是不方便,适用性太差。最近入了《Pathfinder基础包》,准备重新开始跑团,看几个跑团QQ群里都有骰子机器人(方便大家开网团的时候投骰子),他们输入『.r 3d6』『.r 4d10』甚至『.r 1d97』这种实际中并不存在的骰子都可以得到值,自由度非常高。
于是我计划用Python来实现这种高自由度的骰子。首先遇到的问题就是如何让程序识别『3d6』『1d4』『1d8』这种跑团黑话。先普及一下,d4/d6/d8/d10/d20等都指骰子的面数,例如d4指的是四面骰,d20则指二十面骰,1d4指扔1个四面骰,2d6则指扔两个六面骰。普通游戏一般用不到这么多种类的骰子,而在以大量数值检定为核心的TRPG(桌面角色扮演游戏)中,这些骰子就不可或缺了。
首先想到的方法是用正则表达式来解析命令。以最常用的『1d6』为例,『1』指骰子个数,『6』是骰子类型(面数),『d』则是分割二者的分隔符。用正则表达式来写的话,应该是这样:

roll = input('> ')
match = re.search(r'(\d+)([Dd])(\d+)', roll)

先让用户输入命令,然后开始解析命令。命令的结构是『数字』+『D或d』+『数字』,正则表达式如上图。最早的版本里,是 r'(\d)([Dd])(\d)’ ,两个数字位都没『+』,后来发现第二个数字位必须带『+』(因为骰子类型有可能是两位数甚至三位数,例如1d20,1d100),于是我干脆把两个数字位都变成可以无限位取值的。
到此,解析命令完成。下一个问题发生在定义函数时的全局变量上。早期版本如下:

result = 0
def d(n):
    result = randint(1, n)

函数外部出现了变量 result,函数内部又给 result 赋值,电脑就懵圈了。在这里,我一直没搞懂的问题是,定义函数时的返回值,并不是返回给某个变量,而是对应了这个函数本身。result = 0 这个变量的初始化也可以删掉。在朱老师的指导下,终于搞明白这个问题,于是代码顺利改成这样:

def d(n):
    return randint(1, n)

最后,做好一个 for 循环来实现反复扔骰子的动作即可:

for i in range(m):
    result = d(n)
    dice.append(result)
    print(result)
print('和为: ', sum(dice))

至此,其实还没有写完,脚本还有很多地方需要完善,但是已经不再是当初那个被朱老师批评的『到处给全局变量赋值的超级烂代码』了。日拱一卒,余欣慰也。
Github地址
欢迎各位去围观我写的其他小脚本,帮我改改这些超级烂代码!

Tagged : / /