分类 软件开发 下的文章

一段超级严肃的关于样本序列化的集合、子集和超集的文字

我是一名开发者,我读代码,我写代码,我写会写代码的代码,我写会写出供其它代码读的代码的代码。这些都非常火星语,但是有其美妙之处。然而,最后一点,写会写出供其它代码读的代码的代码,可以很快变得比这段文字更费解。有很多方法可以做到这一点。一种不那么复杂而且开发者社区最爱的方式是数据序列化。对于那些不了解我刚刚抛给你的时髦词的人,数据序列化是从一个系统获取一些信息,将其转换为其它系统可以读取的格式,然后将其传递给其它系统的过程。

虽然数据序列化格式多到可以埋葬哈利法塔,但它们大多分为两类:

  • 易于人类读写,
  • 易于机器读写。

很难两全其美,因为人类喜欢让我们更具表现力的松散类型和灵活格式标准,而机器倾向于被确切告知一切事情而没有二义性和细节缺失,并且认为“严格规范”才是它们最爱的口味。

由于我是一名 web 开发者,而且我们是一个创建网站的机构,我们将坚持使用 web 系统可以理解或不需要太多努力就能理解的特殊格式,而且对人类可读性特别有用的格式:XML、JSON、TOML、CSON 以及 YAML。每个都有各自的优缺点和适当的用例场景。

事实最先

回到互联网的早期,一些非常聪明的家伙决定整合一种让每个系统都能理解的标准语言,并创造性地将其命名为 标准通用标记语言 Standard Generalized Markup Language (简称 SGML)。SGML 非常灵活,发布者也很好地定义了它。它成为了 XML、SVG 和 HTML 等语言之父。所有这三个都符合 SGML 规范,可是它们都是规则更严格、灵活性更少的子集。

最终,人们开始看到非常小、简洁、易读且易于生成的数据的好处,这些数据可以在系统之间以编程的方式共享,而开销很小。大约在那个时候,JSON 诞生了并且能够满足所有的需求。而另一方面,其它语言也开始出现以处理更多的专业用例,如 CSON,TOML 和 YAML。

XML:不行了

原本,XML 语言非常灵活且易于编写,但它的缺点是冗长,人类难以阅读、计算机非常难以读取,并且有很多语法对于传达信息并不是完全必要的。

今天,它在 web 上的数据序列化的用途已经消失了。除非你在编写 HTML 或者 SVG,否则你不太能在许多其它地方看到 XML。一些过时的系统今天仍在使用它,但是用它传递数据往往太重了。

我已经可以听到 XML 老爷爷开始在它们的石碑上乱写为什么 XML 是了不起的,所以我将提供一个小小的补充:XML 可以很容易地由系统和人读写。然而,真的,我的意思是荒谬的,很难创建一个可以规范的读取它的系统。这是一个简单美观的 XML 示例:

<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>

太棒了。易于阅读、理解、写入,也容易编码一个可以读写它的系统。但请考虑这个例子:

