乐者为王

Do one thing, and do it well.

使用Scrapy爬取小说(5)

今天的任务是重构TxtPipeline。

先看下TxtPipeline中负责写文件的代码片段:

1
2
3
4
f = open(filename, 'w')
f.write(title)
f.write(content)
f.close()

我们经常会看到这样的代码,但它存在严重的问题,你能把它找出来吗?

首先,open函数打开文件,并返回文件句柄,该句柄是由操作系统分配的。接着就是调用write方法写文件。最后是调用close方法关闭文件。文件使用完毕后必须关闭,因为文件句柄会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的。

由于文件读写时都有可能产生IOError,一旦出错,后面的close方法就不会被调用。所以,为确保无论是否出错都能正常地关闭文件和释放文件句柄,我们需要使用如下方法来实现:

1
2
3
4
5
6
7
try:
  f = open(filename, 'w')
  f.write(title)
  f.write(content)
finally:
  if f:
    f.close()

但每次都这么写实在太繁琐。其实,我们可以使用with语句来帮我们自动管理文件资源:

1
2
3
with open(filename, 'w') as f:
  f.write(title)
  f.write(content)

这和前面的try-finally是一样的,但是代码更加简洁,而且不必调用close方法。

使用Scrapy爬取小说(4)

使用Scrapy爬取小说(1)中,我们使用range(1, 310)来确定章节链接的范围,这很不好。我们编程的时候应该要尽量注意减少代码中的硬编码和魔数,提高代码的可移植性。如果小说章节的链接不是这种连续的数字,或者章节的数量是在逐步增加的,那么这段代码就是无效的,或者会慢慢变得无效。

如何才能把这段代码写得更具可移植性呢?

novel-chapter-urls

我们不必关心章节链接的格式,我们只要知道它是个链接,一定是以<a href="url">text</a>这种形式呈现(如上图所示)。我们也不必关心章节数量是否变化,只要把所有这种形式的链接抓取下来即可。与此对应的XPath表达式是:

1
//center/table[@bordercolorlight]//a/@href

因为页面文档中可能有多个表格,所以要在table后面添加@bordercolorlight属性来指定我们要查找的那个。

那么在Scrapy中如何实现这样的能力呢?以下是具体的代码:

1
2
3
4
5
def parse(self, response):
  links = response.xpath('//center/table[@bordercolorlight]//a/@href').extract()
  for link in links:
    next = response.urljoin(link)
    yield scrapy.Request(next, callback=self.parse_chapter)

使用Scrapy爬取小说(3)

在前文中,我们将小说的每个章节保存为独立的文本文件。今天我们准备把小说内容输出到数据库。对于数据存储,我选择MongoDB。为什么是MongoDB而不是其它?原因是以前没用过,想尝试下。

现在我们已经知道,要把抓取来的数据保存到数据库,只需实现Item Pipeline即可。我们可以仿照前面的实现依葫芦画瓢。

以下是将小说内容保存到MongoDB的代码:

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

class MongoPipeline(object):
  def open_spider(self, spider):
    self.client = pymongo.MongoClient('localhost', 27017)
    self.novel = self.client['novel']
    self.ssjx = self.novel['ssjx']

  def process_item(self, item, spider):
    data = {
      # 标题和内容都是列表类型,必须先转换成字符串
      'title' : ''.join(item['title']),
      'content' : ''.join(item['content']),
    }
    self.ssjx.insert(data)
    return item

  def close_spider(self, spider):
    self.client.close()

将组件添加到novel/settings.py的ITEM_PIPELINES配置中以启用它:

1
2
3
4
ITEM_PIPELINES = {
  'novel.pipelines.TxtPipeline' : 300,
  'novel.pipelines.MongoPipeline' : 400,
}

在项目的根目录中使用下面的指令运行Spider:

1
scrapy crawl novelspider

如果没有问题的话,爬虫会不停地运行,小说的章节内容也会被一个个地保存到数据库。下面的截图是最终的抓取结果:

novel-mongo-gui

使用Scrapy爬取小说(2)

接上文。

将抓取的小说内容保存成本地文件是通过在命令行指定-o选项实现的。虽然工作的很好,但是有两个缺点:一是把所有小说内容保存到单个文件会导致该文件太大,用文本编辑器打开随机浏览的速度非常慢;二是小说章节不是按照顺序保存的,导致阅读指定的章节内容很不方便。

再写个小工具按章节内容分割小说文件?无需如此麻烦。我们可以在Scrapy中直接将每个章节保存为单独的文本文件。Scrapy中的Item Pipeline就是干这类事情的。看下面的Scrapy架构图:

