高可配置的emacs

Table of Contents

  1. customizable emacs
    1. 源码设计
    2. 软件设计
    3. 扩展语言
    4. Emacs并非无所不能

customizable emacs

Emacs是世界上可配置性最强的编辑器。

我最开始试图在上面这句话里,加上“几乎”或者“之一”来让这句话变得更准确。后来发现不管加上哪个词,这句话都会变成一句错误。

是的,世界上没有任何编辑器,在可配置性上能超越它。

争论这句话的对错已经没有意义,我现在想知道,为什么它可以在可配置上能够达到极致。或者,为什么其他编辑器达不到这种可配置程度?

这篇文章将尝试从一个矿工的视角,把最宝贵的这部分内容挖掘出来。

源码设计

linux kernel内核用到了很小一部分的汇编源码,据说新版本已经完全用c来实现了。

如果不是迫不得已,我相信linux kernel还是倾向避免接触到汇编的。

Emacs也遇到了这样的问题,由于效率上的考虑,它不得不将一部分底层代码使用c语言来构建。从本质上来说,Emacs是一个用c语言实现的lisp解释器。

剩下80%的部分,Emacs是使用在c上构建的lisp语言来编写的。

elisp更接近脚本语言,它有点类似python(sorry,其实是python有点类似lisp),也可以编译为字节码。它的一切都是可读的,任务一个lisp语言的函数,都可以快速找到它对应的lisp源码。

这就为用户提供了接近真相的机会,如果你愿意,甚至可以根据自己的需要直接修改核心的Emacs(好吧,你最好不要这么做,这意味着你已经脱离了Emacs和其他Emacer的支持)。

软件设计

软件包括两部分,可执行的二进制或脚本文件和它所包含的文档。

Emacs的核心软件都是结构化的,这使得我们可以很容易地找到相关功能所在的源码。比如,如果你想研究Emacs中的window操作实现,可以找到window操作对应的lisp文件window.el。

文档部分也是经过了精心设计的,从C-h对应的完整的文档分类可见一斑。这使得Emacs的用户很容易地可以查找到一整块完整的功能,读过Emacs手册的人应该都深有体会。

这里需要说明一点的是,Emacs的文档系统其实还包含了动态文档系统,比如查看当前mode或key bindings。所有的文档组成了一张严密的网络,使得我们在Emacs中可以快速地到达想要去的各个地方。

扩展语言

众所周知,Emacs使用了自己用c语言实现的一种lisp方言:elisp。

我认为,elisp被大家过分夸大了,甚至给人一种“Lisp不同寻常的语法决定了其开发者都是资深开发者”的印象。

甚至,我认为,恰恰是elisp的一些缺点,才导致Emacs的扩展性达到了无与伦比的高度。

lisp的括号可能是最被人诟病的设计,有一个lisp笑话被lisp黑客广为流传:“据说一个黑客偷到了美国用于导弹控制的LISP代码的最后一页,却发现这最后一页全是右括号”。

你可能写过python或者c语言这种面向对象或者面向过程的语言,但是遗憾的是,在lisp的世界里,这两种编程范式都很难被广泛实施,有很大原因源自lisp的括号设计。括号的大量出现,决定了你很难一口气写出100行的代码(希望你有勇气接受这个挑战)。所以它强制你精简每一个函数的实现,大量的微小函数组成了一张紧密的网,来提供强大灵活的整体功能。

这其实就是自底向上的编程思想了,没错,lisp强制我们使用自底向上的思想来思考问题。

说到自底向上编码,它的好处在哪里呢?它里面其实包含了分层的思想,即下层的功能可以很容易地被上层代码复用,而且当粒度足够小时,几乎所有想要的功能都一触即达。我们想要的只是一个编辑普通文本的编辑器,最后一不小心就变成了一个操作系统。

当我在借用第三方package实现某个功能时,一下子就找到了我所需要使用了所有函数,我需要做的只是使用自己的风格封闭它。

据说,当你打算设计一个极度复杂的系统时,lisp会让开发成本和维护成本随复杂度呈指数下降。Emacs就是一个例子。

“凡有的,还要给他,让他丰足有余。”

Emacs并非无所不能

最后的这个标题出人意外,前面夸赞了那么多,最后要承认它只不过是个使用习惯令人蹩脚或者过时的编辑器吗?

不是的!

我想知道网络上,Emacer们都在干些什么(问题来自知乎):

  1. 你认为最好看的 Emacs 配色方案(color scheme)是哪款?
  2. emacs还能做什么?emacs比vim好在哪里?
  3. Vim 和 Emacs 分别适合哪些人群?
  4. 谁知道emacs lispy-mode的用法?
  5. 对于使用emacs包管理器ELPA,你有哪些推荐的包?

我之前是一个vim用户,想学习Emacs多次未果,主要是觉得Emacs的按键太反人类。但是这是一种先入为主的思想,现在看来,应该让Emacs来适应我们,而不是本末倒置,要知道,Emacs就强大在它的可配置性上。

不要在自己还没有尝试了解一个东西之前,试图通过直接询问的方式一劳永逸。可以临时解决问题,但是不要在自己还不清楚的事情上浪费太多时间,最后原理没了解,只是在东拼西凑试图猜中答案。

不要做一个完美主义者。我之前也喜欢折腾,从字段,到配色,最后发现,这其实跟不动脑思考只知道蛮干是一样的。要精进不要盲进,少即是多,容忍不知道。

另外,不要试图寻找自己不需要的答案,也不要试图通过询问一次性获得答案。我们不需要知道别人使用哪些package,自己用的顺手才最重要。第一手资料在官方手册或者包列表服务器,我们要找的答案,绝不会简单地摆放在google里。

使用emacs管理生活

Table of Contents

  1. 使用emacs管理生活
    1. 关于这篇文章
    2. 一天应该从一杯牛奶开始吗?
    3. 一天从emacs开始
    4. 来个番茄?
    5. 不能煮咖啡的编辑器不是一个好操作系统
    6. 附件

使用emacs管理生活

关于这篇文章

很久没有写文章了,我是在一周前决定在今天写这篇文章的,用来分享我在emacs上的实践。在这周里,我有时闲下来会想到这篇文章的一些碎片,零零散散,错落有致。

我是在一两周前转到emacs的,在之前我一直使用vim作为我的编辑器。当然,只是编辑器,因为我的大部分时间都是用来编辑文本(代码、配置)。

我转到emacs,最开始是借助了spacemacs,它让我看到了emacs与vim共存的可能。但是因为spacemacs过于范化,不能满足我自己的使用习惯,我决定开始打磨自己的emacs配置。

emacs最令人惊叹的是org-mode,或者更准确地说,围绕org-mode建立起的生态。如果你没有尝试过,建议你在合适的时间研究一下这个模式,相信我,它会彻底改变你的生活方式。