<!DOCTYPE r [ <!ENTITY y "a]>b"> ]>
<r>
<a b="&y;>" />
<![CDATA[[a>b <a>b <a]]>
<?x <a> <!-- <b> ?> c --> d
</r>

这上面是 100% 有效的 XML。几乎不可能阅读、理解或推理。编写可以使用和理解这个的代码将花费至少 36 根头发和 248 磅咖啡渣。我们没有那么多时间或咖啡,而且我们大多数老程序员们现在都是秃头。所以,让它活在我们的记忆里,就像 css hacksIE 6 浏览器真空管一样好了。

JSON:并列聚会

好吧,我们都同意,XML = 差劲。那么,好的替代品是什么? JavaScript 对象表示法 JavaScript Object Notation ,简称 JSON。JSON(读起来像 Jason 这个名字) 是 Brendan Eich 发明的,并且得到了伟大而强力的 JavaScript 意见领袖 Douglas Crockford 的推广。它现在几乎用在任何地方。这种格式很容易由人和机器编写,按规范中的严格规则解析也相当容易,并且灵活 —— 允许深层嵌套数据,支持所有的原始数据类型,及将集合解释为数组或对象。JSON 成为了将数据从一个系统传输到另一个系统的事实标准。几乎所有语言都有内置读写它的功能。

JSON语法很简单。方括号表示数组,花括号表示记录,由冒号分隔的两个值分别表示属性或“键”(在左边)、值(在右边)。所有键必须用双引号括起来:

  {
    "books": [
      {
        "id": "bk102",
        "author": "Crockford, Douglas",
        "title": "JavaScript: The Good Parts",
        "genre": "Computer",
        "price": 29.99,
        "publish_date": "2008-05-01",
        "description": "Unearthing the Excellence in JavaScript"
      }
    ]
  }

这对你来说应该是完全有意义的。它简洁明了,并且从 XML 中删除了大量额外废话,并传达相同数量的信息。JSON 现在是王道,本文剩下的部分会介绍其它语言格式,这些格式只不过是 JSON 的简化版,尝试让其更简洁或对人类更易读,可结构还是非常相似的。

TOML: 缩短到彻底的利他主义

TOML( Tom 的显而易见的最小化语言 Tom’s Obvious, Minimal Language )允许以相当快捷、简洁的方式定义深层嵌套的数据结构。名字中的 Tom 是指发明者 Tom Preston Werner,他是一位活跃于我们行业的创造者和软件开发人员。与 JSON 相比,语法有点尴尬,更类似 ini 文件。这不是一个糟糕的语法,但是需要一些时间适应。

[[books]]
id = 'bk101'
author = 'Crockford, Douglas'
title = 'JavaScript: The Good Parts'
genre = 'Computer'
price = 29.99
publish_date = 2008-05-01T00:00:00+00:00
description = 'Unearthing the Excellence in JavaScript'

TOML 中集成了一些很棒的功能,例如多行字符串、保留字符的自动转义、日期、时间、整数、浮点数、科学记数法和“表扩展”等数据类型。最后一点是特别的,是 TOML 如此简洁的原因:

[a.b.c]
d = 'Hello'
e = 'World'

以上扩展到以下内容:

{
  "a": { 
    "b": {
      "c": { 
        "d": "Hello"
        "e": "World"
      }
    }
  }
}

使用 TOML,你可以肯定在时间和文件长度上会节省不少。很少有系统使用它或非常类似的东西作为配置,这是它最大的缺点。根本没有很多语言或库可以用来解释 TOML。

CSON: 特定系统所包含的简单样本

首先,有两个 CSON 规范。 一个代表 CoffeeScript Object Notation,另一个代表 Cursive Script Object Notation。后者不经常使用,所以我们不会关注它。我们只关注 CoffeeScript。

CSON 需要一点介绍。首先,我们来谈谈 CoffeeScript。CoffeeScript 是一种通过运行编译器生成 JavaScript 的语言。它允许你以更加简洁的语法编写 JavaScript 并转译成实际的 JavaScript,然后你可以在你的 web 应用程序中使用它。CoffeeScript 通过删除 JavaScript 中必需的许多额外语法,使编写 JavaScript 变得更容易。CoffeeScript 摆脱的一个大问题是花括号 —— 不需要它们。同样,CSON 是没有大括号的 JSON。它依赖于缩进来确定数据的层次结构。CSON 非常易于读写,并且通常比 JSON 需要更少的代码行,因为没有括号。

CSON 还提供一些 JSON 不提供的额外细节。多行字符串非常容易编写,你可以通过使用 # 符号开始一行来输入注释),并且不需要用逗号分隔键值对。

books: [
  id: 'bk102'
  author: 'Crockford, Douglas'
  title: 'JavaScript: The Good Parts'
  genre: 'Computer'
  price: 29.99
  publish_date: '2008-05-01'
  description: 'Unearthing the Excellence in JavaScript'
]

这是 CSON 的大问题。它是 CoffeScript 对象表示法 CoffeeScript Object Notation 。也就是说你要用 CoffeeScript 解析/标记化/lex/转译或其它方式来使用 CSON。CoffeeScript 是读取数据的系统。如果数据序列化的目的是允许数据从一个系统传递到另一个系统,这里我们有一个只能由单个系统读取的数据序列化格式,这使得它与防火火柴、防水海绵或者叉匙恼人的脆弱叉子部分一样有用。

如果这种格式被其它系统也采用,那它在开发者世界中可能非常有用。但到目前为止这基本上没有发生,所以在 PHP 或 JAVA 等替代语言中使用它是不行的。

YAML:年轻人的呼喊

开发人员感到高兴,因为 YAML 来自一个 Python 的贡献者。YAML 具有与 CSON 相同的功能集和类似的语法,有一系列新功能,以及几乎所有 web 编程语言都可用的解析器。它还有一些额外的功能,如循环引用、软包装、多行键、类型转换标签、二进制数据、对象合并和集合映射。它具有非常好的可读性和可写性,并且是 JSON 的超集,因此你可以在 YAML 中使用完全合格的 JSON 语法并且一切正常工作。你几乎不需要引号,它可以解释大多数基本数据类型(字符串、整数、浮点数、布尔值等)。

books:
  - id: bk102
  author: Crockford, Douglas
  title: 'JavaScript: The Good Parts'
  genre: Computer
  price: 29.99
  publish_date: !!str 2008-05-01
  description: Unearthing the Excellence in JavaScript

业界的年轻人正在迅速采用 YAML 作为他们首选的数据序列化和系统配置格式。他们这样做很机智。YAML 具有像 CSON 一样简洁的所有好处,以及与 JSON 一样的数据类型解释的所有功能。YAML 像加拿大人容易相处一样容易阅读。

YAML 有两个问题,对我而言,第一个是大问题。在撰写本文时,YAML 解析器尚未内置于多种语言,因此你需要使用第三方库或扩展来为你选择的语言解析 .yaml 文件。这不是什么大问题,可似乎大多数为 YAML 创建解析器的开发人员都选择随机将“附加功能”放入解析器中。有些允许标记化,有些允许链引用,有些甚至允许内联计算。这一切都很好(某种意义上),只是这些功能都不是规范的一部分,因此很难在其他语言的其他解析器中找到。这导致系统限定,你最终遇到了与 CSON 相同的问题。如果你使用仅在一个解析器中找到的功能,则其他解析器将无法解释输入。大多数这些功能都是无意义的,不属于数据集,而是属于你的应用程序逻辑,因此最好简单地忽略它们和编写符合规范的 YAML。

第二个问题是很少有解析器完全实现规范。所有的基本要素都有,但是很难找到一些更复杂和更新的东西,比如软包装、文档标记和首选语言的循环引用。我还没有看到对这些东西的刚需,所以希望它们不让你很失望。考虑到上述情况,我倾向于保持 1.1 规范 中呈现的更成熟的功能集,而避免在 1.2 规范 中找到的新东西。然而,编程是一个不断发展的怪兽,所以当你读完这篇文章时,你或许就可以使用 1.2 规范了。

最终哲学

这是最后一段话。每个序列化语言都应该以个案标准的方式评价。当涉及机器的可读性时,有些 无出其右 the bee’s knees 。对于人类可读性,有些 名至实归 the cat’s meow ,有些只是 金玉其外 gilded turds 。以下是最终细分:如果你要编写供其他代码阅读的代码,请使用 YAML。如果你正在编写能写出供其他代码读取的代码的代码,请使用 JSON。最后,如果你正在编写将代码转译为供其他代码读取的代码的代码,请重新考虑你的人生选择。


via: https://www.zionandzion.com/json-vs-xml-vs-toml-vs-cson-vs-yaml/

作者:Tim Anderson 选题:lujun9972 译者:GraveAccent 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

学习一下 Python 世界里最广泛使用的 ChatOps 库:每个都能做什么,如何使用。

ChatOps 是基于会话导向而进行的开发。其思路是你可以编写能够对聊天窗口中的某些输入进行回复的可执行代码。作为一个开发者,你能够用 ChatOps 从 Slack 合并拉取请求,自动从收到的 Facebook 消息中给某人分配支持工单,或者通过 IRC 检查开发状态。

在 Python 世界,最为广泛使用的 ChatOps 库是 Opsdroid 和 Errbot。在这个月的 Python 专栏,让我们一起聊聊使用它们是怎样的体验,它们各自适用于什么方面以及如何着手使用它们。

Opsdroid

Opsdroid 是一个相对年轻的(始于 2016)Python 开源聊天机器人库。它有着良好的开发文档,不错的教程,并且包含能够帮助你对接流行的聊天服务的插件。

它内置了什么

库本身并没有自带所有你需要上手的东西,但这是故意的。轻量级的框架鼓励你去运用它现有的连接器(Opsdroid 所谓的帮你接入聊天服务的插件)或者去编写你自己的,但是它并不会因自带你所不需要的连接器而自贬身价。你可以轻松使用现有的 Opsdroid 连接器来接入:

  • 命令行
  • Cisco Spark
  • Facebook
  • GitHub
  • Matrix
  • Slack
  • Telegram
  • Twitter
  • Websocket

Opsdroid 会调用使聊天机器人能够展现它们的“技能”的函数。这些技能其实是异步 Python 函数,并使用 Opsdroid 叫做“匹配器”的匹配装饰器。你可以设置你的 Opsdroid 项目,来使用同样从你设置文件所在的代码中的“技能”。你也可以从外面的公共或私人仓库调用这些“技能”。

你同样可以启用一些现存的 Opsdroid “技能”,包括 seen —— 它会告诉你聊天机器人上次是什么时候看到某个用户的,以及 weather —— 会将天气报告给用户。

最后,Opdroid 允许你使用现存的数据库模块设置数据库。现在 Opdroid 支持的数据库包括:

  • Mongo
  • Redis
  • SQLite

你可以在你的 Opdroid 项目中的 configuration.yaml 文件设置数据库、技能和连接器。

Opsdroid 的优势

Docker 支持:从一开始 Opsdroid 就打算在 Docker 中良好运行。在 Docker 中的指导是它 安装文档 中的一部分。使用 Opsdroid 和 Docker Compose 也很简单:将 Opsdroid 设置成一种服务,当你运行 docker-compose up 时,你的 Opsdroid 服务将会开启你的聊天机器人也将就绪。

version: "3"

services:
  opsdroid:
    container_name: opsdroid
    build:
      context: .
      dockerfile: Dockerfile

丰富的连接器: Opsdroid 支持九种像 Slack 和 Github 等从外部接入的服务连接器。你所要做的一切就是在你的设置文件中启用那些连接器,然后把必须的口令或者 API 密匙传过去。比如为了启用 Opsdroid 以在一个叫做 #updates 的 Slack 频道发帖,你需要将以下代码加入你设置文件的 connectors 部分:

 - name: slack
    api-token: "this-is-my-token"
    default-room: "#updates"

在设置 Opsdroid 以接入 Slack 之前你需要添加一个机器人用户

如果你需要接入一个 Opsdroid 不支持的服务,在文档里有有添加你自己的连接器的教程。

相当不错的文档: 特别是对于一个在积极开发中的新兴库来说,Opsdroid 的文档十分有帮助。这些文档包括一篇带你创建几个不同的基本技能的教程。Opsdroid 在技能连接器数据库,以及匹配器方面的文档也十分清晰。

它所支持的技能和连接器的仓库为它的技能提供了富有帮助的示范代码。

自然语言处理: Opsdroid 的技能里面能使用正则表达式,但也同样提供了几个包括 Dialogflowluis.aiRecast.AI 以及 wit.ai 的 NLP API。

Opsdroid 可能的不足

Opsdroid 对它的一部分连接器还没有启用全部的特性。比如说,Slack API 允许你向你的消息添加颜色柱、图片以及其他的“附件”。Opsdroid Slack 连接器并没有启用“附件”特性,所以如果那些特性对你来说很重要的话,你需要编写一个自定义的 Slack 连接器。如果连接器缺少一个你需要的特性,Opsdroid 将欢迎你的贡献。文档中可以使用更多的例子,特别是对于预料到的使用场景。

示例用法

from opsdroid.matchers import match_regex
import random


@match_regex(r'hi|hello|hey|hallo')
async def hello(opsdroid, config, message):
    text = random.choice(["Hi {}", "Hello {}", "Hey {}"]).format(message.user)
    await message.respond(text)

hello/\_\_init\_\_.py

connectors:
  - name: websocket

skills:
  - name: hello
    repo: "https://github.com/<user_id>/hello-skill"

configuration.yaml

Errbot

Errbot 是一个功能齐全的开源聊天机器人。Errbot 发行于 2012 年,并且拥有人们从一个成熟的项目能期待的一切,包括良好的文档、优秀的教程以及许多帮你连入现有的流行聊天服务的插件。

它内置了什么

不像采用了较轻量级方式的 Opsdroid,Errbot 自带了你需要可靠地创建一个自定义机器人的一切东西。

Errbot 包括了对于本地 XMPP、IRC、Slack、Hipchat 以及 Telegram 服务的支持。它通过社区支持的后端列出了另外十种服务。

Errbot 的优势

良好的文档: Errbot 的文档成熟易读。

动态插件架构: Errbot 允许你通过和聊天机器人交谈安全地安装、卸载、更新、启用以及禁用插件。这使得开发和添加特性十分简便。感谢 Errbot 的颗粒性授权系统,出于安全意识这所有的一切都可以被锁闭。

当某个人输入 !help,Errbot 使用你的插件的文档字符串来为可获取的命令生成文档,这使得了解每行命令的作用更加简便。

内置的管理和安全特性: Errbot 允许你限制拥有管理员权限的用户列表,甚至细粒度访问控制。比如说你可以限制特定用户或聊天房间访问特定命令。

额外的插件框架: Errbot 支持钩子、回调、子命令、webhook、轮询以及其它更多特性。如果那些还不够,你甚至可以编写动态插件。当你需要基于在远程服务器上的可用命令来启用对应的聊天命令时,这个特性十分有用。

自带测试框架: Errbot 支持 pytest,同时也自带一些能使你简便测试插件的有用功能。它的“测试你的插件”的文档出于深思熟虑,并提供了足够的资料让你上手。

Errbot 可能的不足

以 “!” 开头: 默认情况下,Errbot 命令发出时以一个惊叹号打头(!help 以及 !hello)。一些人可能会喜欢这样,但是另一些人可能认为这让人烦恼。谢天谢地,这很容易关掉。

插件元数据 首先,Errbot 的 Hello World 插件示例看上去易于使用。然而我无法加载我的插件,直到我进一步阅读了教程并发现我还需要一个 .plug 文档,这是一个 Errbot 用来加载插件的文档。这可能比较吹毛求疵了,但是在我深挖文档之前,这对我来说都不是显而易见的。

示例用法

import random
from errbot import BotPlugin, botcmd

class Hello(BotPlugin):

    @botcmd
    def hello(self, msg, args):
        text = random.choice(["Hi {}", "Hello {}", "Hey {}"]).format(message.user)
        return text

hello.py

[Core]
Name = Hello
Module = hello

[Python]
Version = 2+

[Documentation]
Description = Example "Hello" plugin

hello.plug

你用过 Errbot 或 Opsdroid 吗?如果用过请留下关于你对于这些工具印象的留言。


via: https://opensource.com/article/18/3/python-chatops-libraries-opsdroid-and-errbot

作者:Jeff Triplett, Lacey Williams Henschel 译者:tomjlw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

假如说,如果你的老板给你的任务是一次又一次地访问竞争对手的网站,把对方商品的价格记录下来,而且要纯手工操作,恐怕你会想要把整个办公室都烧掉。

之所以现在网络爬虫的影响力如此巨大,就是因为网络爬虫可以被用于追踪客户的情绪和趋向、搜寻空缺的职位、监控房地产的交易,甚至是获取 UFC 的比赛结果。除此以外,还有很多意想不到的用途。

对于有这方面爱好的人来说,爬虫无疑是一个很好的工具。因此,我使用了 Scrapy 这个基于 Python 编写的开源网络爬虫框架。

鉴于我不太了解这个工具是否会对我的计算机造成伤害,我并没有将它搭建在我的主力机器上,而是搭建在了一台树莓派上面。

令人感到意外的是,Scrapy 在树莓派上面的性能并不差,或许这是 ARM 架构服务器的又一个成功例子?

我尝试 Google 了一下,但并没有得到令我满意的结果,仅仅找到了一篇相关的《Drupal 建站对比》。这篇文章的结论是,ARM 架构服务器性能比昂贵的 x86 架构服务器要更好。

从另一个角度来看,这种 web 服务可以看作是一个“被爬虫”服务,但和 Scrapy 对比起来,前者是基于 LAMP 技术栈,而后者则依赖于 Python,这就导致两者之间没有太多的可比性。

那我们该怎样做呢?只能在一些 VPS 上搭建服务来对比一下了。

什么是 ARM 架构处理器?

ARM 是目前世界上最流行的 CPU 架构。

但 ARM 架构处理器在很多人眼中的地位只是作为一个省钱又省电的选择,而不是跑在生产环境中的处理器的首选。

然而,诞生于英国剑桥的 ARM CPU,最初是用于极其昂贵的 Acorn Archimedes 计算机上的,这是当时世界上最强大的桌面计算机,甚至在很长一段时间内,它的运算速度甚至比最快的 386 还要快好几倍。

Acorn 公司和 Commodore、Atari 的理念类似,他们认为一家伟大的计算机公司就应该制造出伟大的计算机,让人感觉有点目光短浅。而比尔盖茨的想法则有所不同,他力图在更多不同种类和价格的 x86 机器上使用他的 DOS 系统。

拥有大量用户基数的平台会成为第三方开发者开发软件的平台,而软件资源丰富又会让你的计算机更受用户欢迎。

即使是苹果公司也几乎被打败。在 x86 芯片上投入大量的财力,最终,这些芯片被用于生产环境计算任务。

但 ARM 架构也并没有消失。基于 ARM 架构的芯片不仅运算速度快,同时也非常节能。因此诸如机顶盒、PDA、数码相机、MP3 播放器这些电子产品多数都会采用 ARM 架构的芯片,甚至在很多需要用电池或不配备大散热风扇的电子产品上,都可以见到 ARM 芯片的身影。

而 ARM 则脱离 Acorn 成为了一种特殊的商业模式,他们不生产实物芯片,仅仅是向芯片生产厂商出售相关的知识产权。

因此,这或多或少是 ARM 芯片被应用于如此之多的手机和平板电脑上的原因。当 Linux 被移植到这种架构的芯片上时,开源技术的大门就已经向它打开了,这才让我们今天得以在这些芯片上运行 web 爬虫程序。

服务器端的 ARM

诸如微软Cloudflare 这些大厂都在基础设施建设上花了重金,所以对于我们这些预算不高的用户来说,可以选择的余地并不多。

实际上,如果你的信用卡只够付每月数美元的 VPS 费用,一直以来只能考虑 Scaleway 这个高性价比的厂商。

但自从数个月前公有云巨头 AWS 推出了他们自研的 ARM 处理器 AWS Graviton 之后,选择似乎就丰富了一些。

我决定在其中选择一款 VPS 厂商,将它提供的 ARM 处理器和 x86 处理器作出对比。

深入了解

所以我们要对比的是什么指标呢?

Scaleway

Scaleway 自身的定位是“专为开发者设计”。我觉得这个定位很准确,对于开发和原型设计来说,Scaleway 提供的产品确实可以作为一个很好的沙盒环境。

Scaleway 提供了一个简洁的仪表盘页面,让用户可以快速地从主页进入 bash shell 界面。对于很多小企业、自由职业者或者技术顾问,如果想要运行 web 爬虫,这个产品毫无疑问是一个物美价廉的选择。

ARM 方面我们选择 ARM64-2GB 这一款服务器,每月只需要 3 欧元。它带有 4 个 Cavium ThunderX 核心,这是在 2014 年推出的第一款服务器级的 ARMv8 处理器。但现在看来它已经显得有点落后了,并逐渐被更新的 ThunderX2 取代。

x86 方面我们选择 1-S,每月的费用是 4 欧元。它拥有 2 个英特尔 Atom C3995 核心。英特尔的 Atom 系列处理器的特点是低功耗、单线程,最初是用在笔记本电脑上的,后来也被服务器所采用。

两者在处理器以外的条件都大致相同,都使用 2 GB 的内存、50 GB 的 SSD 存储以及 200 Mbit/s 的带宽。磁盘驱动器可能会有所不同,但由于我们运行的是 web 爬虫,基本都是在内存中完成操作,因此这方面的差异可以忽略不计。

为了避免我不能熟练使用包管理器的尴尬局面,两方的操作系统我都会选择使用 Debian 9。

Amazon Web Services(AWS)

当你还在注册 AWS 账号的时候,使用 Scaleway 的用户可能已经把提交信用卡信息、启动 VPS 实例、添加 sudo 用户、安装依赖包这一系列流程都完成了。AWS 的操作相对来说比较繁琐,甚至需要详细阅读手册才能知道你正在做什么。

当然这也是合理的,对于一些需求复杂或者特殊的企业用户,确实需要通过详细的配置来定制合适的使用方案。

我们所采用的 AWS Graviton 处理器是 AWS EC2( 弹性计算云 Elastic Compute Cloud )的一部分,我会以按需实例的方式来运行,这也是最贵但最简捷的方式。AWS 同时也提供竞价实例,这样可以用较低的价格运行实例,但实例的运行时间并不固定。如果实例需要长时间持续运行,还可以选择预留实例

看,AWS 就是这么复杂……

我们分别选择 a1.mediumt2.small 两种型号的实例进行对比,两者都带有 2GB 内存。这个时候问题来了,这里提到的 vCPU 又是什么?两种型号的不同之处就在于此。

对于 a1.medium 型号的实例,vCPU 是 AWS Graviton 芯片提供的单个计算核心。这个芯片由被亚马逊在 2015 收购的以色列厂商 Annapurna Labs 研发,是 AWS 独有的单线程 64 位 ARMv8 内核。它的按需价格为每小时 0.0255 美元。

而 t2.small 型号实例使用英特尔至强系列芯片,但我不确定具体是其中的哪一款。它每个核心有两个线程,但我们并不能用到整个核心,甚至整个线程。

我们能用到的只是“20% 的基准性能,可以使用 CPU 积分突破这个基准”。这可能有一定的原因,但我没有弄懂。它的按需价格是每小时 0.023 美元。

在镜像库中没有 Debian 发行版的镜像,因此我选择了 Ubuntu 18.04。

瘪四与大头蛋爬取 Moz 排行榜前 500 的网站

要测试这些 VPS 的 CPU 性能,就该使用爬虫了。一个方法是对几个网站在尽可能短的时间里发出尽可能多的请求,但这种操作不太礼貌,我的做法是只向大量网站发出少数几个请求。

为此,我编写了 beavis.py(瘪四)这个爬虫程序(致敬我最喜欢的物理学家和制片人 Mike Judge)。这个程序会将 Moz 上排行前 500 的网站都爬取 3 层的深度,并计算 “wood” 和 “ass” 这两个单词在 HTML 文件中出现的次数。(LCTT 译注:beavis(瘪四)和 butt-head(大头蛋) 都是 Mike Judge 的动画片《瘪四与大头蛋》中的角色)

但我实际爬取的网站可能不足 500 个,因为我需要遵循网站的 robot.txt 协定,另外还有些网站需要提交 javascript 请求,也不一定会计算在内。但这已经是一个足以让 CPU 保持繁忙的爬虫任务了。

Python 的全局解释器锁机制会让我的程序只能用到一个 CPU 线程。为了测试多线程的性能,我需要启动多个独立的爬虫程序进程。

因此我还编写了 butthead.py,尽管大头蛋很粗鲁,它也总是比瘪四要略胜一筹。

我将整个爬虫任务拆分为多个部分,这可能会对爬取到的链接数量有一点轻微的影响。但无论如何,每次爬取都会有所不同,我们要关注的是爬取了多少个页面,以及耗时多长。

在 ARM 服务器上安装 Scrapy

安装 Scrapy 的过程与芯片的不同架构没有太大的关系,都是安装 pip 和相关的依赖包之后,再使用 pip 来安装 Scrapy。

据我观察,在使用 ARM 的机器上使用 pip 安装 Scrapy 确实耗时要长一点,我估计是由于需要从源码编译为二进制文件。

在 Scrapy 安装结束后,就可以通过 shell 来查看它的工作状态了。

在 Scaleway 的 ARM 机器上,Scrapy 安装完成后会无法正常运行,这似乎和 service_identity 模块有关。这个现象也会在树莓派上出现,但在 AWS Graviton 上不会出现。

对于这个问题,可以用这个命令来解决:

sudo pip3 install service_identity --force --upgrade

接下来就可以开始对比了。

单线程爬虫

Scrapy 的官方文档建议将爬虫程序的 CPU 使用率控制在 80% 到 90% 之间,在真实操作中并不容易,尤其是对于我自己写的代码。根据我的观察,实际的 CPU 使用率变动情况是一开始非常繁忙,随后稍微下降,接着又再次升高。

在爬取任务的最后,也就是大部分目标网站都已经被爬取了的这个阶段,会持续数分钟的时间。这让人有点失望,因为在这个阶段当中,任务的运行时长只和网站的大小有比较直接的关系,并不能以之衡量 CPU 的性能。

所以这并不是一次严谨的基准测试,只是我通过自己写的爬虫程序来观察实际的现象。

下面我们来看看最终的结果。首先是 Scaleway 的机器:

机器种类耗时爬取页面数每小时爬取页面数每百万页面费用(欧元)
Scaleway ARM64-2GB108m 59.27s38,20521,032.6230.28527
Scaleway 1-S97m 44.067s39,47624,324.6480.33011

我使用了 top 工具来查看爬虫程序运行期间的 CPU 使用率。在任务刚开始的时候,两者的 CPU 使用率都达到了 100%,但 ThunderX 大部分时间都达到了 CPU 的极限,无法看出来 Atom 的性能会比 ThunderX 超出多少。

通过 top 工具,我还观察了它们的内存使用情况。随着爬取任务的进行,ARM 机器的内存使用率最终达到了 14.7%,而 x86 则最终是 15%。

从运行日志还可以看出来,当 CPU 使用率到达极限时,会有大量的超时页面产生,最终导致页面丢失。这也是合理出现的现象,因为 CPU 过于繁忙会无法完整地记录所有爬取到的页面。

如果仅仅是为了对比爬虫的速度,页面丢失并不是什么大问题。但在实际中,业务成果和爬虫数据的质量是息息相关的,因此必须为 CPU 留出一些用量,以防出现这种现象。

再来看看 AWS 这边:

机器种类耗时爬取页面数每小时爬取页面数每百万页面费用(美元)
a1.medium100m 39.900s41,29424,612.7251.03605
t2.small78m 53.171s41,20031,336.2860.73397

为了方便比较,对于在 AWS 上跑的爬虫,我记录的指标和 Scaleway 上一致,但似乎没有达到预期的效果。这里我没有使用 top,而是使用了 AWS 提供的控制台来监控 CPU 的使用情况,从监控结果来看,我的爬虫程序并没有完全用到这两款服务器所提供的所有性能。

a1.medium 型号的机器尤为如此,在任务开始阶段,它的 CPU 使用率达到了峰值 45%,但随后一直在 20% 到 30% 之间。

让我有点感到意外的是,这个程序在 ARM 处理器上的运行速度相当慢,但却远未达到 Graviton CPU 能力的极限,而在 Intel Atom 处理器上则可以在某些时候达到 CPU 能力的极限。它们运行的代码是完全相同的,处理器的不同架构可能导致了对代码的不同处理方式。

个中原因无论是由于处理器本身的特性,还是二进制文件的编译,又或者是两者皆有,对我来说都是一个黑盒般的存在。我认为,既然在 AWS 机器上没有达到 CPU 处理能力的极限,那么只有在 Scaleway 机器上跑出来的性能数据是可以作为参考的。

t2.small 型号的机器性能让人费解。CPU 利用率大概 20%,最高才达到 35%,是因为手册中说的“20% 的基准性能,可以使用 CPU 积分突破这个基准”吗?但在控制台中可以看到 CPU 积分并没有被消耗。

为了确认这一点,我安装了 stress 这个软件,然后运行了一段时间,这个时候发现居然可以把 CPU 使用率提高到 100% 了。

显然,我需要调整一下它们的配置文件。我将 CONCURRENT_REQUESTS 参数设置为 5000,将 REACTOR_THREADPOOL_MAXSIZE 参数设置为 120,将爬虫任务的负载调得更大。

机器种类耗时爬取页面数每小时爬取页面数每万页面费用(美元)
a1.medium46m 13.619s40,28352,285.0470.48771
t2.small41m7.619s36,24152,871.8570.43501
t2.small(无 CPU 积分)73m 8.133s34,29828,137.88910.81740

a1.medium 型号机器的 CPU 使用率在爬虫任务开始后 5 分钟飙升到了 100%,随后下降到 80% 并持续了 20 分钟,然后再次攀升到 96%,直到任务接近结束时再次下降。这大概就是我想要的效果了。

而 t2.small 型号机器在爬虫任务的前期就达到了 50%,并一直保持在这个水平直到任务接近结束。如果每个核心都有两个线程,那么 50% 的 CPU 使用率确实是单个线程可以达到的极限了。

现在我们看到它们的性能都差不多了。但至强处理器的线程持续跑满了 CPU,Graviton 处理器则只是有一段时间如此。可以认为 Graviton 略胜一筹。

然而,如果 CPU 积分耗尽了呢?这种情况下的对比可能更为公平。为了测试这种情况,我使用 stress 把所有的 CPU 积分用完,然后再次启动了爬虫任务。

在没有 CPU 积分的情况下,CPU 使用率在 27% 就到达极限不再上升了,同时又出现了丢失页面的现象。这么看来,它的性能比负载较低的时候更差。

多线程爬虫

将爬虫任务分散到不同的进程中,可以有效利用机器所提供的多个核心。

一开始,我将爬虫任务分布在 10 个不同的进程中并同时启动,结果发现比每个核心仅使用 1 个进程的时候还要慢。

经过尝试,我得到了一个比较好的方案。把爬虫任务分布在 10 个进程中,但每个核心只启动 1 个进程,在每个进程接近结束的时候,再从剩余的进程中选出 1 个进程启动起来。

如果还需要优化,还可以让运行时间越长的爬虫进程在启动顺序中排得越靠前,我也在尝试实现这个方法。

想要预估某个域名的页面量,一定程度上可以参考这个域名主页的链接数量。我用另一个程序来对这个数量进行了统计,然后按照降序排序。经过这样的预处理之后,只会额外增加 1 分钟左右的时间。

结果,爬虫运行的总耗时超过了两个小时!毕竟把链接最多的域名都堆在同一个进程中也存在一定的弊端。

针对这个问题,也可以通过调整各个进程爬取的域名数量来进行优化,又或者在排序之后再作一定的修改。不过这种优化可能有点复杂了。

因此,我还是用回了最初的方法,它的效果还是相当不错的:

机器种类耗时爬取页面数每小时爬取页面数每万页面费用(欧元)
Scaleway ARM64-2GB62m 10.078s36,15834,897.07190.17193
Scaleway 1-S60m 56.902s36,72536,153.55290.22128

毕竟,使用多个核心能够大大加快爬虫的速度。

我认为,如果让一个经验丰富的程序员来优化的话,一定能够更好地利用所有的计算核心。但对于开箱即用的 Scrapy 来说,想要提高性能,使用更快的线程似乎比使用更多核心要简单得多。

从数量来看,Atom 处理器在更短的时间内爬取到了更多的页面。但如果从性价比角度来看,ThunderX 又是稍稍领先的。不过总的来说差距不大。

爬取结果分析

在爬取了 38205 个页面之后,我们可以统计到在这些页面中 “ass” 出现了 24170435 次,而 “wood” 出现了 54368 次。

“wood” 的出现次数不少,但和 “ass” 比起来简直微不足道。

结论

从上面的数据来看,对于性能而言,CPU 的架构并没有它们的问世时间重要,2018 年生产的 AWS Graviton 是单线程情况下性能最佳的。

你当然可以说按核心来比,Xeon 仍然赢了。但是,你不但需要计算美元的变化,甚至还要计算线程数。

另外在性能方面 2017 年生产的 Atom 轻松击败了 2014 年生产的 ThunderX,而 ThunderX 则在性价比方面占优。当然,如果你使用 AWS 的机器的话,还是使用 Graviton 吧。

总之,ARM 架构的硬件是可以用来运行爬虫程序的,而且在性能和费用方面也相当有竞争力。

而这种差异是否足以让你将整个技术架构迁移到 ARM 上?这就是另一回事了。当然,如果你已经是 AWS 用户,并且你的代码有很强的可移植性,那么不妨尝试一下 a1 型号的实例。

希望 ARM 设备在不久的将来能够在公有云上大放异彩。

源代码

这是我第一次使用 Python 和 Scrapy 来做一个项目,所以我的代码写得可能不是很好,例如代码中使用全局变量就有点力不从心。

不过我仍然会在下面开源我的代码。

要运行这些代码,需要预先安装 Scrapy,并且需要 Moz 上排名前 500 的网站的 csv 文件。如果要运行 butthead.py,还需要安装 psutil 这个库。

import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.crawler import CrawlerProcess

ass = 0
wood = 0
totalpages = 0

def getdomains():

  moz500file = open('top500.domains.05.18.csv')

  domains = []
  moz500csv = moz500file.readlines()

  del moz500csv[0]

  for csvline in moz500csv:
    leftquote = csvline.find('"')    
    rightquote = leftquote + csvline[leftquote + 1:].find('"')
    domains.append(csvline[leftquote + 1:rightquote])

  return domains

def getstartpages(domains):
  
  startpages = []
  
  for domain in domains:
    startpages.append('http://' + domain)
  
  return startpages
  
class AssWoodItem(scrapy.Item):
  ass = scrapy.Field()
  wood = scrapy.Field()
  url = scrapy.Field()
  
class AssWoodPipeline(object):
  def __init__(self):
    self.asswoodstats = []

  def process_item(self, item, spider):
    self.asswoodstats.append((item.get('url'), item.get('ass'), item.get('wood')))
    
  def close_spider(self, spider):
    asstally, woodtally = 0, 0
    
    for asswoodcount in self.asswoodstats:
      asstally += asswoodcount[1]
      woodtally += asswoodcount[2]
      
    global ass, wood, totalpages
    ass = asstally
    wood = woodtally
    totalpages = len(self.asswoodstats)

class BeavisSpider(CrawlSpider):
  name = "Beavis"
  allowed_domains = getdomains()
  start_urls = getstartpages(allowed_domains)
  #start_urls = [ 'http://medium.com' ]
  custom_settings = {
    'DEPTH_LIMIT': 3,
    'DOWNLOAD_DELAY': 3,
    'CONCURRENT_REQUESTS': 1500,
    'REACTOR_THREADPOOL_MAXSIZE': 60,
    'ITEM_PIPELINES': { '__main__.AssWoodPipeline': 10 },
    'LOG_LEVEL': 'INFO',
    'RETRY_ENABLED': False,
    'DOWNLOAD_TIMEOUT': 30,
    'COOKIES_ENABLED': False,
    'AJAXCRAWL_ENABLED': True
  }
    
  rules = ( Rule(LinkExtractor(), callback='parse_asswood'), )
  
  def parse_asswood(self, response):
    if isinstance(response, scrapy.http.TextResponse):
      item = AssWoodItem()
      item['ass'] = response.text.casefold().count('ass')
      item['wood'] = response.text.casefold().count('wood')
      item['url'] = response.url
      yield item


if __name__ == '__main__':

  process = CrawlerProcess({
      'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'
  })

  process.crawl(BeavisSpider)
  process.start()

  print('Uhh, that was, like, ' + str(totalpages) + ' pages crawled.')
  print('Uh huhuhuhuh. It said ass ' + str(ass) + ' times.')
  print('Uh huhuhuhuh. It said wood ' + str(wood) + ' times.')

beavis.py

import scrapy, time, psutil
from scrapy.spiders import CrawlSpider, Rule, Spider
from scrapy.linkextractors import LinkExtractor
from scrapy.crawler import CrawlerProcess
from multiprocessing import Process, Queue, cpu_count

ass = 0
wood = 0
totalpages = 0
linkcounttuples =[]

def getdomains():

  moz500file = open('top500.domains.05.18.csv')

  domains = []
  moz500csv = moz500file.readlines()

  del moz500csv[0]

  for csvline in moz500csv:
    leftquote = csvline.find('"')    
    rightquote = leftquote + csvline[leftquote + 1:].find('"')
    domains.append(csvline[leftquote + 1:rightquote])

  return domains

def getstartpages(domains):
  
  startpages = []
  
  for domain in domains:
    startpages.append('http://' + domain)
  
  return startpages
  
class AssWoodItem(scrapy.Item):
  ass = scrapy.Field()
  wood = scrapy.Field()
  url = scrapy.Field()
  
class AssWoodPipeline(object):
  def __init__(self):
    self.asswoodstats = []

  def process_item(self, item, spider):
    self.asswoodstats.append((item.get('url'), item.get('ass'), item.get('wood')))
    
  def close_spider(self, spider):
    asstally, woodtally = 0, 0
    
    for asswoodcount in self.asswoodstats:
      asstally += asswoodcount[1]
      woodtally += asswoodcount[2]
      
    global ass, wood, totalpages
    ass = asstally
    wood = woodtally
    totalpages = len(self.asswoodstats)
          

class ButtheadSpider(CrawlSpider):
  name = "Butthead"
  custom_settings = {
    'DEPTH_LIMIT': 3,
    'DOWNLOAD_DELAY': 3,
    'CONCURRENT_REQUESTS': 250,
    'REACTOR_THREADPOOL_MAXSIZE': 30,
    'ITEM_PIPELINES': { '__main__.AssWoodPipeline': 10 },
    'LOG_LEVEL': 'INFO',
    'RETRY_ENABLED': False,
    'DOWNLOAD_TIMEOUT': 30,
    'COOKIES_ENABLED': False,
    'AJAXCRAWL_ENABLED': True
  }
    
  rules = ( Rule(LinkExtractor(), callback='parse_asswood'), )
  
  
  def parse_asswood(self, response):
    if isinstance(response, scrapy.http.TextResponse):
      item = AssWoodItem()
      item['ass'] = response.text.casefold().count('ass')
      item['wood'] = response.text.casefold().count('wood')
      item['url'] = response.url
      yield item

def startButthead(domainslist, urlslist, asswoodqueue):
  crawlprocess = CrawlerProcess({
      'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)'
  })

  crawlprocess.crawl(ButtheadSpider, allowed_domains = domainslist, start_urls = urlslist)
  crawlprocess.start()
  asswoodqueue.put( (ass, wood, totalpages) )
  

if __name__ == '__main__':
  asswoodqueue = Queue()
  domains=getdomains()
  startpages=getstartpages(domains)
  processlist =[]
  cores = cpu_count()
  
  for i in range(10):
    domainsublist = domains[i * 50:(i + 1) * 50]
    pagesublist = startpages[i * 50:(i + 1) * 50]
    p = Process(target = startButthead, args = (domainsublist, pagesublist, asswoodqueue))
    processlist.append(p)
  
  for i in range(cores):
    processlist[i].start()
    
  time.sleep(180)
  
  i = cores
  
  while i != 10:
    time.sleep(60)
    if psutil.cpu_percent() < 66.7:
      processlist[i].start()
      i += 1
  
  for i in range(10):
    processlist[i].join()
  
  for i in range(10):
    asswoodtuple = asswoodqueue.get()
    ass += asswoodtuple[0]
    wood += asswoodtuple[1]
    totalpages += asswoodtuple[2]

  print('Uhh, that was, like, ' + str(totalpages) + ' pages crawled.')
  print('Uh huhuhuhuh. It said ass ' + str(ass) + ' times.')
  print('Uh huhuhuhuh. It said wood ' + str(wood) + ' times.')  

butthead.py


via: https://blog.dxmtechsupport.com.au/speed-test-x86-vs-arm-for-web-crawling-in-python/

作者:James Mawson 选题:lujun9972 译者:HankChow 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

CommandlineFu 是一个记录脚本片段的网站,每个片段都有对应的功能说明和对应的标签。我想要做的就是尝试用 shell 写一个多进程的爬虫把这些代码片段记录在一个 org 文件中。

参数定义

这个脚本需要能够通过 -n 参数指定并发的爬虫数(默认为 CPU 核的数量),还要能通过 -f 指定保存的 org 文件路径(默认输出到 stdout)。

#!/usr/bin/env bash

proc_num=$(nproc)
store_file=/dev/stdout
while getopts :n:f: OPT; do
    case $OPT in
        n|+n)
            proc_num="$OPTARG"
            ;;
        f|+f)
            store_file="$OPTARG"
            ;;
        *)
            echo "usage: ${0##*/} [+-n proc_num] [+-f org_file} [--]"
            exit 2
    esac