scrapy-architecture

当Item在Spider中被收集之后,它们会被传递到Item Pipeline,这些Pipeline组件按照一定的顺序执行对Item的处理,同时也决定此Item是否继续通过,或是被丢弃而不再进行处理。

以下是Item Pipeline的一些典型应用:

  • 清理HTML数据
  • 验证爬取的数据
  • 查重
  • 将爬取结果保存到数据库中

编写Item Pipeline

编写自己的Item Pipeline非常简单,每个Item Pipeline都是实现以下方法的Python类:

1
process_item(self, item, spider)

此外,下面的方法是可选实现的:

1
2
open_spider(self, spider)  # 该方法在Spider被开启时调用
close_spider(spider)       # 该方法在Spider被关闭时调用

明白原理后,我们就可以开始编写自己的Item Pipeline。以下就是将小说的每个章节写成单独文本文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TxtPipeline(object):
  def process_item(self, item, spider):
    # 标题和内容都是列表类型,必须先转换成字符串
    title = ''.join(item['title'])
    content = ''.join(item['content'])
    # 使用章节名来创建文件
    # 使用strip()来过滤非法字符r'\/:*?"<>|'
    filename = '{}.txt'.format(title.strip())
    f = open(filename, 'w')
    f.write(title)
    f.write(content)
    f.close()
    return item

启用Item Pipeline

要启用Pipeline组件,你必须将它添加到novel/settings.py的ITEM_PIPELINES配置中,就像下面这样:

1
2
3
ITEM_PIPELINES = {
  'novel.pipelines.TxtPipeline' : 300,
}

Pipeline后面的整数值确定它们的运行顺序,Item按数字从低到高通过每个Pipeline。通常将这些值定义在0-1000范围内。

运行Spider

在项目的根目录中执行如下的命令(因为不再把所有的小说内容保存为单个文件,所有不需要指定-o选项):

1
scrapy crawl novelspider

没有报错的话,等个几分钟,就能看到很多文本文件躺在自己的电脑上面。

novel-txt-list

使用Scrapy爬取小说(1)

这几天正在看《Python网络数据采集》,在这过程中觉得有必要写个爬虫来实践学到的知识。便给自己定个小目标:试着用Scrapy爬取小说《蜀山剑侠传》,并把内容保存到本地文件中。

Scrapy是一个开源的Python数据抓取框架,速度快且强大,而且使用简单,可以很方便地抓取网站页面并从中提取结构化的数据。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。

好吧,废话不多说,让我们直接开干!

创建项目

在抓取之前,必须先创建一个Scrapy项目,可以直接用以下命令生成:

1
scrapy startproject novel

这是新建项目的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
.
├── novel                # 项目模块
│   ├── __init__.py
│   ├── items.py         # 定义爬取的数据
│   ├── middlewares.py   # 定义爬取时的中间件
│   ├── pipelines.py     # 定义数据管道
│   ├── __pycache__
│   ├── settings.py      # 项目的设置文件
│   └── spiders          # 放置爬虫代码的文件夹
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg           # Scrapy部署时的配置文件

分析页面结构

主要分析两个页面。一是小说的目录页面,目的是获取小说所有章节的链接以备抓取。二是任意章节页面,用于爬取其中的标题和正文。

通过观察目录页面的源码可以发现,所有章节的链接都类似NUMBER.htm。其中,NUMBER是3位整数,从001到309。

novel-chapter-urls

使用浏览器的检查器(Inspector)查看章节页面,尝试把光标放在正文上,你应该可以看到正文周围的蓝色方块(如下图左侧所示),如果你点击这个方块,就可以选中检查器中相应的HTML代码。可以看到小说的标题和正文都在td标签中。

novel-page-inspector

与此对应的XPath表达式分别是:

1
2
//center/table/tr[2]/td/text()  # 标题的XPath路径
//center/table/tr[4]/td/text()  # 正文的XPath路径

需要注意的是,上面XPath表达式里的中括号内的数字为节点索引,是从1开始的,而不是0。

定义爬取的数据

当需要从某个网站抓取信息时,首先是定义我们要爬取的数据。在Scrapy中,可以通过Item来完成。以下是我们定义的Item:

1
2
3
4
5
import scrapy

class NovelItem(scrapy.Item):
  title = scrapy.Field()
  content = scrapy.Field()

编写爬取数据的Spider

现在我们需要添加一个爬虫来真正做点什么。创建文件novel/spiders/novel_spider.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
import scrapy
from novel.items import NovelItem