由于这篇文章主要是分享自己的一些感受,所以不会涉及太多代码(你知道的,在emacs的世界里,存在大量elisp代码片段)。

一天应该从一杯牛奶开始吗?

每个人都有自己的节奏,我以前的节奏就是,混乱地开始和混乱的结束一天的生活和工作。如果我们有能力使这种模式免于混乱,便可以得到一个更为高级的名字,叫做自底向上的生活方式,过好当下,然后等着上天赐予我们最好的礼物。拖延症患者的生活方式并不是自底向上,而仍处于混乱状态(譬如我),所以还算不上自底向上。

对应的方式就是自顶向下了,我们可能会制定一个N年的计划,比如一个长达三年的学习计划,然后逐步分解去执行。不过实际情况往往会跟目标有出入,可以选择分片段逐步细化的方式。

在经历过长达数年的混沌时期后,我决定让自己逐渐适应并且切换到了第二种方式。我需要一个理论支持,最好是已经成熟并且有很多人实践过的理论。于是我找到了GTD。

关于GTD(Get Things Done),有一些专门的书籍可以学习。不过在我看来,一个好的实践,一定是不需要太多繁杂步骤的,简单而且行之有效。所以我并没有从头到尾阅读书籍,而是了解了GTD的思想,以及它的大体步骤。

GTD最核心的思想,可以用一句话来形容:将头脑中的想法记录到载体上。这样做的好处是什么呢?

首先,大脑没有了负担。设想之前的工作或生活,突然,脑海中想起了一件事,去做或者一会去做,有可能一会又忘记了,大脑承担了很多思考外的低级功能。

其次,待办更清晰了。只要我们能拿到所有的待办,就可以对待办进行评级、排期。只要一件事情做好了排期,我们只要等到对的时间去做就可以了。

一句话,解放大脑,提高专注力。

官方的GTD步骤有五个,我在这里整理下我的实践步骤:

  1. 收集:将大脑中的想法、邮件收件箱或其它的工具的信息收集起来。对于记录脑中的想法而言,手机是很方便的一个工具,我喜欢使用mac自带的Notes来记录脑海中一闪而过的想法,还可以直接同步到电脑端打开,再次处理都很方便。

  2. 整理:收集到的信息,需要进行再次处理,明确意义后归类。比如一个待办的具体定义,要归放到哪个项目下。

  3. 排期:对待办设置优先级,并查看自己的排期,将待办加入到自己的排期中。我自己一般不会对某个待办设置优先级,而是直接对待办所属的项目制定优先级。比如,我在跟进一个非常重要的项目,那它下面的待办就理应具有更高的优先组。

  4. 执行:这个步骤就相对简单了,因为信息已经做了梳理和排期,到了合适的时间,按照计划执行就可以了。

当然,第4个步骤简单,但也困难。困难之处在于,有很多情况是我们预料不到的情况。比如,可能会有其它同事来打断,或者有更高优先级的突发事件,这就需要我们根据其它方法论来处理了。碍于篇幅,这种情况我们暂不展开。

讲了这么多,还没有回答上面的问题。一天应该从一杯牛奶开始吗?对于我来说,答案是否定的。我的一天是从GTD开始的。

一天从emacs开始

市面上有很多GTD工具,可惜我都没使用过。我的工作几乎全部使用电脑完成,所以需要一个更加无缝操作的GTD工具,它最好不用分散我太多的注意力。

emacs里有一个org-mode,可以用来管理TODO列表,例如修改TODO列表状态,对TODO排期,或者记录TODO用时。

org-agenda是一个org-mode的相关工具,可以自定义项目的TODO列表的展示逻辑,比如显示今天有哪些代办,或者显示有哪些项目处于stuck状态(某个项目下没有TODO列表)。

用一句很装逼的话来说,“我已经一点都记不起明天需要做什么了”。这应该是我的一个追求吧,现在还没有完全达到。

到公司后,我的第一件事就是打开emacs。我对它进行了一些配置,打开软件后它会自动为我显示这一天的待办记录,这一天将从查阅待办开始。

在开始某一个待办前,我会使用org-clock-in对待办进行计时,以此来跟踪我在一个待办上花费的时间,并且在完成一个待办后休息一小段时间。

世界从未如此高效而轻松。

来个番茄?

我还找到一个提高专注能力的工作,番茄钟。

发明者为了提高自己的效率,发明了一种时间分片的方式,每25分钟为一个番茄钟,每个番茄钟结束后休息5分钟,每4个番茄钟休息15分钟。

这是一种很好的时间管理方式,它能让我们感知到时间的流逝,更好的对某件事所花费的时间做出评估。

我曾经实践过这种方法,但是发现它过于死板不够灵活,于是根据自己的需要进行了部分改造。

仍然是25分钟一个番茄钟,但是我可以在必要的时候提前或推迟结束时间。比如,某个待办进行的很顺利,20分钟就完成了,那我会选择提前结束这个番茄钟;如果某个待办需要30分钟,我可能会选择延迟5分钟。当然,有一种情况是这个待办需要1个小时,我会在25分钟左右,在合适的时机结束番茄钟。

一句话,节奏感很重要。节奏感是一种感觉,即是感觉,就会因人而异,需要找到适合自己的方式或节奏。

emacs是有包来专门提供番茄钟功能的,我没有使用。在我看来,没有太大的意义。

不能煮咖啡的编辑器不是一个好操作系统

你可能已经听说了,emacs是一个操作系统,而非编辑器。

这并非对emacs的嘲讽,实际上我认为这是对emacs的高度评价。

我想先谈一下gnu/linux或者windows操作系统,如果细想一下,“操作系统”这个词并不是很准确,如果你用过gnu/linux,你可能会同意我这个观点:gnu/linux更像是一个资源管理系统,它的任务更多的是接管底层硬件资源并提供调度方式以最大化使用效率。gnu开发了一个套件,用来实现一些与用户交互的功能,但是这是不够的,我们很多时候还是会安装许多额外工具。

所以称emacs是一个操作系统其实蛮贴切的,错的不是emacs,而是操作系统的定义。

你可能也听说过一种现象,emacer们倾向于把自己想要的所有功能都记录到emacs里,他们在emacs里浏览网页,阅读rss,收发邮件,可能甚至有人想用它来控制自己的洗衣机。

我想试着来解释下这种现象,前面我们说过emacs是一个操作系统,甚至它比gnu/linux做得还好,因为它高度定制,且有一致的操作风格。你可以把它看做是一个根据使用者的使用习惯高度特化的操作环境。