done
shift $(( OPTIND - 1 ))
OPTIND=1

解析命令浏览页面

我们需要一个进程从 CommandlineFu 的浏览列表中抽取各个脚本片段的 URL,这个进程将抽取出来的 URL 存放到一个队列中,再由各个爬虫进程从进程中读取 URL 并从中抽取出对应的代码片段、描述说明和标签信息写入 org 文件中。

这里就会遇到三个问题:

  1. 进程之间通讯的队列如何实现
  2. 如何从页面中抽取出 URL、代码片段、描述说明、标签等信息
  3. 多进程对同一文件进行读写时的乱序问题

实现进程之间的通讯队列

这个问题比较好解决,我们可以通过一个命名管道来实现:

queue=$(mktemp --dry-run)
mkfifo ${queue}
exec 99<>${queue}
trap "rm ${queue} 2>/dev/null" EXIT

从页面中抽取想要的信息

从页面中提取元素内容主要有两种方法:

  1. 对于简单的 HTML 页面,我们可以通过 sedgrepawk 等工具通过正则表达式匹配的方式来从 HTML 中抽取信息。
  2. 通过 html-xml-utils 工具集中的 hxselect 来根据 CSS 选择器提取相关元素。

这里我们使用 html-xml-utils 工具来提取:

function extract_views_from_browse_page()
{
    if [[ $# -eq 0 ]];then
        local html=$(cat -)
    else
        local html="$*"
    fi
    echo ${html} |hxclean |hxselect -c -s "\n" "li.list-group-item > div:nth-child(1) > div:nth-child(1) > a:nth-child(1)::attr(href)"|sed 's@^@https://www.commandlinefu.com/@'
}

function extract_nextpage_from_browse_page()
{
    if [[ $# -eq 0 ]];then
        local html=$(cat -)
    else
        local html="$*"
    fi
    echo ${html} |hxclean |hxselect -s "\n" "li.list-group-item:nth-child(26) > a"|grep '>'|hxselect -c "::attr(href)"|sed 's@^@https://www.commandlinefu.com/@'
}

这里需要注意的是:hxselect 对 HTML 解析时要求遵循严格的 XML 规范,因此在用 hxselect 解析之前需要先经过 hxclean 矫正。另外,为了防止 HTML 过大,超过参数列表长度,这里允许通过管道的形式将 HTML 内容传入。

循环读取下一页的浏览页面,不断抽取代码片段 URL 写入队列

这里要解决的是上面提到的第三个问题: 多进程对管道进行读写时如何保障不出现乱序? 为此,我们需要在写入文件时对文件加锁,然后在写完文件后对文件解锁,在 shell 中我们可以使用 flock 来对文件进行枷锁。 关于 flock 的使用方法和注意事项,请参见另一篇博文 Linux shell flock 文件锁的用法及注意事项

由于需要在 flock 子进程中使用函数 extract_views_from_browse_page,因此需要先导出该函数:

export -f extract_views_from_browse_page

由于网络问题,使用 curl 获取内容可能失败,需要重复获取:

function fetch()
{
    local url="$1"
    while ! curl -L ${url} 2>/dev/null;do
        :
    done
}

collector 用来从种子 URL 中抓取待爬的 URL,写入管道文件中,写操作期间管道文件同时作为锁文件:

function collector()
{
    url="$*"
    while [[ -n ${url} ]];do
        echo "从$url中抽取"
        html=$(fetch "${url}")
        echo "${html}"|flock ${queue} -c "extract_views_from_browse_page >${queue}"
        url=$(echo "${html}"|extract_nextpage_from_browse_page)
    done
    # 让后面解析代码片段的爬虫进程能够正常退出,而不至于被阻塞.
    for ((i=0;i<${proc_num};i++))
    do
        echo >${queue}
    done
}

这里要注意的是, 在找不到下一页 URL 后,我们用一个 for 循环往队列里写入了 =proc_num= 个空行,这一步的目的是让后面解析代码片段的爬虫进程能够正常退出,而不至于被阻塞。

解析脚本片段页面

我们需要从脚本片段的页面中抽取标题、代码片段、描述说明以及标签信息,同时将这些内容按 org 模式的格式写入存储文件中。

  function view_page_handler()
  {
      local url="$1"
      local html="$(fetch "${url}")"
      # headline
      local headline="$(echo ${html} |hxclean |hxselect -c -s "\n" ".col-md-8 > h1:nth-child(1)")"
      # command
      local command="$(echo ${html} |hxclean |hxselect -c -s "\n" ".col-md-8 > div:nth-child(2) > span:nth-child(2)"|pandoc -f html -t org)"
      # description
      local description="$(echo ${html} |hxclean |hxselect -c -s "\n" ".col-md-8 > div.description"|pandoc -f html -t org)"
      # tags
      local tags="$(echo ${html} |hxclean |hxselect -c -s ":" ".functions > a")"
      if [[ -n "${tags}" ]];then
          tags=":${tags}"
      fi
      # build org content
      cat <<EOF |flock -x ${store_file} tee -a ${store_file}
* ${headline}      ${tags}

:PROPERTIES:
:URL:       ${url}
:END:

${description}
#+begin_src shell
${command}
#+end_src

EOF
  }

这里抽取信息的方法跟上面的类似,不过代码片段和描述说明中可能有一些 HTML 代码,因此通过 pandoc 将之转换为 org 格式的内容。

注意最后输出 org 模式的格式并写入存储文件中的代码不要写成下面这样:

    flock -x ${store_file} cat <<EOF >${store_file}
    * ${headline}\t\t ${tags}
    ${description}
    #+begin_src shell
    ${command}
    #+end_src
EOF

它的意思是使用 flockcat 命令进行加锁,再把 flock 整个命令的结果通过重定向输出到存储文件中,而重定向输出的这个过程是没有加锁的。

spider 从管道文件中读取待抓取的 URL,然后实施真正的抓取动作。

function spider()
{
    while :
    do
        if ! url=$(flock ${queue} -c 'read -t 1 -u 99 url && echo $url')
        then
            sleep 1
            continue
        fi

        if [[ -z "$url" ]];then
            break
        fi
        view_page_handler ${url}
    done
}

这里要注意的是,为了防止发生死锁,从管道中读取 URL 时设置了超时,当出现超时就意味着生产进程赶不上消费进程的消费速度,因此消费进程休眠一秒后再次检查队列中的 URL。

组合起来

collector "https://www.commandlinefu.com/commands/browse" &

for ((i=0;i<${proc_num};i++))
do
    spider &
done
wait

抓取其他网站

通过重新定义 extract_views_from_browse_pageextract_nextpage_from-browse_pageview_page_handler 这几个函数, 以及提供一个新的种子 URL,我们可以很容易将其改造成抓取其他网站的多进程爬虫。

例如通过下面这段代码,就可以用来爬取 xkcd 上的漫画:

function extract_views_from_browse_page()
{
    if [[ $# -eq 0 ]];then
        local html=$(cat -)
    else
        local html="$*"
    fi
    max=$(echo "${html}"|hxclean |hxselect -c -s "\n" "#middleContainer"|grep "Permanent link to this comic" |awk -F "/" '{print $4}')
    seq 1 ${max}|sed 's@^@https://xkcd.com/@'
}

function extract_nextpage_from_browse_page()
{
    echo ""
}

function view_page_handler()
{
    local url="$1"
    local html="$(fetch "${url}/")"
    local image="https:$(echo ${html} |hxclean |hxselect -c -s "\n" "#comic > img:nth-child(1)::attr(src)")"
    echo ${image}
    wget ${image}
}

collector "https://xkcd.com/" &

了解一些技巧助你减少代码查错时间。

在周五的下午三点钟(为什么是这个时间?因为事情总会在周五下午三点钟发生),你收到一条通知,客户发现你的软件出现一个错误。在有了初步的怀疑后,你联系运维,查看你的软件日志以了解发生了什么,因为你记得收到过日志已经搬家了的通知。

结果这些日志被转移到了你获取不到的地方,但它们正在导入到一个网页应用中——所以到时候你可以用这个漂亮的应用来检索日志,但是,这个应用现在还没完成。这个应用预计会在几天内完成。我知道,你觉得这完全不切实际。然而并不是,日志或者日志消息似乎经常在错误的时间消失不见。在我们开始查错前,一个忠告:经常检查你的日志以确保它们还在你认为它们应该在的地方,并记录你认为它们应该记的东西。当你不注意的时候,这些东西往往会发生令人惊讶的变化。

好的,你找到了日志或者尝试了呼叫运维人员,而客户确实发现了一个错误。甚至你可能认为你已经知道错误在哪儿。

你立即打开你认为可能有问题的文件并开始查错。

1、先不要碰你的代码

阅读代码,你甚至可能会想到该阅读哪些部分。但是在开始搞乱你的代码前,请重现导致错误的调用并把它变成一个测试。这将是一个集成测试,因为你可能还有其他疑问,目前你还不能准确地知道问题在哪儿。

确保这个测试结果是失败的。这很重要,因为有时你的测试不能重现失败的调用,尤其是你使用了可以混淆测试的 web 或者其他框架。很多东西可能被存储在变量中,但遗憾的是,只通过观察测试,你在测试里调用的东西并不总是明显可见的。当我尝试着重现这个失败的调用时,我并不是说我要创建一个可以通过的测试,但是,好吧,我确实是创建了一个测试,但我不认为这特别不寻常。

从自己的错误中吸取教训。

2、编写错误的测试

现在,你有了一个失败的测试,或者可能是一个带有错误的测试,那么是时候解决问题了。但是在你开干之前,让我们先检查下调用栈,因为这样可以更轻松地解决问题。

调用栈包括你已经启动但尚未完成地所有任务。因此,比如你正在烤蛋糕并准备往面糊里加面粉,那你的调用栈将是:

  • 做蛋糕
  • 打面糊
  • 加面粉

你已经开始做蛋糕,开始打面糊,而你现在正在加面粉。往锅底抹油不在这个列表中,因为你已经完成了,而做糖霜不在这个列表上因为你还没开始做。

如果你对调用栈不清楚,我强烈建议你使用 Python Tutor,它能帮你在执行代码时观察调用栈。

现在,如果你的 Python 程序出现了错误, Python 解释器会帮你打印出当前调用栈。这意味着无论那一时刻程序在做什么,很明显错误发生在调用栈的底部。

3、始终先检查调用栈底部

在栈底你不仅能看到发生了哪个错误,而且通常可以在调用栈的最后一行发现问题。如果栈底对你没有帮助,而你的代码还没有经过代码分析,那么使用代码分析是非常有用的。我推荐 pylint 或者 flake8。通常情况下,它会指出我一直忽略的错误的地方。

如果错误看起来很迷惑,你下一步行动可能是用 Google 搜索它。如果你搜索的内容不包含你的代码的相关信息,如变量名、文件等,那你将获得更好的搜索结果。如果你使用的是 Python 3(你应该使用它),那么搜索内容包含 Python 3 是有帮助的,否则 Python 2 的解决方案往往会占据大多数。

很久以前,开发者需要在没有搜索引擎的帮助下解决问题。那是一段黑暗时光。充分利用你可以使用的所有工具。

不幸的是,有时候问题发生在更早阶段,但只有在调用栈底部执行的地方才显现出来。就像当蛋糕没有膨胀时,忘记加发酵粉的事才被发现。

那就该检查整个调用栈。问题更可能在你的代码而不是 Python 标准库或者第三方包,所以先检查调用栈内你的代码。另外,在你的代码中放置断点通常会更容易检查代码。在调用栈的代码中放置断点,然后看看周围是否如你预期。

“但是,玛丽,”我听到你说,“如果我有一个调用栈,那这些都是有帮助的,但我只有一个失败的测试。我该从哪里开始?”

pdb,一个 Python 调试器。

找到你代码里会被这个调用命中的地方。你应该能够找到至少一个这样的地方。在那里打上一个 pdb 的断点。

一句题外话

为什么不使用 print 语句呢?我曾经依赖于 print 语句。有时候,它们仍然很方便。但当我开始处理复杂的代码库,尤其是有网络调用的代码库,print 语句就变得太慢了。我最终在各种地方都加上了 print 语句,但我没法追踪它们的位置和原因,而且变得更复杂了。但是主要使用 pdb 还有一个更重要的原因。假设你添加一条 print 语句去发现错误问题,而且 print 语句必须早于错误出现的地方。但是,看看你放 print 语句的函数,你不知道你的代码是怎么执行到那个位置的。查看代码是寻找调用路径的好方法,但看你以前写的代码是恐怖的。是的,我会用 grep 处理我的代码库以寻找调用函数的地方,但这会变得乏味,而且搜索一个通用函数时并不能缩小搜索范围。pdb 就变得非常有用。

你遵循我的建议,打上 pdb 断点并运行你的测试。然而测试再次失败,但是没有任何一个断点被命中。留着你的断点,并运行测试套件中一个同这个失败的测试非常相似的测试。如果你有个不错的测试套件,你应该能够找到一个这样的测试。它会命中了你认为你的失败测试应该命中的代码。运行这个测试,然后当它运行到你的断点,按下 w 并检查调用栈。如果你不知道如何查看因为其他调用而变得混乱的调用栈,那么在调用栈的中间找到属于你的代码,并在堆栈中该代码的上一行放置一个断点。再试一次新的测试。如果仍然没命中断点,那么继续,向上追踪调用栈并找出你的调用在哪里脱轨了。如果你一直没有命中断点,最后到了追踪的顶部,那么恭喜你,你发现了问题:你的应用程序名称拼写错了。

没有经验,小白,一点都没有经验。

4、修改代码

如果你仍觉得迷惑,在你稍微改变了一些的地方尝试新的测试。你能让新的测试跑起来么?有什么是不同的呢?有什么是相同的呢?尝试改变一下别的东西。当你有了你的测试,以及可能也还有其它的测试,那就可以开始安全地修改代码了,确定是否可以缩小问题范围。记得从一个新提交开始解决问题,以便于可以轻松地撤销无效地更改。(这就是版本控制,如果你没有使用过版本控制,这将会改变你的生活。好吧,可能它只是让编码更容易。查阅“版本控制可视指南”,以了解更多。)

5、休息一下

尽管如此,当它不再感觉起来像一个有趣的挑战或者游戏而开始变得令人沮丧时,你最好的举措是脱离这个问题。休息一下。我强烈建议你去散步并尝试考虑别的事情。

6、把一切写下来

当你回来了,如果你没有突然受到启发,那就把你关于这个问题所知的每一个点信息写下来。这应该包括:

  • 真正造成问题的调用
  • 真正发生了什么,包括任何错误信息或者相关的日志信息
  • 你真正期望发生什么
  • 到目前为止,为了找出问题,你做了什么工作;以及解决问题中你发现的任何线索。

有时这里有很多信息,但相信我,从零碎中挖掘信息是很烦人。所以尽量简洁,但是要完整。

7、寻求帮助

我经常发现写下所有信息能够启迪我想到还没尝试过的东西。当然,有时候我在点击求助邮件(或表单)的提交按钮后立刻意识到问题是是什么。无论如何,当你在写下所有东西仍一无所获时,那就试试向他人发邮件求助。首先是你的同事或者其他参与你的项目的人,然后是该项目的邮件列表。不要害怕向人求助。大多数人都是友善和乐于助人的,我发现在 Python 社区里尤其如此。

Maria McKinley 已在 PyCascades 2019 演讲 代码查错,2 月 23-24,于西雅图。


via: https://opensource.com/article/19/2/steps-hunting-code-python-bugs

作者:Maria Mckinley 选题:lujun9972 译者:LazyWolfLin 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

通过这 5 个插件扩展 Vim 功能来提升你的编码效率。

我用 Vim 已经超过 20 年了,两年前我决定把它作为我的首要文本编辑器。我用 Vim 来编写代码、配置文件、博客文章及其它任意可以用纯文本表达的东西。Vim 有很多超级棒的功能,一旦你适合了它,你的工作会变得非常高效。

在日常编辑工作中,我更倾向于使用 Vim 稳定的原生功能,但开源社区对 Vim 开发了大量的插件,可以扩展 Vim 的功能、改进你的工作流程和提升工作效率。

以下列举 5 个非常好用的可以用于编写任意编程语言的插件。

1、Auto Pairs

Auto Pairs 插件可以帮助你插入和删除成对的文字,如花括号、圆括号或引号。这在编写代码时非常有用,因为很多编程语言都有成对标记的语法,就像圆括号用于函数调用,或引号用于字符串定义。

Auto Pairs 最基本的功能是在你输入一个左括号时会自动补全对应的另一半括号。比如,你输入了一个 [,它会自动帮你补充另一半 ]。相反,如果你用退格键删除开头的一半括号,Auto Pairs 会删除另一半。

如果你设置了自动缩进,当你按下回车键时 Auto Pairs 会在恰当的缩进位置补全另一半括号,这比你找到放置另一半的位置并选择一个正确的括号要省劲多了。

例如下面这段代码:

package main

import "fmt"

func main() {
    x := true
    items := []string{"tv", "pc", "tablet"}

    if x { 
        for _, i := range items
    } 
}

items 后面输入一个左花括号按下回车会产生下面的结果:

package main

import "fmt"

func main() {
    x := true
    items := []string{"tv", "pc", "tablet"}

    if x {
        for _, i := range items  {
            | (cursor here)
        }
    }
}

Auto Pairs 提供了大量其它选项(你可以在 GitHub 上找到),但最基本的功能已经很让人省时间了。

2、NERD Commenter

NERD Commenter 插件给 Vim 增加了代码注释的功能,类似在 IDE integrated development environment 中注释功能。有了这个插件,你可以一键注释单行或多行代码。

NERD Commenter 可以与标准的 Vim filetype 插件配合,所以它能理解一些编程语言并使用合适的方式来注释代码。

最易上手的方法是按 Leader+Space 组合键来切换注释当前行。Vim 默认的 Leader 键是 \

可视化模式 Visual mode 中,你可以选择多行一并注释。NERD Commenter 也可以按计数注释,所以你可以加个数量 n 来注释 n 行。

还有个有用的特性 “Sexy Comment” 可以用 Leader+cs 来触发,它的块注释风格更漂亮一些。例如下面这段代码:

package main

import "fmt"

func main() {
    x := true
    items := []string{"tv", "pc", "tablet"}

    if x {
        for _, i := range items {
            fmt.Println(i)
        }
    }
}

选择 main 函数中的所有行然后按下 Leader+cs 会出来以下注释效果:

package main

import "fmt"

func main() {
/*
 *    x := true
 *    items := []string{"tv", "pc", "tablet"}
 *
 *    if x {
 *        for _, i := range items {
 *            fmt.Println(i)
 *        }
 *    }
 */
}

因为这些行都是在一个块中注释的,你可以用 Leader+Space 组合键一次去掉这里所有的注释。

NERD Commenter 是任何使用 Vim 写代码的开发者都必装的插件。

3、VIM Surround

Vim Surround 插件可以帮你“环绕”现有文本插入成对的符号(如括号或双引号)或标签(如 HTML 或 XML 标签)。它和 Auto Pairs 有点儿类似,但是用于处理已有文本,在编辑文本时更有用。

比如你有以下一个句子:

"Vim plugins are awesome !"

当你的光标处于引起来的句中任何位置时,你可以用 ds" 组合键删除句子两端的双引号。

Vim plugins are awesome !

你也可以用 cs"' 把双端的双引号换成单引号:

'Vim plugins are awesome !'

或者再用 cs'[ 替换成中括号:

[ Vim plugins are awesome ! ]

它对编辑 HTML 或 XML 文本中的 标签 tag 尤其在行。假如你有以下一行 HTML 代码:

<p>Vim plugins are awesome !</p>

当光标在 “awesome” 这个单词的任何位置时,你可以按 ysiw<em> 直接给它加上着重标签(<em>):

<p>Vim plugins are <em>awesome</em> !</p>

注意它聪明地加上了 </em> 闭合标签。

Vim Surround 也可以用 ySS 缩进文本并加上标签。比如你有以下文本:

<p>Vim plugins are <em>awesome</em> !</p>

你可以用 ySS<div class="normal"> 加上 div 标签,注意生成的段落是自动缩进的。

<div class="normal">
        <p>Vim plugins are <em>awesome</em> !</p>
</div>

Vim Surround 有很多其它选项,你可以参照 GitHub 上的说明尝试它们。

4、Vim Gitgutter

Vim Gitgutter 插件对使用 Git 作为版本控制工具的人来说非常有用。它会在 Vim 的行号列旁显示 git diff 的差异标记。假设你有如下已提交过的代码:

  1 package main
  2
  3 import "fmt"
  4
  5 func main() {
  6     x := true
  7     items := []string{"tv", "pc", "tablet"}
  8
  9     if x {
 10         for _, i := range items {
 11             fmt.Println(i)
 12         }
 13     }
 14 }

当你做出一些修改后,Vim Gitgutter 会显示如下标记:

    1 package main
    2
    3 import "fmt"
    4
_   5 func main() {
    6     items := []string{"tv", "pc", "tablet"}
    7
~   8     if len(items) > 0 {
    9         for _, i := range items {
   10             fmt.Println(i)
+  11             fmt.Println("------")
   12         }
   13     }
   14 }

_ 标记表示在第 5 行和第 6 行之间删除了一行。~ 表示第 8 行有修改,+ 表示新增了第 11 行。

另外,Vim Gitgutter 允许你用 [c]c 在多个有修改的块之间跳转,甚至可以用 Leader+hs 来暂存某个变更集。

这个插件提供了对变更的即时视觉反馈,如果你用 Git 的话,有了它简直是如虎添翼。

5、VIM Fugitive

Vim Fugitive 是另一个将 Git 工作流集成到 Vim 中的超棒插件。它对 Git 做了一些封装,可以让你在 Vim 里直接执行 Git 命令并将结果集成在 Vim 界面里。这个插件有超多的特性,更多信息请访问它的 GitHub 项目页面。

这里有一个使用 Vim Fugitive 的基础 Git 工作流示例。设想我们已经对下面的 Go 代码做出修改,你可以用 :Gblame 调用 git blame 来查看每行最后的提交信息:

e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│    1 package main
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│    2
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│    3 import "fmt"
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│    4
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│_   5 func main() {
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│    6     items := []string{"tv", "pc", "tablet"}
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│    7
00000000 (Not Committed Yet 2018-12-05 18:55:00 -0500)│~   8     if len(items) > 0 {
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│    9         for _, i := range items {
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│   10             fmt.Println(i)
00000000 (Not Committed Yet 2018-12-05 18:55:00 -0500)│+  11             fmt.Println("------")
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│   12         }
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│   13     }
e9949066 (Ricardo Gerardi   2018-12-05 18:17:19 -0500)│   14 }

可以看到第 8 行和第 11 行显示还未提交。用 :Gstatus 命令检查仓库当前的状态:

  1 # On branch master
  2 # Your branch is up to date with 'origin/master'.
  3 #
  4 # Changes not staged for commit:
  5 #   (use "git add <file>..." to update what will be committed)
  6 #   (use "git checkout -- <file>..." to discard changes in working directory)
  7 #
  8 #       modified:   vim-5plugins/examples/test1.go
  9 #
 10 no changes added to commit (use "git add" and/or "git commit -a")
--------------------------------------------------------------------------------------------------------
    1 package main
    2
    3 import "fmt"
    4
_   5 func main() {
    6     items := []string{"tv", "pc", "tablet"}
    7
~   8     if len(items) > 0 {
    9         for _, i := range items {
   10             fmt.Println(i)
+  11             fmt.Println("------")
   12         }
   13     }
   14 }

Vim Fugitive 在分割的窗口里显示 git status 的输出结果。你可以在该行按下 - 键用该文件的名字暂存这个文件的提交,再按一次 - 可以取消暂存。这个信息会随着你的操作自动更新:

  1 # On branch master
  2 # Your branch is up to date with 'origin/master'.
  3 #
  4 # Changes to be committed:
  5 #   (use "git reset HEAD <file>..." to unstage)
  6 #
  7 #       modified:   vim-5plugins/examples/test1.go
  8 #
--------------------------------------------------------------------------------------------------------
    1 package main
    2
    3 import "fmt"
    4
_   5 func main() {
    6     items := []string{"tv", "pc", "tablet"}
    7
~   8     if len(items) > 0 {
    9         for _, i := range items {
   10             fmt.Println(i)
+  11             fmt.Println("------")
   12         }
   13     }
   14 }

现在你可以用 :Gcommit 来提交修改了。Vim Fugitive 会打开另一个分割窗口让你输入提交信息:

  1 vim-5plugins: Updated test1.go example file
  2 # Please enter the commit message for your changes. Lines starting
  3 # with '#' will be ignored, and an empty message aborts the commit.
  4 #
  5 # On branch master
  6 # Your branch is up to date with 'origin/master'.
  7 #
  8 # Changes to be committed:
  9 #       modified:   vim-5plugins/examples/test1.go
 10 #

:wq 保存文件完成提交:

[master c3bf80f] vim-5plugins: Updated test1.go example file
 1 file changed, 2 insertions(+), 2 deletions(-)
Press ENTER or type command to continue

然后你可以再用 :Gstatus 检查结果并用 :Gpush 把新的提交推送到远程。

  1 # On branch master
  2 # Your branch is ahead of 'origin/master' by 1 commit.
  3 #   (use "git push" to publish your local commits)
  4 #
  5 nothing to commit, working tree clean

Vim Fugitive 的 GitHub 项目主页有很多屏幕录像展示了它的更多功能和工作流,如果你喜欢它并想多学一些,快去看看吧。

接下来?

这些 Vim 插件都是程序开发者的神器!还有另外两类开发者常用的插件:自动完成插件和语法检查插件。它些大都是和具体的编程语言相关的,以后我会在一些文章中介绍它们。

你在写代码时是否用到一些其它 Vim 插件?请在评论区留言分享。


via: https://opensource.com/article/19/1/vim-plugins-developers

作者:Ricardo Gerardi 选题:lujun9972 译者:pityonline 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出