class NovelSpider(scrapy.Spider):
  name = 'novelspider'
  allowed_domains = ['example.com']
  start_urls = ['http://example.com/wuxia/hzlz/ssjx/']

  def parse(self, response):
    # 还记得前面分析目录页面时的结果吗:000、001...309。
    for i in range(1, 310):
      # 生成每个章节的绝对链接
      next = response.urljoin('{0:03d}.htm'.format(i))
      # 生成新的请求对象解析小说的标题和正文
      yield scrapy.Request(next, callback=self.parse_chapter)

  def parse_chapter(self, response):
    item = NovelItem()
    title = response.xpath('//center/table/tr[2]/td/text()').extract()
    print('Title is', title)
    content = response.xpath('//center/table/tr[4]/td/text()').extract()
    print('Content is', content)
    item['title'] = title
    item['content'] = content
    return item

运行Spider

完成爬虫后,如何通过它来得到我们想要的结果呢?在项目的根目录中执行如下的命令:

1
scrapy crawl novelspider -o novel.json

没有报错的话,等个几分钟,就能看到一个完整的JSON数据文件躺在自己的电脑上面。

不过如果打开的话,可能只会看到“\uXXXX”这样的乱码,它们都是中文字符的Unicode编码。要直接显示成中文的话,需要在novel/settings.py中添加以下设置:

1
FEED_EXPORT_ENCODING = 'utf-8'

最终的结果如图:

novel-json-chinese

软件版本号的思考

software-version

产品的名称用来表明产品目的,通常在产品的整个生命周期中都使用它。软件产品的版本号则用来表明产品在特定时间段内所拥有的功能集。它们关注的是以何种方式对提供给用户的软件版本进行识别。

版本号用于捕获相关产品修订和变种(通常与可移植性、国际化或者性能特征相关)的信息。目标是采用尽量少的标识符去收集所有信息。遗憾的是,在工业生产中还没有一种标准的版本编号机制,或者说,不存在单一的、通用的算法。更多情况是,需要你针对发布的内容和目标群体做些细微不同的调整。

但不管你的目标群体是什么,完全发布最好通过以下方式进行标识。这是经过不断地反复实践,被各种封闭、开源软件所广泛使用的惯例。

  • 软件的名称。
  • 用x.y.z.build四位元组去捕捉修订信息。
  • 根据需要生成的任意变种信息。

版本编号中的元组定义:

元组 定义
x 主版本号。表示产品的当前主要版本。用来表示提供给客户的产品功能的主要增强。在一个极端的例子中,主版本号的增加用来说明产品现在已经拥有一个全新的功能类。
y 次版本号。表示给产品新增了一些特征,或者是在原来文档中描述的特征上做了重要的修改。用来确定次版本号什么时候需要修改的一个衡量标准就是产品功能说明书。
z 修订版本号。用来表示给产品所做的缺陷维护行为的等级。产品缺陷是在产品的功能说明书中没有定义,并且已经或者可能对产品的使用者造成不利影响的任何行为。缺陷维护可以看作是支持该版本功能说明的一切活动。
build 构建版本号。一般是编译器在编译过程中自动生成。

除z和build的意义比较明确外,对x和y的解释都太笼统。我们需要更加详细的说明以指导我们的开发工作。

修订过的版本编号中的元组定义:

元组 定义
x 主版本号。用于有扩展性的、客户可见的架构上或特性上的改变。以一个管理大型数据库的系统为例,你可能在以下情况时需要定义一个主要的版本发布:
* 改变数据库的结构,导致单纯的升级系统对客户产生比较严重的影响。
* 改变已经发布的API,导致它与前一版本不兼容。
* 删除功能(好的架构师应该删除一些不需要的功能)。
* 持续增加新的功能,例如对新的操作系统的支持。
x的增加也可以是出于纯粹的商业理由。例如,客户的技术支持合同标明,在下个主要版本发布以后,软件可以得到18个月的技术支持。通过增加x,你将强迫客户去升级。
y 次版本号。通常与期望的功能或其它改进措施相关。当市场部门认为这个版本的一系列特性已经通过证实,次要版本号就会增加。决定增加x或y可能会比较随意。市场架构师应该定义触发任何一个增加的事件(定义与x相关的触发事件比定义y要更容易)。
z 修订版本号。主要版本号和次要版本号都相同的维护版本应该彼此兼容。
build 构建版本号。一般是编译器在编译过程中自动生成。

注意:这里的版本编号规则仅适用于发布周期较长的软件产品。如果发布周期很短,像Chrome和Firefox那样,可能就不太适用。