举个例子,比如我现在在编辑当前这篇文章,但是感觉这个自然段的表达方式有点问题,但是现在我的关注点是先把这篇文章写完,此时我可以使用remember命令调出一个临时记录note的buffer,记录一下现在的想法,这时想法就被记录到一个叫做remember-notes里了。我可以继续编辑这篇文章,使我的思绪被打算的可能性降到最低。

所以答案就很简单了,因为这个“操作系统”里已经集成了很多可用的工具,如果把另一个工具集成进来,效率和便利性会成指数级增长。

但是这是一个致命的诱惑,你感觉这是一个充满花蜜的骨朵,很有可能它会把你粘住拖进它的花苞消化分解掉,变成它的食物。

如果你像我一样,没有太多的精力去做这些看上去很有乐趣的事情,那就一定要克制住自己了,一定要守住自己的边界,知道什么不可以做。

附件

img

当前正在做的事情被高亮显示(图中为黄色),且会在modeline显示。

如果某个昨天的待办没有完成,Scheduled会变成Sched. 1x,并被加到今天。比如,写作emacs使用实践和腹肌撕裂者两个待办昨天都没有完成(现在已经是第二天的00:22)了,所以被加到了今天。

有一种任务是每隔几天就做一次,比如刮胡子这个待办需要每三天做一次,使用 2019-03-03 Sun +3d 时间,系统就会每三天生成一个待办。

img

我喜欢给一些需要随意做的任务记录时间,这样就可以知道我做某件事的整个时间周期了。成就感悠然而生。

spacemacs从入门到放弃

我的vim史

我是一个vim编辑器的重度用户,使用vim已经十年有余。从日常记录文本、开发到远程服务器的运维,它一直是我必备的工具。我有自己的一套配置,裸用vim也不在话下。

我在很久之前接触lisp,并为之倾倒。从来没有哪一门语言的外在表现可以如此完美统一,我相信一门编程语言的设计是完全可以塑造一个人的思维。谈及lisp,几乎毫无疑问谈到emacs编辑器。

这里顺带谈一个我对emacs设计理念的理解,emacs在我看来是lisp精神的一个很完美的体现(并不是说emacs完美)。emacs使用了20%的c语言代码,实现了一个lisp解释器,然后使用其余80%的代码,在lisp上构造了整个编辑器。所有的操作都对应着lisp函数,使得所有操作都是清晰和可查的。比如我想查找<C-x> <C-f>的实现函数和具体实现,只需要<C-h> k <C-x> <C-f>就可以了。

在可查的历史里,我曾经尝试N次从vim到emacs的转变,但是最终都以失败告终。

后来,一个偶然的机会,遇到spacemacs,让我再一次经历了转变失败的过程。

spacemacs是啥

不要问我是啥偶然的机会,因为我自己也已经记不得了。

spacemacs的官网是http://spacemacs.org/,通过作者的精心包装,从命名到完善的文档,使得它看上去更像一个完整的emacs发行版。但是其它,它的本质是另一份emacs配置。

是的,它是另一份emacs配置,就像https://github.com/上其它人托管的.emacs一样。

官方对它的介绍是,”A community-driven Emacs distribution”,一个社区驱动的emacs发行版。

spacemacs为什么这么吸引我

如果你也是一个vim重度用户,并尝试从vim转到emacs阵营,你就会像我一样首先被它的操作模式吸引。spacemacs默认集成了evil,使得在操作习惯上像是一个lisp配置的vim。

当然,对我来说它还有另一个致命的吸引力,那就是华丽丽的外表。默认是一个紫色的本色方案(基佬紫),非常漂亮美观。可以看下面官方的图片:

ss2

下面说一下官方宣传的优点:

我们知道使用emacs的用户,有一些患上了RSI,spacemacs通过集成evil,并将<leader>绑定到space来解决这个问题。由于space键主要使用拇指按压,所以大大减轻了手指的负担。

spacemacs通过使用助词符前缀,将各个功能对应绑定到各个按键。比如buffer操作绑定到b,help绑定到h。也就是说,buffer对应的操作按键是<space> b ...,并且每一个按键都会有功能提示,交互性也得到了一定保证。可以参考图片。整体上很连贯一致。

spacemacs引入了一个layer的概念,使用某一个功能实际上是添加某个layer,这样就把具体的功能跟第三方包解耦,我们只需要知道自己想要的功能,而不需要知道这个功能需要安装哪些包。这可以说是脱离折腾苦海的一个绝妙的办法,把折腾的成本交给社区。

spacemacs默认集成了100多个包,从编辑到cvs管理,功能涉及到方方页面,几乎将emacs包装成了一个简单易用且全面的系统。而且会根据打开的文件类型动态提醒添加的包,完成一站式体验(类似vscode等的包安装提醒)。

当然,还有一些其它的优势,让我在emacs的体验上走得更远。整整试用了两天时间。

两天的体验之旅

在使用spacemacs的这两天里,我发现了很多更优秀的工作流。

举个例子,项目内的批量替换,spacemacs使用插件使得整个过程更加流畅,首先项目搜索某个关键字,然后在交互环境里整体替换就可以了。

当然,有一个难以逃避的弊端,应该归给emacs的设计。那就是buffer的管理相当混乱,甚至使用git都要打开n个buffer,导致有很大的成本耗在查找和切换buffer上。

关于emacs的缺点,可以参考https://www.v2ex.com/t/332566,个人感觉总结的很到位。

放弃spacemacs

最终我决定放弃spacemacs,我发现它并不是我要找的圣杯。

首先,它过于臃肿。我指的臃肿并不是指其他人说的反应慢,而是它有太多不重要的功能,占用了过多的键位,导致看似设计优良的<space>操作方式,一个简单的功能都藏到很深的地方,比如describe-function被绑定到了SPC h d f,甚至很多时候都记不得某个功能对应的按键序列,需要根据交互提醒找到对应按键。当然,这些按键都是可以重新定义的,但是由于设计它耗费了社区很大精力,修改这样的默认按键就显得更加吃力。不知道修改后它还能不能叫做spacemacs了。

其次,之前谈到的,buffer过多难以维护,严重影响思维逻辑。

接下来

你现在看到的这篇文章,是我用emacs写成的。没错,我成功的转到了emacs,使用着我之前使用的vim键位绑定,甚至有一些比以前更好的解决方案。这要归功于spacemacs,它让我看到了自己之前存在的问题。

spacemacs还是会有大批人在使用的,这印证了一个事实,一个高度可配置的编辑器,仍然可以存在一份可以供一群人使用的通用配置。但这并不能发挥可高度定制这个杀手锏,甚至会阻碍你做过多的配置。

正如我使用vim的经历一样,现在这份emacs配置是我自己从零开始配置的,这是完全属于我自己的配置,并且不能被其它人使用。