参考资料

  • 语义化版本
  • 《软件发布方法》
  • 《超越软件架构:创建和维护优秀解决方案》

追逐时髦的技术

英文原文:https://www.nemil.com/musings/shinyandnew.html

有关当前最好的框架或编程语言的争论经常发生在Web开发中。就这点而言,Scribd的联合创始人Jared Friedman在2015年写了一篇文章推荐创业公司使用Node.js代替Rails。

他提出几个关键点:

  • Rails很慢。
  • 黑客学院的毕业生都在使用Rails,贬低了它对高级工程师的价值,并减少了它的未来前景。
  • 创业公司应该使用那些前瞻性工程师今后将使用的技术,以保证它们的应用不过时。
  • 在Scribd,过去几年里它们已经从Prototype转换到jQuery,再到CoffeeScript,再到Angular,再到React。

Node.js是创业公司的绝佳选择,但它饱受批评的两个部分令我担忧。首先,一名创业公司的工程师应该了解什么技术将会在几年后流行,以保证它们的技术栈不过时。第二,杰出的软件工程师将被时髦的技术栈吸引到创业公司,而不是有趣的技术问题。在过去我还听到过更恶劣的传闻,创业公司的开发者拒绝接受使用ES5 JavaScript编程的工作(那时CoffeeScript刚出来),Mongo发布不久工程师就执意在生产环境下使用Mongo替代Postgres,渴望用最新的前端框架不断重构项目。

我担心有些程序员(和他们的雇主)有这种倾向,即把注意力放在转换技术栈到最新上。他们主要基于框架选择公司,力求在工作中使用最新而不是最好的工具。他们把时间花在新的库和框架上,而不是提高他们的核心技术能力。我们把他们称为技术栈追逐者——他们奋力追求在创业公司的技术栈中使用那些对核心输出(用户重视的软件功能、开发团队的生产力)提升有限的新技术(或者他们自己喜欢的技术)。

“时髦的”Web开发

很同情那些在Hacker News上的时髦的Web或移动应用开发者。作为在2012年的全栈创业公司的开发者,你正在构建后端使用Ruby/Rails,前端使用Backbone/CoffeeScript/Underscore的网站,同时使用Capistrano(或相关的Python类似物)部署你的应用。到2013年,你已经将后端转换到Node/Express/Mongo,前端为Grunt/Ember。在2014年,你已经彻底切换到MEAN技术栈,但在尝试过Koa以后考虑转移到Go(在Express核心贡献者告别Node.js转向Go以后)。在2015年,你在后端使用Express/Go,前端使用Gulp/ES2015/React,使用React Native代替原生移动语言,并且慢慢地将系统转换为使用Docker的微服务。很快,你将会被转换到Phoenix,如果Angular 2是正确的选择也会转换过去——甚至可能创造一个Go可以工作在Android上以及开源Swift可以适合你的技术栈的世界。(我显然是夸大效果,尽管这是HN头条新闻流行什么的一个合理表示。)

有几个原因表明这可能是合理的。时髦的Web工程师需要“时尚”才能获得未来的工作或合同。雇主使用框架或语言作为过滤器,而不是测试批判性思维和技能。雇主没有意识到有实力的开发者如果有正确的支持,可以在几个星期,通常是几天内成为许多语言或者框架的专家。有时趋势是无法阻止的:Swift正在取代Objective-C,世界正在转向更薄、更小的单体后端和更重、反应更灵敏的前端。通常,转变有着巨大的优势:生产力大幅上升,或者新的用户功能突然变得可能。然而,所有的变化都不会导致早期到中期的公司不采用就死,而为了乐趣或业余项目学习技术和认为它是生产环境的关键是迥然不同的。

我们可以用创业公司的时髦的Web或移动开发者与我们的计算机科学家作为对比。我的一个朋友是一家顶级科技公司的计算机神经学家——跟几乎所有从事技术工作的人一样,他的世界每隔几个月就会被重塑——得益于计算能力、脑成像和深度学习算法的快速发展。基本的编程工具其实变化不大。公平地说,只有C++从 11转换到14引起了一些焦虑。还有分布式计算系统、键/值存储和其它外部服务,但这些都是使用稳定的API构建的。他的大部分时间都花在单个DSL中的架构和算法上,而不是重写功能相似的代码或者快速学习提供有争议的好处和改变的库。

选择工具

人们可能会建议创业公司选择时髦的技术栈,因为它是招聘杰出的工程师的关键工具。我自己的观察是,杰出的工程师注重其它的东西。到目前为止,最重要的是提供有趣的问题去解决——有趣的人与他们合作。吸引力和强大的使命感是吸引优秀人才(工程师或者其他)的其它途径。

我并不是在抱怨技术发展太快,也不是说我们都应该用汇编语言或者C++或者Ruby编程。软件工程师清楚他们的目标——我们的领域以令人目眩的速度发展,但对于我们拥有的影响力这都是值得的,因为有10亿人上网。我认为你需要有能力快速地学会新的框架、语言或库(如何完成它的Ask HN)——依靠周围那些经验丰富的工程师,你的目标应该是尽快地具有生产力。除此之外,你应该深刻理解多种语言,而不仅仅是一种(但是同样的态度,不应盲目地扩展到框架或者轻量级的DSL)。

对于创业公司而言,Paul Graham在2013年被问到关于理想的语言:“我的意思是,我们有的创业公司在用PHP编写代码——这让我有点担心,但这并不像其它事情那么让我担心。”GitHub的技术主管Sam Lambert在最近的一次采访中谈到,他在2013年被GitHub的CTO面试时,对GitHub的技术栈是Rails、C和Bash脚本感到惊讶:“随着面试的继续,我发现他们实际上是一群非常务实的黑客,他们只钻研Ruby和C,使用更稳定的技术栈以便花时间工作在更有趣的事情上,而不是追逐最新最酷炫的技术。”GitHub的方法在我看来是Web和移动开发者的合理的平衡:广泛地探索工具,然后务实地选择解决你所面临的问题的工具(YAGNI适用于更多的地方,而不仅仅是面向用户的功能开发)。

令我担心的是,某些开发者,特别是在职业生涯早期的开发者,可能会以为创业公司的工程师不是问题解决者或计算机科学家,而是一个荣誉查找表——他们的任务是每隔几个月记住一个新的DSL——只能获得有限的好处。这使我们这些早期的工程师贬值——构建人们想要的东西,从事有趣的技术问题,快速交付代码。

无论如何,要在额外的时间里广泛地实践。如果好处是压倒性的,则切换生产环境中的语言/框架,但要考虑是哪些好处。警惕那些追求新技术却不考虑它对团队的预期优势的人。花时间学习概念和解决有趣的技术或用户问题。如果你有正确的应用边界,并选择你有现成生产力的框架,一旦你这样做了,你将具有一定的灵活性,但需要足够坚持才能达到产品与市场的匹配和超越。

任何一天打开Hacker News,你都能看到有帖子诱惑你使用某个框架、语言、类库或者服务去贡献和构建应用(包括一些像Mongo这样有大笔现金的公司,因此在它们的平台后面有营销预算)。有些工具拥有改变游戏规则的能力,其余的只有一些关键的不同功能,但是它们都需要时间才能成为专家。有些工具会大声宣告它们才是未来,并且嘲笑你所学到的东西——但是它们需要你的技能和意识与现有的技术真正地竞争。你会如何选择?

checked/unchecked应该翻译成什么?

翻译有关Java异常的文章时,总是犹豫是否该把checked/unchecked也翻译过来。原因是,不是很清楚该如何优雅传神地翻译这两个单词。

《Java核心技术》将它们翻译成“已检查/未检查”。《Java编程思想》和《Effictive Java中文版》则翻译成“被检查的/不检查的”。至于技术文章的翻译更是花样百出,有“检测/非检测”、“可检测/非检测”、“可查/不可查”、“受查/非受查”、“检查型/非检查型”、“检查/非检查”等。

到底该翻译成什么呢?在回答这个问题前,让我们先确定什么是checked/unchecked异常?

exception-hierarchy

上图是Java中的异常层次结构图。Java语言规范将派生自RuntimeException类和Error类的所有异常称为“unchecked异常”,其它的异常称为“checked异常”。

The unchecked exception classes are the run-time exception classes and the error classes.

The checked exception classes are all exception classes other than the unchecked exception classes. That is, the checked exception classes are Throwable and all its subclasses other than RuntimeException and its subclasses and Error and its subclasses.

并且,在编译时编译器会检查程序是否为所有的“checked异常”提供处理器。

This compile-time checking for the presence of exception handlers is designed to reduce the number of exceptions which are not properly handled.

从上述的描述可以得出,“checked异常”和“unchecked异常”是两种异常类型,且“checked异常”隐含有必须要检查的思想。