而且,转换的成功有赖于我另一个努力,那就是努力不使用过多的功能。emacs的功能多到眼花,可能有一些功能这辈子都用不到,而且还会创建过多buffer,导致陷入之前的场景,没法继续使用下去。只使用自己需要的部分功能,就可以熟悉它对buffer的操作,让整个使用过程不会膨胀到自己的控制之外。

就写这么多吧,思维逻辑比较混乱,本来这件事也有点混乱,刚好了。

更换ssh证书导致paramiko报No session existing错误

问题出现

现在公司的发布系统使用了paramiko来执行远程操作,ssh连接用的证书被记录在配置文件里,是一个列表的形式。没错,我们的证书有很多,用来连接到不同的环境。

接到运维通知,由于安全原因,访问某台机器使用的证书做了更换。

随后没多久,就收到测试同学的反馈,发布代码时系统提示“No existing session”。

简单问题简单解决

这个问题乍看上去只要更新一下配置就可以了,简单的一批。

修改配置文件,重启系统,又重试了一次。结果,还是报这个错误。疯狂了疯狂了,配置也生效了,竟然还是不能连接吗?

问题初探

连接的代码很简单,就是单纯的连接远程机器。

1
2
3
4
5
6
7
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, SSH_PORT, SSH_USER, SSH_PASSWORD, key_filename=SSH_KEYS)

# ...

client.close()

通过测试,发现用命令行连接远程机器是可以的,而且也没有使用ProxyCommand之类,但是通过paramiko就出问题。毫无疑问,问题出在paramiko或者配置上。

session关键字让这个问题看起来像是paramiko的锅,它没处理好自己的会话吗?

一番google,发现了这个链接:https://stackoverflow.com/questions/6832248/paramiko-no-existing-session-exception。这个哥们也是遇到了一样的错误提示,但是他用了密码而系统查找私钥导致出错,关掉密钥查找就解决了这个问题。

看来问题出在密钥的配置上。

开启paramiko日志

发布系统使用的是paramiko 2.1.2,在paramiko的源码里找到了打开日志的方法。

paramiko/util.py:238

1
def log_to_file(filename, level=DEBUG):