紧紧围绕这些描述,细细地思考和比较,个人认为:1. 《Java核心技术》的翻译存在问题,“已检查”和“未检查”说明的是异常的检查状态,没有表达出异常的分类这个概念。2. 《Java编程思想》和《Effictive Java中文版》的翻译则正确地表达了异常的分类,但“被检查”翻译的有点无厘头,如果能改成“要检查”则会更好,缺陷是连接“异常”这个词组后是短语,而非名词,读来费劲,也不上口;如果去掉“的”的话,后者会有歧义,听起来像是命令。3. “检测/非检测”和“检查/非检查”是同个意思。4. “可检测”这个翻译看上去似乎表示异常是可以检查的,和Java语言规范要求的该类异常必须要检查不符。5. “可查/不可查”也是如此。6. “受查/非受查”的翻译则有些莫名其妙的感觉。7. “检查型/非检查型”翻译的很好,既表达了异常的分类,也表达了一种异常是要检查的,另一种异常是不要检查的意义,只是前者还缺少点强制的意味。

分析到这里,结果已经是不言而明。“要检查的/不检查的”和“检查型/非检查型”是两种更好的翻译,都能把Java语言规范对checked/unchecked异常的描述尽量地表述出来。而后者在实际使用中更为简洁适宜。

接下来的事情就是把以前译文中未翻译的checked/unchecked修改成“检查型/非检查型”。在以后的翻译中也继续使用这个翻译结果,除非能找到更好的表述方式。

10x工程师经常的打盹

英文原文:https://hackernoon.com/10x-engineers-take-long-naps-ed2ca00a953

最近,我在为一个疯狂的悖论而烦恼:我感觉从来没有像今天这样工作效率高过,然而我也注意到在给定的每周内我工作的时间并没有减少。这让我想起另一个令人困惑的悖论,我在成功的软件公司看到,它们重视那些不足以令人信服的10x工程师传说,但同时也重视那些花费很多个人时间并确保避免倦怠的员工。

我对这些最近我给予它们很多激烈思考的悖论感到困惑,但我倾向于认为我已经得出结论,该结论可以很好地解释这两个似乎对立的观点。

之所以用“似乎”这个词,是因为我认为它们其实并不算是真正的对立面。简而言之,我的看法是,虽然我们使用相同的词汇谈论工程师的效率,但我们谈论的是两种截然不同的效率:

  • 一方面,效率是指在最短的时间内交付最多的产出。这个可以通过检查软件编写的数量(提交次数、代码行数等)或与软件相关的工作(文档页面、可交付的成果等)进行衡量。更长更多的工作时间意味着更多的产出。
  • 另一方面,效率是指用最短的时间和精力产出最多的业务价值。这个可以通过利益相关者(客户、同事等)能从工程师的工作中的获得多少实际的货币化/有用的价值进行衡量。就像我一直在说的,软件工程师的职责不是解决技术问题,而是用技术方案解决业务问题。

混乱可能源于在大多数工作中,投入和产出的关系在一定程度上是呈线性的。最简单的例子:如果你的工作是工厂产品的日常生产,你工作的时间越长,生产的产品就越多。更多的工作时间=更多的价值。但事实是,在我们的工作中,这些可以变得非常无关,我甚至可以冒昧地说,有时候它们甚至是相互成反比的。

它们可以是不相关的,因为你可以被某个疯狂的问题困扰整周的时间,捶胸顿足,感觉就像你没有带来任何价值,直到你把事情弄清楚,并且在10分钟内真正地解决这些事情(因此终于将业务向前推进)。或者,仅仅因为不同的心态,你可以花1个小时自动化某个过程,这将使其它问题过时,同时给另一个团队每周节省几个小时。或者你可以突然想到在结帐渠道中引入某个变更,它会使你的公司的转化率提高1%,因此可以提高数百万的收入。

它们有时候也可以是成反比的(更多的工作时间=创造更少的价值),因为如果以“创造业务价值”为KPI,你的生产效率将取决于很多难以掌控的未知因素。如果你遇到这样的情况,花费整天的努力去尝试解决某些问题,然后经过充分的休息后在第二天早上回来,并在5分钟内诊断出问题,你可能会同意充分的休息是主要因素。

“10x工程师”和“快速行动”

所以让我们回到“10x工程师”这个话题。在软件层次上,有更快速的工程师能完成其他工程师10x的代码这个概念在我看来是非常不切实际的。但是,如果你正在考虑的KPI是创造实际的业务价值,这听起来就不那么牵强。我可以告诉你的是,目前正在从事技术问题的一些工程师几乎不能给他们的公司增加任何业务价值,他们解决技术问题只是因为它们是有趣和富有挑战性的,工程师们太缺乏经验或者不够价值驱动,以至于不能认识到他们为什么跑偏;他们的领导技术太弱,以至于不能理解这项工作可能应该被重新调整优先级。相比之下,如果以业务价值为KPI,业界所有其他的工程师都是数学上无限的工程师!

此外,通过以业务价值为KPI,“快速行动和推陈出新”就可以被理解为“在最短的时间内优化你的业务价值能力并达到目标,不要太过满足于已经创造的价值,它们可以优化的更好”。如果“优化你的业务价值能力”意味着你需要减少每天的工作量,并获得充分的休息,以便能够产出更多的价值,那么即使要达到目标也不意味着会给你带来更多的压力,或者是大量的工作时间。

行业现状

诚然,并不是所有的公司都同意我的理解。一个极端是,一些公司(Uber、Apple等)希望顶尖的工程师能够每天都全力以赴,并最大化他们的日常产出,以达到所需的业务价值。另一个极端是,其它一些公司(Facebook、Salesforce等)坚决要求工程师需要知道什么时候应该放慢速度,在必要的时候减少工作时间,以更聪明和更周全的方式工作,因为这些公司认为它们的工程师将因此做出更好的选择,可以用更少的时间和精力建立更多的价值。

当然,由于辩论双方都有公司是成功的,所以我不认为某些公司比其它公司更正确。我的观点是:

  • 由于软件工作的本质及其在最优条件下的潜在价值乘数效应,后者公司提出的方法对于员工来说既不是悖论,也不是不道德的。
  • 作为业界的工程师,你可以在这两种文化(以及两者的折中)之间进行选择,与其它行业相比,我们真的算是幸运的。

结论

回到标题:10x开发者真的都经常的打盹吗?不,不,他们不是都这样做。不过我看到很多“业务价值10x”的开发者经常这样,因为他们很清楚地知道自己的极限,他们知道何时应该切换到非工作相关的事情来触发当他们恢复工作时倍增业务价值生产效率的时刻。对于其中的某些人来说,它是经常的打盹,但对于你来说,你可能会发现它是别的东西(在正确的时间散步、快速的视频游戏以放空你的大脑、和你的小孩一起玩等)。

那么接下来该怎么做?如果你不确定如何在现实生活中应用这些想法,这里是主要的提示:注意观察你用少量代码解决大部分业务需求时都发生了什么的模式;实验你的日常例程,看看什么工作可以产生“乘数效应”;注意学习你自己的极限,随着时间的推移,优化你的健康、心态和生产效率;同样具有挑战性的是,让你自己遵循你所发现的极限。

10x开发者不是神话

英文原文:http://www.ybrikman.com/writing/2013/09/29/the-10x-developer-is-not-myth/

更新:你可以在这里找到本文的西班牙语翻译

昨天晚上,我在Twitter发布了以下内容:

我被“10x”或者“摇滚明星开发者”是神话的说法所迷惑。明星运动员、艺术家、作家,或者摇滚明星是神话?

— Yevgeniy Brikman (@brikis98) September 29, 2013

我收到了大量的回复和问题,但Twitter是个糟糕的讨论媒介,所以我写这篇博文作为补充。

有大堆的文章[1234]声称10x开发者不存在。反对的理由通常分成3种:

  1. 原来的10x数字来自单个的有缺陷的研究(Sackman,Erikson和Grant(1968))。
  2. 生产率是个模糊的事情,很难测量,所以我们不能做出任何10x的声明。
  3. 人才有分布,但没有单个工程师可以做10倍的工作。

我不同意所有这些。让我们逐个地检查这些理由。

这不是一个研究

虽然Twitter和Hacker News上的空谈科学家喜欢严厉谴责已被同行评审的研究,但在那种情况下的证据是相当有吸引力的,不仅限于单项研究。请允许我引用这个问题在Stack Overflow上的讨论的最顶部的回复:

...发现个体编程生产力的巨大差异的原始研究是在20世纪60年代末由Sackman、Erikson和Grant(1968)进行的。他们研究了平均7年经验的专业程序员,发现最佳和最差程序员之间的初始编码时间的比例约为20:1;调试时间超过25:1;程序大小5:1;并且程序执行速度约为10:1。他们发现程序员的丰富经验与代码质量或生产率之间没有任何关系。

对Sackman、Erikson和Grant的发现的详细检查显示出其方法论的一些缺陷。但是,即使考虑到这些缺陷,他们的数据仍然显示出最佳和最差程序员之间的差距超过10倍。

在原始研究之后的几年中,“程序员之间存在数量级差距”的一般性发现已经被许多其它对专业程序员的研究所证实(Curtis 1981、Mills 1983、DeMarco和Lister 1985、Curtis等人1986、Card 1987、Boehm和Papaccio 1988、Valett和McGarry 1989、Boehm等人2000)...