通过该函数,可以将paramiko的日志打印到某个文件,比如paramiko.util.log('/tmp/paramiko-debug.log')。打印的日志如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.transport: starting thread (client mode): 0x8ab46350L
.transport: Local version/idstring: SSH-2.0-paramiko_2.1.2
.transport: Remote version/idstring: SSH-2.0-OpenSSH_5.3
.transport: Connected (version 2.0, client OpenSSH_5.3)
.transport: kex algos:[u'diffie-hellman-group-exchange-sha256', u'diffie-hellman-group-exchange-sha1', u'diffie-hellman-group14-sha1', u'diffie-hellman-group1-sha1'] server key:[
.transport: Kex agreed: diffie-hellman-group1-sha1
.transport: Cipher agreed: aes128-ctr
.transport: MAC agreed: hmac-sha2-256
.transport: Compression agreed: none
.transport: kex engine KexGroup1 specified hash_algo <built-in function openssl_sha1>
.transport: Switch to new keys ...
.transport: Adding ssh-rsa host key for [10.0.2.243]:36000: 2e3faf53075afccf09a3ac2q391dd5e8
.transport: Trying key 3a96e7e9e3f59963fbee693f44x8cf58 from /home/user/.ssh/id_rsa1
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 8628c2021979e0e0302aac6bc8xcbcef from /home/user/.ssh/id_rsa2
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 6e399bc162a737150e1bd643abx7737d from /home/user/.ssh/id_rsa3
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 77d22314df154bfd5ca591bd16x19363 from /home/user/.ssh/id_rsa4
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 77d22314df154bfd5ca591bd16x19363 from /home/user/.ssh/id_rsa5
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key c1909f00bf62c74eed5a3a9e5dxa73d1 from /home/user/.ssh/id_rsa6
.transport: userauth is OK
.transport: Disconnect (code 2): Too many authentication failures for user
.transport: Trying key c897a8e51a0d41facf7fa712a2xeace8 from /home/user/.ssh/id_rsa7
.transport: Trying discovered key 3a96e7e9e3f5996xfbee693f44b8cf58 in /home/user/.ssh/id_rsa1

我们几乎可以一眼看到最后的错误提示:“Disconnect (code 2): Too many authentication failures for user”。看起来像是失败次数过多被拒了。google后得到了这个链接:https://superuser.com/questions/187779/too-many-authentication-failures-for-username。“This is usually caused by inadvertently offering multiple ssh keys to the server.”,答主建议明确指定使用哪个证书来访问服务器。

我down了一份OpenSSH_7.8的源码下来,经过一番grep,找到了下面这几个代码片段。

ssh/auth.c:281

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
auth_maxtries_exceeded(Authctxt *authctxt)
{
struct ssh *ssh = active_state; /* XXX */

error("maximum authentication attempts exceeded for "
"%s%.100s from %.200s port %d ssh2",
authctxt->valid ? "" : "invalid user ",
authctxt->user,
ssh_remote_ipaddr(ssh),
ssh_remote_port(ssh));
packet_disconnect("Too many authentication failures");
/* NOTREACHED */
}

ssh/auth2.c:322

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
userauth_finish(struct ssh *ssh, int authenticated, const char *method,
const char *submethod)
{
// ...
if (authenticated == 1) {
/* turn off userauth */
// ...
} else {
/* Allow initial try of "none" auth without failure penalty */
if (!partial && !authctxt->server_caused_failure &&
(authctxt->attempt > 1 || strcmp(method, "none") != 0))
authctxt->failures++;
if (authctxt->failures >= options.max_authtries)
auth_maxtries_exceeded(authctxt);
// ...
}
}

ssh/srvconf.h:39

1
#define DEFAULT_AUTH_FAIL_MAX   6       /* Default for MaxAuthTries */

原来ssh默认允许尝试6个密钥,如果还没有成功,就会直接退出了。这么做的原因想来也显而易见了。

解决办法

我的临时解决办法是,将密钥文件的配置控制在6个以内。当然,更好点的办法是使用fabric来替代直接使用paramiko,并复用系统的.ssh/config配置,这就是后面需要考虑的了。

使用subprocess模块异步并发执行远程命令

远程执行命令

运维自动化平台不可避免地会涉及到远程命令执行操作,主要分为两类主要做法:目标机器安装agent,或者使用ssh。

saltstack是一个典型的agent模式的远程控制工具,麻烦的地方是首先要在目标机器上安装saltstack的agent。

使用ssh的模块居多,fabric和ansible是此类工具中的典型,这类工具的优点是方便,不用在目标机安装agent。值得一提的是,这两个工具都是基于paramiko。

使用ssh执行的另一种做法是,直接调用本地的ssh来完成。缺点是针对每一台远程主机都需要开一个进程,比起在程序内建立连接要耗费更多的资源。优点也很明显,比如,公司最近在做ssh的改造,在改造的过程中,可能会出现明明ssh命令可以连接,使用第三方模块就是不灵的情况。ssh的命令和第三方模块在配置上毕竟有差异,需要维护两份配置。

这篇文章就是要使用进程来完成在批量远程机器上执行某个命令。

subprocess模块大概

subprocess模块是python2中的一个官方模块,顾名思义,它主要用于操作子进程。

subprocess.Popen()用于创建一个子进程,它的第一个参数是列表,即创建进程所需要的参数,类似c语言中的argv。

需要注意的一点是,Popen是异步执行的,也就是说创建Popen后会立刻返回,子进程继续执行。关于异步,还有很多可以聊的地方,后面另开一篇文章写一下。

既然是异步的,我们就需要某种机制来跟它进行通信。Popen提供了多个方式来与子进程通信。

Popen.wait()将主程序挂起等待子进程的结束,结束后会返回子进程的返回码。

Popen.poll()可以检测子进程是否还在执行,并返回子进程的返回码。

下面我们将用几个小例子来不断扩展,最终实现一个可在多台远程机器并行执行命令的功能。

一个简单的远程执行示例

我们来写一个简单的示例,说明如何使用subprocess模块远程执行一条命令。

1
2
3
4
5
6
7
8
9
10
11
import subprocess


cmd = 'echo hello'
cmd_arg = ['ssh', 'localhost', cmd]

process = subprocess.Popen(
cmd_arg, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
retcode = process.wait() # 0
out = process.stdout.read() # 'hello'
err = process.stderr.read() # ''

我们首先创建了一个Popen对象,然后等待这个子进程的执行结束,获取返回值和输出。

这断代码很简单,注意stdout=subprocess.PIPE,我们想捕捉到子进程的输出,而不是直接打印到标准标出,所以要求Popen把输出打印到管道供我们读取。

同时在多个机器上执行命令

这里我们利用了Popen的异步特性,来加快多服务器任务的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import subprocess


cmd = 'sleep 3 && echo hello'
cmd_arg1 = ['ssh', 'localhost', cmd]
cmd_arg2 = ['ssh', '127.0.0.1', cmd]

process1 = subprocess.Popen(
cmd_arg1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process2 = subprocess.Popen(
cmd_arg2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

retcode1 = process1.wait()
retcode2 = process1.wait()

这次,我们连接了两个机器并执行耗时3s的操作,由于Popen是异步的,这个脚本的实际执行时间仍然是3s。这个地方的操作是不是与多线程的操作有点类似,哈哈。

多个机器执行获取执行时间

由于网络环境和机器配置的不同,我们在不同机器的执行时间是有差别的。有时候,我们需要一个时间数据来了解哪个机器执行耗时比较久,以此做优化。

这时,我们需要Popen.poll()来异步检测命令是否执行完成,而不是将主进程挂起等待第一个子进程。如果第一个子进程执行时间比第二个子进程要长,我们就获取不到第二个子进程的执行时间。

为了更好的可读性,这里我们没有加入for循环之类的结束,放弃了部分逻辑上的灵活性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import time
import subprocess


cmd = 'sleep 3 && echo hello'
cmd_arg1 = ['ssh', 'localhost', cmd]
cmd_arg2 = ['ssh', '127.0.0.1', cmd]

process1 = subprocess.Popen(
cmd_arg1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process2 = subprocess.Popen(
cmd_arg2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

process1_ok = False
process2_ok = False

start_time = time.time()
while True:
if process1_ok and process2_ok:
break

if not process1_ok:
if process1.poll() is not None:
process1_ok = True
print 'process 1 finished, delta time:', time.time() - start_time

if not process2_ok:
if process2.poll() is not None:
process2_ok = True
print 'process 2 finished, delta time:', time.time() - start_time

time.sleep(1) # 防止空跑导致cpu占用过高

执行输出为:

1
2
process 1 finished, delta time: 4.01682209969
process 2 finished, delta time: 4.01691508293

最后的执行时间多了1s,这是因为我们做了一个time.sleep(1)操作。由于我们考查的是哪台机器影响了整体的耗时,而且远程任务执行时间远不止1s,所以这里的1s不影响我们的判断。当然,适当缩小time.sleep()的参数也是可以的。

封闭一个可并行在多台服务器执行的函数

我们将上面的脚本封闭一下,形成一个可以复用的函数。有点长,但是只是在之前基础上做了一些简单的操作,并没有增加什么高深的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def run(hostname, command, user=None):
"""
使用ssh执行远程命令

hostname 字符串或数组,多个hostname使用数组
"""
if not isinstance(hostname, list):
hostname = [hostname]
else:
hostname = list(set(hostname))

# 创建执行命令子进程
processes = {}
for h in hostname:
if user is None:
user_at_hostname = h
else:
user_at_hostname = '{0}@{1}'.format(user, h)

cmd_args = ['ssh', user_at_hostname, command]
processes[h] = subprocess.Popen(
cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

# 等待子进程结束,获取结果
start_time = time.time()
result = {}
while True:
running_hostnames = set(processes.keys()) - set(result.keys())
if len(running_hostnames) == 0:
break

for h in running_hostnames:
process = processes[h]
retcode = process.poll()

if retcode is None:
continue

status = STATUS_SUCC if retcode == 0 else STATUS_FAIL
out, err = process.stdout.read(), process.stderr.read()
delta_time = time.time() - start_time

result[h] = {
'out': out,
'err': err,
'status': status,
'delta_time': delta_time
}

time.sleep(1)

r = {
'ts': time.time(),
'cmd': command,
'result': result,
}

return r

celery使用fabric出现大量ssh -W进程

公司的运维平台有很大一部分是python写的。

发布系统不可避免的要与远程机器做交互,我们选择了基于paramiko的fabric模块来完成这部分工作。

由于发布过程中存在大量耗时很久的任务,所以选用了celery来执行异步任务。有一部分远程操作是通过celery的任务调用fabric来完成的。

出现大量ssh -W

在某一天,突然接到运维同学的反馈,说生产环境存在大量ssh -W进程,希望可以查一下出现的原因。

通过定位,很快确认了是ssh连接远程时使用了ProxyCommand,导致出现了ssh -W进程,这些进程实际上是ssh连接到远端的代理。

但是奇怪的是,为什么任务执行完后仍然存在大量ssh -W不能正常退出。

后来通过阅读fabric的源代码,在代码里找到了答案。

fabric/state.py

1
2
3
4
5
#
# Host connection dict/cache
#

connections = HostConnectionCache()

fabric/network.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class HostConnectionCache(dict):
"""
...
"""
def connect(self, key):
"""
Force a new connection to ``key`` host string.
"""
from fabric.state import env

user, host, port = normalize(key)
key = normalize_to_string(key)
seek_gateway = True
# break the loop when the host is gateway itself
if env.gateway:
seek_gateway = normalize_to_string(env.gateway) != key
self[key] = connect(
user, host, port, cache=self, seek_gateway=seek_gateway)

def __getitem__(self, key):
"""
Autoconnect + return connection object
"""
key = normalize_to_string(key)
if key not in self:
self.connect(key)
return dict.__getitem__(self, key)


# ...


def disconnect_all():
"""
Disconnect from all currently connected servers.

Used at the end of ``fab``'s main loop, and also intended for use by
library users.
"""
from fabric.state import connections, output
# Explicitly disconnect from all servers
for key in connections.keys():
if output.status:
# Here we can't use the py3k print(x, end=" ")
# because 2.5 backwards compatibility
sys.stdout.write("Disconnecting from %s... " % denormalize(key))
connections[key].close()
del connections[key]
if output.status:
sys.stdout.write("done.\n")

原来,fabric默认会维护一个连接缓存,并且不会主动断开这些连接,直到脚本结束垃圾回收时自己断开。但是由于使用了celery,celery是常驻进程不会退出,导致fabric不能自己主动释放连接,所以连接一直在,ssh -W也一直在。

知道原因,就很好解决了。可以主动调用network.disconnect_all()来释放连接,或者干脆就保持着连接,以待下次使用。

需要注意的是,HostConnectionCache是一个全局变量,在celery这类常驻进程中使用还是存在一些坑的。

cpu飙升到100%

知道原因后,我做了一个kill操作,手动将ssh -W杀死了。这时候,再次向之前的目标机发送命令时,ssh应该会报错,因为代理被干掉了。

意料之中,系统再次执行命令时果然提示“broken pipe”。

但是,奇怪的是,运行celery的机器,cpu占用率达到了100%。这就有点奇怪了。

查找原因无果,在github上提交了一个issue https://github.com/paramiko/paramiko/issues/1277。ploxiln提供了另一个关联issue,表明这是paramiko库的一个bug,通过链接可以查找到patch文件。

到此,问题都已解决。

吹牛逼的艺术

写下这个题目,我是非常心虚的。因为我不会吹牛逼。

不过我想起了之前做过的一些软件系统,它们最开始看上去确实其貌不扬,但是经过不断地优化改进,最终的效果非常令人满意。

所以我决定写这么一篇其貌不扬的文章,或者说给自己看的一份指南,就像手机使用指南一样。不同的是,这份指南不提供产品,因为我们指导的其实就是我们自己。然后,我来慢慢地把它优化改进,让它变成一份更有参考价值的指南。

不得不说,吹牛逼是一件很有技术含量的事。

为什么我们需要吹牛逼

我觉得我们是需要吹牛逼的,就像我们需要手机。

对于个人,吹牛逼能将我们丰富的内心世界跃然嘴前。为了不让这么一个美丽的心灵埋没,我们必须要学会吹牛逼的能力。

对于他人,吹牛逼能把我们的神奇经历传授于众,让人们的生活更丰富。

如何吹牛逼

要想吹一个满分的牛逼,需要做到下面几点:

  1. 必须要有吹牛逼的资本。吹自己擅长的,不吹自己不擅长的(这时要吸)。
  2. 吹牛逼一定要吹实,不吹虚。如果吹虚不务实,再牛的逼也会被吹破,让自己陷入尴尬的境地。
  3. 吹牛逼一定要打草稿。不打草稿的牛逼,往往是吹到一半就卡壳泄了气。
  4. 平日一定要注意自己的言行。要时刻牢记,注意当下的言行,以为我们日后吹牛逼积攒资本。
  5. 勤学苦练。我觉得这个在最开始是最难的,因为最开始可吹的牛逼是很少的。这是个量变到质变的过程,不可强硬,顺其自然。
  6. 从小事练起。吹牛逼非一日之功,而且要注意从一些微不足道的地方练起,从走路吃饭说话写字练起,吹可吹与不可吹之牛逼。

唠唠python(9) -- 基础概念的梳理

温顾而知新,可以为师矣。

温顾知新,可以为师。当然,我们的目标不是为师。

时常复习一下自己学习到的东西,是一种不错的习惯。就像牛的反刍,复习是对已有知识的梳理,这个过程既轻松,又能更好的巩固我们的所学,而且时常还可以关注到自己之前因匆忙而遗漏的东西。

当然,这篇文章草草看一下也是可以的,有很多东西是不需要一次掌握的。

当我们都意识不到有这么一个概念时,实际上已经掌握个七八分了,只需要再稍微整理一下思路,就可以轻松掌握了。这就是所谓的质变吧?

术语

我们首先得说说“术语”这个词。

“术语”听起来给人的第一印象是吓人,又是什么神奇的正常人看不懂的东西?

为什么会有术语这种神奇的东西呢?

人类的语言是很重要的,从诞生以来,它大大提高了人们沟通的效率。但是随着语言的发展,同一个词都衍生出了多种意思,而在使用不当的情况下,语言恰恰又阻碍了人们的沟通。

怎么来避免这种一词多义的情况呢,于是人们发明出了术语这么个东西。术语的特点在于,在某种特定的领域或上下文中,同一个术语是精确的,它会被多方理解成同一种意思,这样人们就达成了共识。

说白了,术语就是为了使大家沟通的过程尽可能少的产生歧义。

为了达到这种目的,术语就不得不避开我们平日经常会用到的词。这就导致术语看起来稀奇古怪,对我们不那么友好。实际上,它是为了精确的沟通而作了妥协,我们要体谅。

字符串

1
print "hello, I'm a string"

我们之前说,print可以显示“一句话”。实际上,“一句话”是非常不准确的,因为一句话有可能是我们听到的一段录音,也有可能是文章中的文字。我们可以找个更明确的词。

在计算机中,一个字母a叫做一个字符。顾名思义,就是一个符号,它代表一个字母。就像我们的羊肉串是把很多羊肉串起来,我们之前所说的“一句话”是由一个或多个字符组成的,故叫做字符串。

“a”是字符串,”abc”也是字符串。

变量

1
2
s = "hello, I'm a string"
print s

根据我们之前的解释,s类似于一个箱子,它可以把我们的字符串”hello, I’m a string”放进去,在使用的时候(如print s)再从里面取出来,这样就达到了类似箱子一样的暂存功能。

这里的箱子只是一种类比,在语言中有一个术语,叫做“变量”,这个术语听起来怪怪的,不那么直观。它实际上是从数学里借来的概念(说借并不合适,计算机本就是数学的一个分支),意思是没有固定的值,可以改变的数。到计算机里,它也可能是字符串或其它类型的东西。

这里的“变”很好理解,一个箱子可以放衣服,也可以放鞋袜。

当然,有变量就会有常量。比如我们的文具盒只能放铅笔像皮,放不下衣物。python是一门简洁的语言,在语法上是没有定义常量的,所以我们可以不需要纠结“常量”的概念,在这里提到是为了帮助我们理解“变量”这个概念。

功能

我们说洗衣机把洗衣服的很多步骤打包封装起来,只留给我们一个按钮开关。“洗衣机有洗衣服的功能”,虽然听起来有点白痴,但它确实是个事实。

功能的概念是很好好理解的。它比较接近我们的生活。

在计算机中也有一个概念,叫做“函数”,它是比功能更准确的一种描述。这也是一个从数学中借用过来的概念,它并非我们日常生活中提到的功能,而就是单指语言中所提供的对步骤的封装。

类是使用了生物学中的概念。

生物学中对生物有“界门纲目科属种”的不同精细程度的分类,如豹是猫科,豹属,再细分下来是豹种。计算机语言相对于生物学分类,它只有类的概念,但它提供了继承,即豹可以继承自动物,所以在计算机语言中同样可以提供如生物学分类中那样复杂的从属关系。

类中有类似变量和函数的概念,习惯上被称为属性和方法,以区分我们上面谈到的变量和函数。

实际上,类在计算机里并不是必要的。在一些古老的计算机语言里,甚至都不存在类这个概念。

使用类来抽象现实世界的编程方式,又叫做面向对象编程。而不使用类,仅使用函数和变量的编程方式,叫做面向过程编程。前者更接近人类的思考模式,但需要我们掌握更多的概念。

我们之前在操作表格文件时,用到过一个openpyxl包。它是一些类和函数文件的集合。

为了更精确地描述,我们有时会把包称作“库”。我们通常会把从网上找到的库叫做第三方库。

那哪些是第一方库和第二方库呢?有点意思,哈哈。

我们同样可以把单个文件作为类和函数等的集合。通常我们会把单文件的包叫做模块,比如之前的示例:

1
2
3
import os

print os.name

为了演示方便,我们使用了一个随python语言打包的模块,叫做os。它实际上就是一个模块。

碎碎念

想要装一个完美的x,熟悉并掌握这些概念是必须的。

不过不必急于求成,就算我们不掌握这些概念,对我们的学习也没有太大影响。

有很多优秀的东西,我们是可以潜移默化地吸收的。可能突然有一天,我们回头时猛然发现,以前不明不白的概念,竟然已经被我们彻底理解了。

在那之前,我们可以继续愉快地叨叨。

唠唠python(8) -- 使用脚本代替手工数据操作

这个问题已经过了很多天了,都记不太清细节了。

先捋一捋我们的需求。

需求

待处理表格

这个表格有点熟悉,好像在之前的文章里看到过。我们还是以它为例子。

我们有一张如上的物料订单表格。每两行对应一种物料,即需要的料数和订购的料数。不同的列对应不同时期需要的物料数量。

因为供应商提供给我们的物料都是以10000为单位的,所以就算我们只需要9颗料,也要订购10000颗料。也就是以10000为单位,对料数上取整。

我们看到其中有一些表格的数量是红颜色,它表示这些数据已经被手工填写了。每个单元格的料数以10000为单位上取整后,被填入它下面的一个单元格。

计算公式

我们在之前的文章里已经给出了订单数量的计算公式:(n+10000-1) / 10000 * 10000,其中n为需要的实际数量。

功能清单

我们列一下需要的功能清单:

  1. 打开或保存excel文件
  2. 读取或写入单元格

再不需要其它的功能了。

上一篇讲到的openpyxl很好地实现了我们需要的这些功能。

打开excel使用workbook = openpyxl.load_workbook("excel文件位置"),保存使用workbook.save("保存到的路径")

读取某个单元格使用print sheet["B5"].value,写入某个单元格使用sheet["B5"] = 30000。其中sheet是我们使用workbook.active获取的工作簿里的第一张表格。

几点逻辑

我们需要对上面的表格中,I3到S28的奇数行数据查看一遍,还需要根据单元格的内容和它下面一个单元格的内容来判断是否需要处理单元格数据。

挨个查看一遍我们会用到一个这样的语句:

1
2
for row in range(3, 30, 2):
处理row行数据

这条语句的意思是,row分别等于3到28(不包含28)的奇数,然后依次执行下面的语句。后面的2是说,从3开始,取相隔为2的数。

这里有一点需要注意,我们的行号是数字表示的,但是列号是字母,我们需要先把字母转换成数字,在处理每一列时,再把数字转换成字母。庆幸的是,python已为我们准备好这些了。

1
2
3
4
for row in range(3, 30, 2):
for col in range(ord("I"), ord("U")+1, 1):
col_char = chr(col)
处理(row, col_char)单元格数据

ord()是将一个字母转化为数字,chr()是将数字转化回字母。注意我们给ord(“U”)加了一个1,因为ord(“U”)是不包含U的。我们再一次与python的作者来了一场完美的合作。

判断是否需要处理单元格,我们可以使用下面这条语句:

1
2
if sheet["I3"].value is not None and sheet["I4"].value is None:
处理I3单元格

意思也很好懂,翻译过来就是,如果I3单元格不是空的,并且它下面的I4单元格是空的,我们就处理I3单元格,并将处理结果填充到I4单元格。

实现脚本

说了这么多,其实脚本已经差不多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import openpyxl


workbook = openpyxl.load_workbook("a.xlsx")
sheet = workbook.active

def count_bill(n):
return (n+10000-1) / 10000 * 10000

for row in range(3, 30, 2):
for col in range(ord("I"), ord("U")+1, 1):
col_char = chr(col)

cell_src_idx = col_char + str(row)
cell_dst_idx = col_char + str(row + 1)

if (sheet[cell_src_idx].value is not None and
sheet[cell_dst_idx].value is None):
n = int(sheet[cell_src_idx].value)
sheet[cell_dst_idx] = count_bill(n)

sheet.save("b.xlsx")

14行代码,就实现了我们的需求。它读取当前目录下的a.xlsx文件,并根据我们之前所说的规则,处理指定范围内的表格数据,并将处理后的结果保存到一个新文件b.xlsx。

它真正将我们从枯燥无味的繁重工作中解放了出来,终于可以安心地睡个好觉了。

更好的脚本

当然,它还不够好,我们在这篇文章里不打算再把它变得更好,因为这个过程是很漫长的。但是它现在已经能帮我们节省比付出多几十倍甚至几百倍的时间。

它不够好,还能做怎么样的改进呢?

第一,里面全是代码,代码相对于人类自己的语言来说总没那么直观,毕竟我们是在迁就计算机嘛。

第二,每次执行时都要修改代码(输入输出文件名和单元格范围),十分之不友好。我们希望它可以让我们通过文件选择框选择某个文件,然后用鼠标拖选出我们要处理的范围,并询问我们要保存到哪个文件。

以上两点是显而易见的,特别是第二点。

不过最紧急的事情完成了,这个脚本已经可以帮我们节省下大量时间,以实现我们上面所说的改进了。

总结

终于把处理表格的傻逼工作交接给了计算机,可以舒一口气了。

为了不加重我们的心理负担,文章里有很多地方只是一带而过。至少对现在这个阶段来说,不需要知道的东西都是不重要的。如果哪里感到困惑,恭喜,这说明我们并不满足现状,想要一个更清晰的结果。

下一篇,我们先不急着改进上面说的两点不足,它们已经不那么紧急了,我们先来回顾一下之前提到的内容,并重新整理一下知识结构,让他们形成一个清晰的脉络,以便为以后装逼打下坚实的基础。

叶脉

碎碎念

零零散散地终于把这个脚本完成了,是时候回过头来梳理一下了。

小金约定,两天或两天以上的假期,每天一篇文章,失约100红包。本是想逼迫自己,竟然有时候写的不易乐乎,到了凌晨2、3点钟还在想读者会不会读懂这句话在说什么。

作者从读者的角度思考,读者又反过来从作者的角度思考,想想真的是一件很有趣的事情。

不得不说,写作的过程很快乐,虽然可能没有什么人看,但它至少已经帮助到了我。希望可以尽自己的一点微薄之力,让碰巧看到的人能生活得更轻松。

唠唠python(7) -- 包

这是我们打基础的最后一节。下一节,我们就来兑现之前许下的承诺,用脚本来实现表格的自动处理。

这里所说的包就是《你的背包》的包,是对背包概念的一种模拟。

这两天小郭去武汉玩,为了途中更方便一点,我就把我的背包给她用了(其实是我一直在用她的背包😁)。我担心公司有一些事务要处理,所以一直都保持着背笔记本回家的习惯。最近几天都是手拿笔记本,十分的不方便。

所以你看,包还是很重要的。

我们一般会把一些平时经常用到的物品放到书包,如果我们有多个包,则会把同类物品放到同个包。比如我们把纸笔放进背包里,只要背着这个背包出去,就可以在需要的时候写字了,而且背在肩上很方便。

python里的包也是类似的用途。

python中的包

互联网的兴起,让全球的人都可以有机会合作,大大提高了人们的工作效率。

实际上,python中的包还有另外一个异常重要的作用:复用。比如,我们之前说的表格处理的例子,世界上有很大一部分人都会用到表格处理的功能,如果大家每人做一套工具来操作表格(比如读取或写入表格中的某个单元格),成本很高,不利于人类知识的叠加。有句话叫站在巨人的肩膀上,说得很有道理。

那怎么做呢?

把我们的功能合集打个包,而且只把人们要用的东西提供出来。举个例子,我们要读取某个单元格的数据(比如我们要读取B5单元格的数据),只需要sheet["B5"].value就可以了,具体怎么获取的,交给那个精通表格文件处理的人就可以了,我们不需要理会。

背包很好理解,但python中的包听起来就有点玄乎了。python中的包到底是什么东西呢?或者说,它的外在表现是什么呢?

我们之前把洗衣服的操作封装成一台洗衣机,叫做功能。又把人的说学逗唱的功能和人的姓名等属性封装成一个人类的概念,叫做类。包其实就是一个功能和类的合集,也就是说,它里面既包含有功能,又包含有类。说白了,其实就是把我们之前所有的代码放到一块。

使用包

之前我们讨论类的时候,自己亲自写了一个类来更深入的体会类的概念。

现在我们使用包,再来亲自手写一个包?

这次我们不写了,我们之前谈到封装,其实就是把细节隐藏起来,而让一个东西更好用。比如一台洗衣机,我们只需要学会按按键就可以了。如果我们读了洗衣机的使用说明还不知道洗衣机怎么用,只能说这台洗衣机做得太烂了,或者说交互太差了。

我们直接来使用一个包体验一下:

1
2
import os
print os.name

执行上面两条命令后,会显示当前系统的名称。如在mac下,会显示”posix”。它是怎么获取的,我们并不关心,只要返回的名称正确,我们就认为,嗯,小伙子干得不错。

import意思是使用,我们请求使用os包,并要求显示我们os的name。

读取表格文件

我们已经来到了这里:读取表格文件。

只要能用语言精确表达出来的功能,大部分都很简单,而且非常有可能已被其他人实现了。表格处理就是这样。

现在有一个问题,既然这个包有可能已经被实现了,那我们怎么获取这个包呢?

我们知道肯定不止一个包可以做这种操作。一般我们会在网上搜索大量资料,然后对比选取符合我们需求的包。

假设我们已经阅读并对比了大量资料,发现了一个可以读写表格文件的库,叫做openpyxl

现在我们要准备安装这个包了。这是一个最好的时代,只要确定了使用哪个包,安装极其简单,我们甚至都不用关心去哪里下载,下载完成后要做什么,它会不会依赖其它的包,等等等等这些问题。python已为我们准备好了安装的方式。

1
$ pip install openpyxl

是的,就是这么一条简单的命令。然后稍许的等待,这个包就安装成功了。真的方便极了,美好的东西总能让我们心情愉快。我们甚至也不用关心它被安装到了哪,直接用就可以了。

想想如果我们用表格处理软件查看B5单元格的数据。先打开目标表格文件,然后翻到第一张表格(一个表格文件对应一个工作簿,里面可能有多张表格),找到B5单元格,并查看内容。

1
2
3
4
5
6
import openpyxl

workbook = openpyxl.load_workbook('a.xlsx')
sheet = workbook.active

print sheet["B5"].value

就这么简单的几行,就把B5单元格的内容显示出来了。

结语

现在我们已经可以获取表格中的某个单元格了,写入跟获取是同样简单的。下一篇我们来看如何写入,以及如何来实现我们之前的物料取整问题。

终于可以不用对着成百上千的数据心力交瘁,提心吊胆了。可以有更多时间喝咖啡了。想想好激动。

碎碎念

晚上9点30,写完了,可以去吃饭了,哈哈。

老板来一个煎饼果子,一盒水果,一个鸡腿。街边小吃让人愉悦。

想起申总的一句话:不吃街边小吃的人,不知道要丧失多少乐趣。