想知道更多可以看这里这里

如果你不能测量它,你仍然可以推理它

即使你忽略上面的研究,并宣称“编程生产力”很难测量——它是的——我们仍然可以讨论10x程序员。只是因为某些东西难以测量并不意味着我们无法推理它。

例如,你是如何为最近的项目挑选编程语言的?你有没有查阅“证明”这门语言比其它语言更有效的研究?就个人而言,我不需要实验来证明,Ruby比起C在构建网站时会是有一个数量级的更有生产力的选择。你可以胡乱拼凑些粗略的指标(库可用性、社区支持、文档),但现实是,大多数人基于直觉推理而不是双盲研究来做出这类语言决策。尽管缺乏过硬的数据,但我敢打赌,大多数时候,采用Ruby而不是C进行网站开发都将是正确的决策。

当然,编程不是唯一这样的事情:什么“指标”可以告诉你一位作家、艺术家、老师或者哲学家比另外一位更好?仅仅观察他们,我不能给出表明莎士比亚、纳博科夫或奥威尔比普通作家好一个数量级的“生产力指标”,但是绝大多数人都会同意这点。

编程不是体力劳动

抗拒10x程序员的最大问题是有些人认为编程是手工劳动,而程序员是装配线工人。有些程序员比其他程序员好些,但可以肯定的是,单个程序员不可能持续接近其他程序员的10倍!10个人的团队总是胜过单个编码者!9个女人不能在1个月内生产婴儿!

上面的逻辑听起来像是编程生产力只关乎打字速度。仿佛10x程序员只是能够生产平均水平的10倍代码。这种推理无视编程是创造性行业,不是体力劳动:解决同样问题的方法有很多种。停止简单地类比,更多地考虑罪案解决的类比:10名普通侦探与夏洛克·福尔摩斯。谁能更快地解决罪案?

10x开发者具有洞察力,能找到普通程序员永远不会找到的解决方案。他们将避免花费普通程序员大量时间的整类问题。编写正确代码的单个工程师绝对可以胜过编写错误代码的10个工程师。

编程是关于选择

考虑构建单个软件产品(如网站)的决策有多少:你用哪种语言?什么Web框架?你用什么数据存储?你用什么缓存?你在哪里托管站点?你怎么监控它?你如何推进新的变化?你如何存储代码?你安排什么样的自动化测试?

10个普通程序员将在每个步骤中做出“平均”质量的决策,这些决策的成本或收益将倍增。想象下流量以指数级增长,而这个普通的团队维护着普通的网站,数据存储引擎难以分片,没有足够冗余的主机,版本控制没有正确备份,没有CI环境,也没有监控。如果他们花费所有的时间去灭火,这10个编码者的效率如何?

如果程序员可以以减少一个数量级的工作量的方式对问题进行建模,那么单个程序员可以胜过这个10人团队。从多年的经验来看,伟大的程序员会知道,以后修复错误要花费更多。通过在前面作出良好的决策,10x程序员可以避免几个月的工作。

它不是编写更多的代码,它是编写正确的代码。成为10x程序员不是通过做一个数量级的更多工作,而是通过做出比平常一个数量级的更好的决策。

这不是说10x程序员根本不犯错误。程序员每天都要做出很多选择,而伟大的程序员做出正确的选择比普通程序员多得多。

编程不是唯一这样的事情。你愿意有10名普通科学家还是艾萨克·牛顿?10名普通科学家没有提出运动定律、重力理论、二项式系列、微积分等,艾萨克·牛顿做到了。你愿意让你的球队有迈克尔·乔丹还是10名普通球员(注意:乔丹获得了NBA平均薪水的10倍)?你愿意让史蒂夫·乔布斯(Steve Jobs)或者伊隆·马斯克(Elon Musk)经营一家公司还是把钥匙交给10个普通企业家?

10x程序员很少见

重要的是以正确的角度看待事情。明星的程序员、运动员、作家和科学家极其少见。我不推荐围绕只雇佣“摇滚明星”来制定招聘策略,它会使你看起来很愚蠢和孤独。不要让完美成为好的敌人:聘请你能得到的最好工程师,并给他们充足的机会去发展和变得更好。

但是,不要陷入所有程序员生而平等的谬误。任何创造性行业都有广泛的能力。一方面是会让组织陷入困境的雇员类型,用他们编写的每行代码积极增加技术债务。另一方面,有些人可以编写带来更多可能的代码,并且具有比平均水平大一个数量级的影响。