分类 技术 下的文章

这是 Python 之禅特别系列的一部分,重点是第十和第十一条原则:沉默的错误(或不沉默)。

 title=

处理“异常情况”是编程中争论最多的问题之一。这可能是因为风险很大:处理不当的错误值甚至可以使庞大的系统瘫痪。由于“异常情况”从本质上来说,是测试不足的,但发生的频率却令人不快,因此,是否正确处理它们往往可以将一个噩梦般的系统与一个“可以工作”的系统区分开来。

从 Java 的 checked 异常,到 Erlang 的故障隔离,再到 Haskell 的 Maybe,不同的语言对错误处理的态度截然不同。

这两条 Python 之禅是 Python 对这个话题的冥思。

错误绝不应该悄悄传递... Errors should never pass silently…

当 Python 之禅在 Tim Peters 眼里闪烁而出之前,在维基百科被俗称为“维基”之前,第一个维基网站 C2 就已经存在了,它是一个编程指南的宝库。这些原则大多来自于 Smalltalk 编程社区。Smalltalk 的思想影响了许多面向对象的语言,包括 Python。

C2 维基定义了 武士原则 Samurai Principle :“胜利归来,要么不归。”用 Python 人的术语来说,它鼓励摒弃 哨兵值 sentinel value ,比如用返回 None-1 来表示无法完成任务,而是采用引发异常的方式。一个 None 是无声的:它看起来像一个值,可以放在一个变量中,然后到处传递。有时,它甚至是一个有效的返回值。

这里的原则是,如果一个函数不能完成它的契约,它应该“高调失败”:引发一个异常。所引发的异常永远不会看起来像是一个可能的值。它将跳过 returned_value = call_to_function(parameter) 行,并上升到调用栈中,可能使程序崩溃。

崩溃的调试是很直接的:有一个堆栈跟踪来指示问题以及调用堆栈。崩溃可能意味着程序的必要条件没有满足,需要人为干预。它可能意味着程序的逻辑有问题。无论是哪种情况,高调失败都比一个隐藏的、“缺失”的值要好。用 None 来感染程序的有效数据,直到它被用在某个地方,就如你可能已经知道的,错误信息会说 “None 没有方法进行拆分”。

除非显式消除 Unless explicitly silenced

有时需要显式地捕获异常。我们可能会预见到文件中的某些行格式错误,并希望以特殊的方式来处理它们,也许可以把它们放在一个“需要人来看看的行”的文件中,而不是让整个程序崩溃。

Python 允许我们用 except 来捕获异常。这意味着错误可以被显式消除。这种明确性意味着 except 行在代码审查中是可见的。质疑为什么应该在这里显式消除异常并从异常中恢复,是有意义的。自问一下我们是否捕获了太多或太少的异常也是有意义的。

因为这些全都是明确的,所以有人可以阅读代码并了解哪些异常是可以恢复的。


via: https://opensource.com/article/19/12/zen-python-errors

作者:Moshe Zadka 选题:lujun9972 译者:wxy 校对:wxy

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

如果某些事情不得不重复做三遍,尝试使其自动化。

 title=

之前写过关于社区管理员的工作,如果你问 10 位社区管理员,你会得到 12 个不同的答案。不过,大多数情况下,你做的是社区需要你在任何特定时刻做的事情。而且很多事情可能是重复的。

当我还是系统管理员时,我遵循一个规则:如果某些事必须做三遍,我会尝试使其自动化。当然,如今,使用诸如 Ansible 这样的强大工具,就有了一整套科学的方法。

我每天或每周要做的一些事情涉及在一些地方查找内容,然后生成信息的摘要或报告,然后发布到别处。这样的任务是自动化的理想选择。这些并不是什么 难事,当我与同事共享其中一些脚本时,总是至少有一个能证明这是有用的。

在 GitHub 上,我有几个每周都要使用的脚本。它们都不是很复杂,但每次都为我节省几分钟。其中一些是用 Perl 写的,因为我快 50 岁了(这些是我早些年写的)。有些是用 Python 写的,因为几年前,我决定要学习 Python。

以下是概述:

tshirts.py

这个简单的 tshirts.py 脚本会根据你要定制的活动 T 恤的数量,然后告诉你尺寸分布是什么。它将这些尺寸分布在一条正态曲线(也称为 “钟形曲线”)上,以我的经验,这和一个正常的会议观众的实际需求非常吻合。如果在美国使用,则可能需要将脚本中的值调整的稍大一些;如果在欧洲使用,则可能需要将脚本中的值稍稍缩小一些。你的情况也许不同。

用法:

[rbowen@sasha:community-tools/scripts]$ ./tshirts.py                                                                                                                                                          
How many shirts? 300
For a total of 300 shirts, order:

30.0 small
72.0 medium
96.0 large
72.0 xl
30.0 2xl

followers.py

followers.py 脚本为我提供了我关心的 Twitter 账号的关注者数量。

该脚本只有 14 行,没什么复杂的,但是它可能节省我十分钟的加载网页和查找数字的时间。

你需要编辑 feed 数组以添加你关心的帐户:

feeds = [
        'centosproject',
        'centos'
        ];

注意:如果你在英语国家以外的地方运行它,则可能无法正常工作,因为它只是一个简单的屏幕抓取脚本,它读取 HTML 并查找其中包含的特定信息。因此,当输出使用其他语言时,正则表达式可能不匹配。

用法:

[rbowen@sasha:community-tools/scripts]$ ./followers.py                                                                                                                                                                          
centosproject: 11,479 Followers
centos: 18,155 Followers

get\_meetups

get\_meetups 脚本S 另一种类别的脚本 —— API脚本。这个特定的脚本使用 meetup.com 网站的 API 来寻找在特定区域和时间范围内特定主题的聚会,以便我可以将它报告给我的社区。你所依赖的许多服务都提供了 API,因此你的脚本可以查找信息,而无需手动查找网页。学习如何使用这些 API 既令人沮丧又耗时,但是最终将获得可以节省大量时间的技能。

免责声明:meetup.com 已于 2019 年 8 月更改了他们的 API,我还没有将这个脚本更新到新的API,所以它现在实际上并没有工作。在接下来的几周内请关注此版本的修复版本。

centos-announcements.pl

centos-announcements.pl 脚本要复杂得多,而且对我的用例来说非常特殊,但你可能有类似的情况。在本例中该脚本查看邮件列表存档(centos-announce 邮件列表),并查找具有特定格式的邮件,然后生成这些邮件的报告。报告有两种不同的格式:一种用于我的每月新闻通讯,另一种用于安排 Twitter 信息(借助于 Hootsuite)。

我使用 Hootsuite 为 Twitter 安排内容,它们具有便捷的 CSV(逗号分隔值)格式,你可以一次批量安排整整一周的推文。从各种数据源(比如:邮件列表、博客、其他网页)自动生成 CSV 格式可以节省大量时间。但是请注意,这可能只应该用于初稿,然后你可以对其进行检查和编辑,以便最终不会自动发布你不想要内容的推文。

reporting.pl

reporting.pl 脚本也是相当特定的,以满足我的特殊需求,但是这个概念本身是通用的。我每月向 CentOS SIG(特殊兴趣小组)发送邮件,这些邮件计划在给定的月份报告。该脚本只是告诉我本月有哪些 SIG,并记录需要发送给他们的电子邮件。

但是,因以下两个原因,实际上并未发送该电子邮件。第一,我希望在消息发送之前对其进行编辑。第二,虽然发送电子邮件的脚本在过去很有效,但如今,很可能被当做垃圾邮件而被过滤。

总结

在这个存储库中还有一些其他脚本,这些脚本或多或少是针对我的特定需求的,但是我希望其中至少有一个脚本对你有用,并且希望这些脚本的多样性能激励你去自动化一些你自己的东西。我也希望看到你的自动化脚本存储库;在评论中链接他们!


via: https://opensource.com/article/20/3/automating-community-management-python

作者:Rich Bowen 选题:lujun9972 译者:stevenzdg988 校对:wxy

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

本文是 Python 之禅特别系列的一部分,重点此篇着眼于第七、八、九条原则:可读性、特殊情况和实用性。

 title=

软件开发是一门充满了取舍的学科。每一个选择,都有一个同样合理但相反的选择。将一个方法私有化?你在鼓励复制粘贴。将一个方法公开?你在过早地对一个接口做出承诺。

软件开发者每时每刻都在做艰难的选择。虽然 Python 之禅 中的所有原则都在一定程度上涵盖了权衡,但下面的原则对一些权衡进行了最艰难、最冷酷的审视。

可读性很重要 Readability counts

从某种意义上说,这一中间原则确实是整个 Python 之禅的中心。这条原则与编写高效的程序无关。在大多数情况下,它甚至与编写健壮的程序也无关。它讲的是编写出别人能读懂的程序

阅读代码,就其本质而言,发生在代码被添加到系统中之后。通常,它会发生在很久很久以后。忽略可读性是最简单的选择,因为它对现在没有伤害。无论添加新代码的原因是什么,它都会对现在造成影响,无论是一个令人痛苦的 bug 还是一个被强烈要求的功能。

如果面对巨大的压力,把可读性扔到一边,只管“解决问题”,而 Python 之禅提醒我们:可读性很重要。编写代码让它适合阅读,无论是对自己还是他人,都是一种慈悲。

特殊情况不足以违反规则 Special cases aren't special enough to break the rules

总是有各种借口:这个 bug 特别麻烦,先简单处理一下吧;这个功能特别紧急,别管美观了;这种情况下所涉及的领域规则特别复杂,嵌套深点也没关系。

一旦我们对特例的借口妥协,大坝就会破裂,就丧失了原则;事情就会演变成一个疯狂麦克斯的荒诞症,每个程序员都会为自己试图找到最好的借口。

纪律需要承诺。只有当事情艰辛、有强烈的诱惑时,才是对一个软件开发人员的考验。总是有合理的借口来破坏规则,这就是为什么必须坚守规矩的原因。纪律就是向例外说不的艺术。没有任何解释可以改变这一点。

虽然,实用性胜过纯洁性 Although, practicality beats purity

“如果你只想着击打、弹跳、撞击、触碰敌人,你将无法真正打倒他。” —— 《宫本武藏:水之卷

归根结底,软件开发是一门实用的学科。它的目标是解决真实的人所面临的实际问题。实用性比纯粹性更重要:首先,我们必须解决问题。如果我们只考虑可读性、简单性或美观性,我们将无法真正解决问题

正如宫本武藏所说的,每一次代码修改的首要目标应该是解决问题。这个问题需要我们心心念念地去解决它。如果我们不以解决问题为目标,只想着 Python 之禅,我们就辜负了这些原则。这是 Python 之禅所固有的另一种矛盾。


via: https://opensource.com/article/19/12/zen-python-trade-offs

作者:Moshe Zadka 选题:lujun9972 译者:wxy 校对:wxy

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

测算网站的文本和背景之间的对比度,以确保站点易于阅读。

如果希望人们发现你的网站实用,那么他们需要能够阅读它。为文本选择的颜色可能会影响网站的可读性。不幸的是,网页设计中的一种流行趋势是在打印输出文本时使用低对比度的颜色,就像在白色背景上的灰色文本。对于 Web 设计师来说,这也许看起来很酷,但对于许多阅读它的人来说确实很困难。

W3C 提供了《 Web 内容可访问性指南 Web Content Accessibility Guidelines 》,其中包括帮助 Web 设计人员选择易于区分文本和背景色的指导。z这就是所谓的“ 对比度 contrast ratio ”。 W3C 定义的对比度需要进行一些计算:给定两种颜色,首先计算每种颜色的相对亮度,然后计算对比度。对比度在 1 到 21 的范围内(通常写为 1:1 到 21:1)。对比度越高,文本在背景下的突出程度就越高。例如,白色背景上的黑色文本非常醒目,对比度为 21:1。对比度为 1:1 的白色背景上的白色文本不可读。

W3C 说,正文 的对比度至少应为 4.5:1,标题至少应为 3:1。但这似乎是最低限度的要求。W3C 还建议正文至少 7:1,标题至少 4.5:1。

计算对比度可能比较麻烦,因此最好将其自动化。我已经用这个方便的 Bash 脚本做到了这一点。通常,脚本执行以下操作:

  1. 获取文本颜色和背景颜色
  2. 计算相对亮度
  3. 计算对比度

获取颜色

你可能知道显示器上的每种颜色都可以用红色、绿色和蓝色(R、G 和 B)来表示。要计算颜色的相对亮度,脚本需要知道颜色的红、绿和蓝的各个分量。理想情况下,脚本会将这些信息读取为单独的 R、G 和 B 值。 Web 设计人员可能知道他们喜欢的颜色的特定 RGB 代码,但是大多数人不知道不同颜色的 RGB 值。作为一种替代的方法是,大多数人通过 “red” 或 “gold” 或 “maroon” 之类的名称来引用颜色。

幸运的是,GNOME 的 Zenity 工具有一个颜色选择器应用程序,可让你使用不同的方法选择颜色,然后用可预测的格式 rgb(R,G,B) 返回 RGB 值。使用 Zenity 可以轻松获得颜色值:

color=$( zenity --title 'Set text color' --color-selection --color='black' )

如果用户(意外地)单击 “Cancel(取消)” 按钮,脚本将假定一种颜色:

if [ $? -ne 0 ] ; then
        echo '** color canceled .. assume black'
        color='rgb(0,0,0)'
fi

脚本对背景颜色值也执行了类似的操作,将其设置为 $background

计算相对亮度

一旦你在 $color 中设置了前景色,并在 $background 中设置了背景色,下一步就是计算每种颜色的相对亮度。 W3C 提供了一个算法 用以计算颜色的相对亮度。

对于 sRGB 色彩空间,一种颜色的相对亮度定义为:

L = 0.2126 * R + 0.7152 * G + 0.0722 * B

R、G 和 B 定义为:

if R sRGB​ <= 0.03928 then R = R sRGB​/12.92

else R = ((R sRGB​+0.055)/1.055) 2.4

if G sRGB​ <= 0.03928 then G = G sRGB​/12.92

else G = ((G sRGB​+0.055)/1.055) 2.4

if B sRGB​ <= 0.03928 then B = B sRGB​/12.92

else B = ((B sRGB​+0.055)/1.055) 2.4

R sRGB​、G sRGB​ 和 B sRGB​ 定义为:

R sRGB​ = R 8bit​/255

G sRGB​ = G 8bit​/255

B sRGB​ = B 8bit​/255

由于 Zenity 以 rgb(R,G,B) 的格式返回颜色值,因此脚本可以轻松拉取分隔开的 R、B 和 G 的值以计算相对亮度。AWK 可以使用逗号作为字段分隔符(-F,),并使用 substr() 字符串函数从 rgb(R,G,B) 中提取所要的颜色值:

R=$( echo $color | awk -F, '{print substr($1,5)}' )
G=$( echo $color | awk -F, '{print $2}' )
B=$( echo $color | awk -F, '{n=length($3); print substr($3,1,n-1)}' )

有关使用 AWK 提取和显示数据的更多信息,查看 AWK 备忘表

最好使用 BC 计算器来计算最终的相对亮度。BC 支持计算中所需的简单 if-then-else,这使得这一过程变得简单。但是由于 BC 无法使用非整数指数直接计算乘幂,因此需要使用自然对数替代它做一些额外的数学运算:

echo "scale=4
rsrgb=$R/255
gsrgb=$G/255
bsrgb=$B/255
if ( rsrgb <= 0.03928 ) r = rsrgb/12.92 else r = e( 2.4 * l((rsrgb+0.055)/1.055) )
if ( gsrgb <= 0.03928 ) g = gsrgb/12.92 else g = e( 2.4 * l((gsrgb+0.055)/1.055) )
if ( bsrgb <= 0.03928 ) b = bsrgb/12.92 else b = e( 2.4 * l((bsrgb+0.055)/1.055) )
0.2126 * r + 0.7152 * g + 0.0722 * b" | bc -l

这会将一些指令传递给 BC,包括作为相对亮度公式一部分的 if-then-else 语句。接下来 BC 打印出最终值。

计算对比度

利用文本颜色和背景颜色的相对亮度,脚本就可以计算对比度了。 W3C 确定对比度 是使用以下公式:

(L1 + 0.05) / (L2 + 0.05),这里的 L1 是颜色较浅的相对亮度, L2 是颜色较深的相对亮度。

给定两个相对亮度值 $r1$r2,使用 BC 计算器很容易计算对比度:

echo "scale=2
if ( $r1 > $r2 ) { l1=$r1; l2=$r2 } else { l1=$r2; l2=$r1 }
(l1 + 0.05) / (l2 + 0.05)" | bc

使用 if-then-else 语句确定哪个值($r1$r2)是较浅还是较深的颜色。BC 执行结果计算并打印结果,脚本可以将其存储在变量中。

最终脚本

通过以上内容,我们可以将所有内容整合到一个最终脚本。 我使用 Zenity 在文本框中显示最终结果:

#!/bin/sh
# script to calculate contrast ratio of colors

# read color and background color:
# zenity returns values like 'rgb(255,140,0)' and 'rgb(255,255,255)'

color=$( zenity --title 'Set text color' --color-selection --color='black' )
if [ $? -ne 0 ] ; then
        echo '** color canceled .. assume black'
        color='rgb(0,0,0)'
fi

background=$( zenity --title 'Set background color' --color-selection --color='white' )
if [ $? -ne 0 ] ; then
        echo '** background canceled .. assume white'
        background='rgb(255,255,255)'
fi

# compute relative luminance:

function luminance()
{
        R=$( echo $1 | awk -F, '{print substr($1,5)}' )
        G=$( echo $1 | awk -F, '{print $2}' )
        B=$( echo $1 | awk -F, '{n=length($3); print substr($3,1,n-1)}' )

        echo "scale=4
rsrgb=$R/255
gsrgb=$G/255
bsrgb=$B/255
if ( rsrgb <= 0.03928 ) r = rsrgb/12.92 else r = e( 2.4 * l((rsrgb+0.055)/1.055) )
if ( gsrgb <= 0.03928 ) g = gsrgb/12.92 else g = e( 2.4 * l((gsrgb+0.055)/1.055) )
if ( bsrgb <= 0.03928 ) b = bsrgb/12.92 else b = e( 2.4 * l((bsrgb+0.055)/1.055) )
0.2126 * r + 0.7152 * g + 0.0722 * b" | bc -l
}

lum1=$( luminance $color )
lum2=$( luminance $background )

# compute contrast

function contrast()
{
        echo "scale=2
if ( $1 > $2 ) { l1=$1; l2=$2 } else { l1=$2; l2=$1 }
(l1 + 0.05) / (l2 + 0.05)" | bc
}

rel=$( contrast $lum1 $lum2 )

# print results

( cat<<EOF
Color is $color on $background

Contrast ratio is $rel
Contrast ratios can range from 1 to 21 (commonly written 1:1 to 21:1).

EOF

if [ ${rel%.*} -ge 4 ] ; then
        echo "Ok for body text"
else
        echo "Not good for body text"
fi
if [ ${rel%.*} -ge 3 ] ; then
        echo "Ok for title text"
else
        echo "Not good for title text"
fi

cat<<EOF

W3C 说明:

1.4.3 对比度(最小值):文本和文本图像的视觉呈现方式的对比度至少为 4.5:1,但以下情况除外:(AA 级)

    大文本:大文本和大文本图像的对比度至少为 3:1;

    附带说明:作为非活动用户界面组件一部分,纯装饰的,任何人都不可见或图片的一部分包含特定的其他可视内容的文本或文本图像没有对比度要求。

    小示意图:徽标或商标名称中的文本没有最低对比度要求。

1.4.6 对比度(增强):文本和文本图像的视觉表示具有至少 7:1 的对比度,但以下情况除外:(AAA 级)

    大文本:大文本和大文本图像的对比度至少为 4.5:1;

    附带说明:作为非活动用户界面组件一部分,纯装饰的,任何人都不可见或图片的一部分包含特定的其他可视内容的文本或文本图像没有对比度要求。
 
    小示意图:徽标或商标名称中的文本没有最低对比度要求。
EOF
) | zenity --text-info --title='Relative Luminance' --width=800 --height=600

最后,我希望提供有关 W3C 建议的参考信息,以提醒自己。

Zenity 颜色选择器完成了所有解释颜色的艰苦工作,用户可以通过单击色轮或输入值来选择颜色。 Zenity 接受网站上使用的标准十六进制颜色值,例如 #000000#000rgb(0,0,0)(所有这些均为黑色)。这是白色背景上的黑色文本的示例计算:

Zenity 还识别标准的颜色名称,如“cadetblue”、“orange”或“gold”。在Zenity 中输入颜色名称,然后点击 Tab 键,Zenity 会将颜色名称转换为十六进制颜色值,如以下示例中对金色背景上的黑色文本的计算:


via: https://opensource.com/article/19/2/make-websites-more-readable-shell-script

作者:Jim Hall 选题:lujun9972 译者:stevenzdg988 校对:wxy

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

通过基本的 Python 工具获得爬取完整 HTML 网站的实践经验。

 title=

有很多很棒的书可以帮助你学习 Python ,但是谁真正读了这那些大部头呢?(剧透:反正不是我)。

许多人觉得教学书籍很有用,但我通常不会从头到尾地阅读一本书来学习。我是通过做一个项目,努力的弄清楚一些内容,然后再读另一本书来学习。因此,暂时丢掉书,让我们一起学习 Python。

接下来是我的第一个 Python 爬取项目的指南。它对 Python 和 HTML 的假定知识要求很低。这篇文章旨在说明如何使用 Python 的 requests 库访问网页内容,并使用 BeatifulSoup4 库以及 JSON 和 pandas 库解析网页内容。我将简要介绍 Selenium 库,但我不会深入研究如何使用该库——这个主题值得有自己的教程。最终,我希望向你展示一些技巧和小窍门,以减少网页爬取过程中遇到的问题。

安装依赖

我的 GitHub 存储库 中提供了本指南的所有资源。如果需要安装 Python3 的帮助,请查看 LinuxWindowsMac 的教程。

$ python3 -m venv
$ source venv/bin/activate
$ pip install requests bs4 pandas

如果你喜欢使用 JupyterLab ,则可以使用 notebook 运行所有代码。安装 JupyterLab 有很多方法,这是其中一种:

# from the same virtual environment as above, run:
$ pip install jupyterlab

为网站抓取项目设定目标

现在我们已经安装了依赖项,但是爬取网页需要做什么?

让我们退一步,确保使目标清晰。下面是成功完成网页爬取项目需求列表:

  • 我们收集的信息,是值得我们花大力气去建立一个有效的网页爬取器的。
  • 我们所下载的信息是可以通过网页爬取器合法和道德地收集的。
  • 对如何在 HTML 代码中找到目标信息有一定的了解。
  • 利用恰当的工具:在此情况下,需要使用 BeautifulSoup 库和 requests 库。
  • 知道(或愿意去学习)如何解析 JSON 对象。
  • 有足够的 pandas 数据处理技能。

关于 HTML 的备注:HTML 是运行在互联网上的“猛兽”,但我们最需要了解的是标签的工作方式。标签是一对由尖括号包围关键词(一般成对出现,其内容在两个标签中间)。比如,这是一个假装的标签,称为 pro-tip

<pro-tip> All you need to know about html is how tags work </pro-tip>

我们可以通过调用标签 pro-tip 来访问其中的信息(All you need to know…)。本教程将进一步介绍如何查找和访问标签。要进一步了解 HTML 基础知识,请查看 本文

网站爬取项目中要找的是什么

有些数据利用网站爬取采集比利用其他方法更合适。以下是我认为合适项目的准则:

没有可用于数据(处理)的公共 API。通过 API 抓取结构化数据会容易得多,(所以没有 API )有助于澄清收集数据的合法性和道德性。而有相当数量的结构化数据,并有规律的、可重复的格式,才能证明这种努力的合理性。网页爬取可能会很痛苦。BeautifulSoup(bs4)使操作更容易,但无法避免网站的个别特殊性,需要进行定制。数据的相同格式化不是必须的,但这确实使事情变得更容易。存在的 “边际案例”(偏离规范)越多,爬取就越复杂。

免责声明:我没有参加过法律培训;以下内容无意作为正式的法律建议。

关于合法性,访问大量有价值信息可能令人兴奋,但仅仅因为它是可能的,并不意味着应该这样做。

值得庆幸的是,有一些公共信息可以指导我们的道德规范和网页爬取工具。大多数网站都有与该网站关联的 robots.txt 文件,指出允许哪些爬取活动,哪些不被允许。它主要用于与搜索引擎(网页抓取工具的终极形态)进行交互。然而,网站上的许多信息都被视为公共信息。因此,有人将 robots.txt 文件视为一组建议,而不是具有法律约束力的文档。 robots.txt 文件并不涉及数据的道德收集和使用等主题。

在开始爬取项目之前,问自己以下问题:

  • 我是否在爬取版权材料?
  • 我的爬取活动会危害个人隐私吗?
  • 我是否发送了大量可能会使服务器超载或损坏的请求?
  • 爬取是否会泄露出我不拥有的知识产权?
  • 是否有规范网站使用的服务条款,我是否遵循了这些条款?
  • 我的爬取活动会减少原始数据的价值吗?(例如,我是否打算按原样重新打包数据,或者可能从原始来源中抽取网站流量)?

当我爬取一个网站时,请确保可以对所有这些问题回答 “否”。

要深入了解这些法律问题,请参阅 2018 年出版的 Krotov 和 Silva 撰写的《Web 爬取的合法性和道德性》 和 Sellars 的《二十年 Web 爬取和计算机欺诈与滥用法案》

现在开始爬取网站

经过上述评估,我想出了一个项目。我的目标是爬取爱达荷州所有 Family Dollar 商店的地址。 这些商店在农村地区规模很大,因此我想了解有多少家这样的商店。

起点是 Family Dollar 的位置页面

 title=

首先,让我们在 Python 虚拟环境中加载先决条件。此处的代码将被添加到一个 Python 文件(如果你想要个名称,则为 scraper.py)或在 JupyterLab 的单元格中运行。

import requests # for making standard html requests
from bs4 import BeautifulSoup # magical tool for parsing html data
import json # for parsing data
from pandas import DataFrame as df # premier library for data organization

接下来,我们从目标 URL 中请求数据。

page = requests.get("https://locations.familydollar.com/id/")
soup = BeautifulSoup(page.text, 'html.parser')

BeautifulSoup 将 HTML 或 XML 内容转换为复杂树对象。这是我们将使用的几种常见对象类型。

  • BeautifulSoup —— 解析的内容
  • Tag —— 标准 HTML 标记,这是你将遇到的 bs4 元素的主要类型
  • NavigableString —— 标签内的文本字符串
  • Comment —— NavigableString 的一种特殊类型

当我们查看 requests.get() 输出时,还有更多要考虑的问题。我仅使用 page.text() 将请求的页面转换为可读的内容,但是还有其他输出类型:

  • page.text() 文本(最常见)
  • page.content() 逐字节输出
  • page.json() JSON 对象
  • page.raw() 原始套接字响应(对你没啥用)

我只在使用拉丁字母的纯英语网站上操作。 requests 中的默认编码设置可以很好地解决这一问题。然而,除了纯英语网站之外,就是更大的互联网世界。为了确保 requests 正确解析内容,你可以设置文本的编码:

page = requests.get(URL)
page.encoding = 'ISO-885901'
soup = BeautifulSoup(page.text, 'html.parser')

仔细研究 BeautifulSoup 标签,我们看到:

  • bs4 元素 tag 捕获的是一个 HTML 标记。
  • 它具有名称和属性,可以像字典一样访问:tag['someAttribute']
  • 如果标签具有相同名称的多个属性,则仅访问第一个实例。
  • 可通过 tag.contents 访问子标签。
  • 所有标签后代都可以通过 tag.contents 访问。
  • 你始终可以使用以下字符串:re.compile("your_string") 访问一个字符串的所有内容,而不是浏览 HTML 树。

确定如何提取相应内容

警告:此过程可能令人沮丧。

网站爬取过程中的提取可能是一个令人生畏的充满了误区的过程。我认为解决此问题的最佳方法是从一个有代表性的示例开始然后进行扩展(此原理对于任何编程任务都是适用的)。查看页面的 HTML 源代码至关重要。有很多方法可以做到这一点。

你可以在终端中使用 Python 查看页面的整个源代码(不建议使用)。运行此代码需要你自担风险:

print(soup.prettify())

虽然打印出页面的整个源代码可能适用于某些教程中显示的玩具示例,但大多数现代网站的页面上都有大量内容。甚至 404 页面也可能充满了页眉、页脚等代码。

通常,在你喜欢的浏览器中通过 “查看页面源代码” 来浏览源代码是最容易的(单击右键,然后选择 “查看页面源代码” )。这是找到目标内容的最可靠方法(稍后我将解释原因)。

 title=

在这种情况下,我需要在这个巨大的 HTML 海洋中找到我的目标内容 —— 地址、城市、州和邮政编码。通常,对页面源(ctrl+F)的简单搜索就会得到目标位置所在的位置。一旦我实际看到目标内容的示例(至少一个商店的地址),便会找到将该内容与其他内容区分开的属性或标签。

首先,我需要在爱达荷州 Family Dollar 商店中收集不同城市的网址,并访问这些网站以获取地址信息。这些网址似乎都包含在 href 标记中。太棒了!我将尝试使用 find_all 命令进行搜索:

dollar_tree_list = soup.find_all('href')
dollar_tree_list

搜索 href 不会产生任何结果,该死。这可能是因为 href 嵌套在 itemlist 类中而失败。对于下一次尝试,请搜索 item_list。由于 class 是 Python 中的保留字,因此使用 class_ 来作为替代。soup.find_all() 原来是 bs4 函数的瑞士军刀。

dollar_tree_list = soup.find_all(class_ = 'itemlist')
for i in dollar_tree_list[:2]:
  print(i)

有趣的是,我发现搜索一个特定类的方法一般是一种成功的方法。通过找出对象的类型和长度,我们可以了解更多有关对象的信息。

type(dollar_tree_list)
len(dollar_tree_list)

可以使用 .contents 从 BeautifulSoup “结果集” 中提取内容。这也是创建单个代表性示例的好时机。

example = dollar_tree_list[2] # a representative example
example_content = example.contents
print(example_content)

使用 .attr 查找该对象内容中存在的属性。注意:.contents 通常会返回一个项目的精确的列表,因此第一步是使用方括号符号为该项目建立索引。

example_content = example.contents[0]
example_content.attrs

现在,我可以看到 href 是一个属性,可以像字典项一样提取它:

example_href = example_content['href']
print(example_href)

整合网站抓取工具

所有的这些探索为我们提供了前进的路径。这是厘清上面逻辑的一个清理版本。

city_hrefs = [] # initialise empty list

for i in dollar_tree_list:
    cont = i.contents[0]
    href = cont['href']
    city_hrefs.append(href)

#  check to be sure all went well
for i in city_hrefs[:2]:
  print(i)

输出的内容是一个关于抓取爱达荷州 Family Dollar 商店 URL 的列表。

也就是说,我仍然没有获得地址信息!现在,需要抓取每个城市的 URL 以获得此信息。因此,我们使用一个具有代表性的示例重新开始该过程。

page2 = requests.get(city_hrefs[2]) # again establish a representative example
soup2 = BeautifulSoup(page2.text, 'html.parser')

 title=

地址信息嵌套在 type="application/ld+json" 里。经过大量的地理位置抓取之后,我开始认识到这是用于存储地址信息的一般结构。幸运的是,soup.find_all() 开启了利用 type 搜索。

arco = soup2.find_all(type="application/ld+json")
print(arco[1])

地址信息在第二个列表成员中!原来如此!

使用 .contents 提取(从第二个列表项中)内容(这是过滤后的合适的默认操作)。同样,由于输出的内容是一个列表,因此我为该列表项建立了索引:

arco_contents = arco[1].contents[0]
arco_contents

喔,看起来不错。此处提供的格式与 JSON 格式一致(而且,该类型的名称中确实包含 “json”)。 JSON 对象的行为就像是带有嵌套字典的字典。一旦你熟悉利用其去工作,它实际上是一种不错的格式(当然,它比一长串正则表达式命令更容易编程)。尽管从结构上看起来像一个 JSON 对象,但它仍然是 bs4 对象,需要通过编程方式转换为 JSON 对象才能对其进行访问:

arco_json =  json.loads(arco_contents)
type(arco_json)
print(arco_json)

在该内容中,有一个被调用的 address 键,该键要求地址信息在一个比较小的嵌套字典里。可以这样检索:

arco_address = arco_json['address']
arco_address

好吧,请大家注意。现在我可以遍历存储爱达荷州 URL 的列表:

locs_dict = [] # initialise empty list

for link in city_hrefs:
  locpage = requests.get(link)   # request page info
  locsoup = BeautifulSoup(locpage.text, 'html.parser')
      # parse the page's content
  locinfo = locsoup.find_all(type="application/ld+json")
      # extract specific element
  loccont = locinfo[1].contents[0]  
      # get contents from the bs4 element set
  locjson = json.loads(loccont)  # convert to json
  locaddr = locjson['address'] # get address
  locs_dict.append(locaddr) # add address to list

用 Pandas 整理我们的网站抓取结果

我们在字典中装载了大量数据,但是还有一些额外的无用项,它们会使重用数据变得比需要的更为复杂。要执行最终的数据组织,我们需要将其转换为 Pandas 数据框架,删除不需要的列 @typecountry,并检查前五行以确保一切正常。

locs_df = df.from_records(locs_dict)
locs_df.drop(['@type', 'addressCountry'], axis = 1, inplace = True)
locs_df.head(n = 5)

确保保存结果!!

df.to_csv(locs_df, "family_dollar_ID_locations.csv", sep = ",", index = False)

我们做到了!所有爱达荷州 Family Dollar 商店都有一个用逗号分隔的列表。多令人兴奋。

Selenium 和数据抓取的一点说明

Selenium 是用于与网页自动交互的常用工具。为了解释为什么有时必须使用它,让我们来看一个使用 Walgreens 网站的示例。 “检查元素” 提供了浏览器显示内容的代码:

 title=

虽然 “查看页面源代码” 提供了有关 requests 将获得什么内容的代码:

 title=

如果这两个不一致,是有一些插件可以修改源代码 —— 因此,应在将页面加载到浏览器后对其进行访问。requests 不能做到这一点,但是 Selenium 可以做到。

Selenium 需要 Web 驱动程序来检索内容。实际上,它会打开 Web 浏览器,并收集此页面的内容。Selenium 功能强大 —— 它可以通过多种方式与加载的内容进行交互(请阅读文档)。使用 Selenium 获取数据后,继续像以前一样使用 BeautifulSoup:

url = "https://www.walgreens.com/storelistings/storesbycity.jsp?requestType=locator&state=ID"
driver = webdriver.Firefox(executable_path = 'mypath/geckodriver.exe')
driver.get(url)
soup_ID = BeautifulSoup(driver.page_source, 'html.parser')
store_link_soup = soup_ID.find_all(class_ = 'col-xl-4 col-lg-4 col-md-4')

对于 Family Dollar 这种情形,我不需要 Selenium,但是当呈现的内容与源代码不同时,我确实会保留使用 Selenium。

小结

总之,使用网站抓取来完成有意义的任务时:

  • 耐心一点
  • 查阅手册(它们非常有帮助)

如果你对答案感到好奇:

 title=

美国有很多 Family Dollar 商店。

完整的源代码是:

import requests
from bs4 import BeautifulSoup
import json
from pandas import DataFrame as df

page = requests.get("https://www.familydollar.com/locations/")
soup = BeautifulSoup(page.text, 'html.parser')

# find all state links
state_list = soup.find_all(class_ = 'itemlist')

state_links = []

for i in state_list:
    cont = i.contents[0]
    attr = cont.attrs
    hrefs = attr['href']
    state_links.append(hrefs)

# find all city links
city_links = []

for link in state_links:
    page = requests.get(link)
    soup = BeautifulSoup(page.text, 'html.parser')
    familydollar_list = soup.find_all(class_ = 'itemlist')
    for store in familydollar_list:
        cont = store.contents[0]
        attr = cont.attrs
        city_hrefs = attr['href']
        city_links.append(city_hrefs)
# to get individual store links
store_links = []

for link in city_links:
    locpage = requests.get(link)
    locsoup = BeautifulSoup(locpage.text, 'html.parser')
    locinfo = locsoup.find_all(type="application/ld+json")
    for i in locinfo:
        loccont = i.contents[0]
        locjson = json.loads(loccont)
        try:
            store_url = locjson['url']
            store_links.append(store_url)
        except:
            pass

# get address and geolocation information
stores = []

for store in store_links:
    storepage = requests.get(store)
    storesoup = BeautifulSoup(storepage.text, 'html.parser')
    storeinfo = storesoup.find_all(type="application/ld+json")
    for i in storeinfo:
        storecont = i.contents[0]
        storejson = json.loads(storecont)
        try:
            store_addr = storejson['address']
            store_addr.update(storejson['geo'])
            stores.append(store_addr)
        except:
            pass

# final data parsing
stores_df = df.from_records(stores)
stores_df.drop(['@type', 'addressCountry'], axis = 1, inplace = True)
stores_df['Store'] = "Family Dollar"

df.to_csv(stores_df, "family_dollar_locations.csv", sep = ",", index = False)

作者注释:本文改编自 2020 年 2 月 9 日在俄勒冈州波特兰的我在 PyCascades 的演讲


via: https://opensource.com/article/20/5/web-scraping-python

作者:Julia Piaskowski 选题:lujun9972 译者:stevenzdg988 校对:wxy

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

只要付出一点点努力,你就可以帮助下一个人,不只是绘制出安全路径,还可以留下危险的警告。

 title=

在博客圈里,人们对基础架构即代码、持续集成/持续交付(CI/CD)管道、代码审查和测试制度赞不绝口,但人们很容易忘记,这种精心设计的象牙塔只是一种理想,而不是现实。虽然不完美的系统困扰着我们,但我们必须交付一些东西。

在系统自动化的过程中,很少有比那些通过粘合 API 创建的象牙塔更脆弱的塔。这是一个脆弱的世界。要让它“工作起来”,交付它,然后继续前进,压力巨大。

要解决的问题

想象一个简单的功能请求:编写一些 Ansible 代码,在外部系统中创建几条记录,以记录一个 VLAN 的一些详细信息。我最近很想做一些实验室的管理工作来完成这个任务。这个外部系统是一个常见的 互联网协议地址管理 Internet Protocol Address Management (IPAM)工具,但对于一个更抽象的 配置管理数据库 Configuration Management DataBase (CMDB)或一个与网络无关的记录来说,困难是一样的。在这个例子中,我创建一个记录的直接愿望就是让系统保存记录而已。

如果我们的目标是一个超紧凑的、直接的、笨拙的宏,那么它可能用 100 行代码就能写出来。如果我记得 API,我也许能在一个小时内把它敲出来,该代码的作用不会超过预期,除了确切的成品之外,什么也没留下。对它的目的而言是完美的,但是对未来的扩展毫无用处。

如今,我希望几乎每个人都能从一个 角色 role 和几个 任务 task 文件开始这项任务,准备扩展到十几个创建、读取、更新和删除(CRUD)操作。因为我不了解这个 API,我可能会花上几个小时到几天的时间,仅仅是摆弄它,弄清楚它的内部模式和工艺,弥和它的功能和我用代码编写出来的意图之间的差距。

在研究 API 的时候,我发现创建一个 VLAN 记录需要一个父对象引用 vlan_view_ref。这看起来像一个路径片段,里面有随机字符。也许它是一个哈希,也许它真的是随机的,我不确定。我猜想,许多在泥泞中挣扎的人,在迫在眉睫的截止日期前,可能会把这个任意的字符串复制粘贴到 Ansible 中,然后继续混下去。忽略这个 角色 role 的实现细节,显而易见这个 剧本 playbook 级的任务应该是这样:

- name: "Create VLAN"
  include_role:
    name: otherthing
    tasks_from: vlan_create.yml
  vars:
    vlan_name: "lab-infra"
    vlan_tag: 100
    vlan_view_ref: "vlan_view/747f602d-0381"

不幸的是,除了通过 API,vlan_view_ref 标识符是不可用的,所以即使把它移到 清单文件 inventory 或额外的变量中也没有什么帮助。 剧本 playbook 的用户需要对系统有一些更深入的理解,才能找出正确的引用 ID。

在实验室建设的情况下,我会经常重新部署这个记录系统。因此,这个父对象引用 ID 每天都会发生变化,我不希望每次都要手动找出它。所以,我肯定要按名称搜索该引用。没问题:

- name: Get Lab vlan view reference
  include_role:
    name: otherthing
    tasks_from: search_for.yml
  vars:
    _resource: vlan_view
    _query: "name={{ vlan_parent_view_name }}"

最终,它进行了一个 REST 调用。这将“返回” 一个 JSON,按照惯例,为了便于在角色外访问,我把它填充进了 _otherthing_search_result 中,。search_for.yml 的实现是抽象的,它总是返回一个包含零或多个结果的字典。

正如我读过的几乎所有真实世界的 Ansible 代码所证明的那样,大多数 Ansible 开发者将会继续前进,好像一切都很好,并且可以直接访问预期的单个结果:

- name: Remember our default vlan view ref
  set_fact:
    _thatthig_vlan_view_ref: "{{ _otherthing_search_result[0]._ref }}"

- name: "Create VLAN"
  include_role:
    name: otherthing
    tasks_from: vlan_create.yml
  vars:
    vlan_name: "lab-infra"
    vlan_tag: 100
    vlan_view_ref: "{{ vlan_parent_view_name }}"

但有时 _otherthing_search_result[0] 是未定义的,所以 _thatthig_vlan_view_ref 也将是未定义的。很有可能是因为代码运行在不同的真实环境中,而有人忘记了在清单中或在命令行中更新 {{ vlan_parent_view_name }}。或者,无论公平与否,也许有人进入了工具的图形用户界面(GUI),删除了记录或更改了它的名称什么的。

我知道你在想什么。

“好吧,不要这样做。这是一个没有哑巴的场所。不要那么笨。”

也许我对这种情况还算满意,反驳道:“Ansible 会很正确的告诉你错误是:list 对象没有元素 0,甚至会带个行号。你还想怎样?”作为开发者,我当然知道这句话的意思 —— 我刚写的代码。我刚从三天的和 API 斗智斗勇中走出来,我的脑子很清醒。

明天是另一个故事

但是到了明天,我可能会忘记什么是父对象引用,我肯定会忘记第 30 行上的内容。如果一个月后出了问题,就算你能找到我,我也得花一个下午的时间重新解读 API 指南,才能搞清楚到底出了什么问题。

而如果我出门了呢?如果我把代码交给了一个运维团队,也许是一个实习生通过 Tower 来运行,把 vlan_view_name 手动输入到表单之类的东西呢?那第 30 行出的问题是对他们没有帮助的。

你说,加注释吧! 嗯,是的。我可以在代码中写一些梗概,以帮助下周或下个月的开发人员。这对运行代码的人没有帮助,他的“工作”刚刚失败,当然对于企业也无济于事。

记住,我们此刻无所不能。在写代码或者跳过写代码的时候,我们是站在实力和知识的立场上进行的。我们花了几个小时,甚至几天的时间,研究了文档、现实、其他 bug、其他问题,我们留下了代码、注释,甚至可能还有文档。我们写的代码是分享成功的,而成功正是我们用户想要的。但是在这种学习中也有很多失败的地方,我们也可以留下这些。

在代码中留言

“第 30 行有错误”对任何人都没有帮助。至少,我可以用更好的错误信息来处理明显的错误情况:

  - name: Fail if zero vlan views returned
     fail:
       msg: "Got 0 results from searching for VLAN view {{ vlan_parent_view_name }}. Please verify exists in otherthing, and is accessible by the service account."
     when: _otherthing_search_result | length == 0

在这四行代码中(没有额外的思考),我把具体的、有用的建议留给了下一个人 —— 那个无助的运维团队成员,或者更有可能是一个月后的我 —— 这是关于现实世界中的问题,其实根本不是关于代码的。这条消息可以让任何人发现一个简单的复制/粘贴错误,或者记录系统发生了变化。不需要 Ansible 知识,不需要凌晨 3 点给开发人员发短信“看看第 30 行”。

但是等等!还有更多!

在了解 otherthing 的过程中,我了解到它在一个关键的方面,嗯,还挺笨的。它的许多记录类型(如果不是全部的话)没有唯一性约束,可能存在几个相同的记录。VLAN 视图被定义为有一个名称、一个开始 ID 和一个结束 ID;其他记录类型也同样简单,显然这应该是一个唯一的元组 —— 基于现实和数据库规范化的抽象概念。但 otherthing 允许重复的元组,尽管在概念上讲永远不可能。

在我的实验室里,我很乐意尝试并记住不要这样做。在企业生产环境中,我可能会写一个策略。不管是哪种方式,经验告诉我,系统会被破坏,会在倒霉的时候被破坏,而且可能需要很长时间才能让这些问题发酵成,嗯,一个问题。

对于 “第 30 行有错误”,一个本来有丰富经验的 Ansible 开发者可能会认识到这是“记录没有找到”,而不用知道其他的事情就足以解决这个问题。但如果 _otherthing_search_result[0] 只有有时是正确的 vlan_view_ref,那就糟糕多了,它让整个世界被破坏,而悄无声息。而这个错误可能完全表现在其他地方,也许六个月后的安全审计会将其标记为记录保存不一致,如果有多种工具和人工访问方式,可能需要几天或几周的时间才能发现这个特定代码出错的事实。

在几天对 API 的摸索中,我学到了这一点。我不是在找问题,如果有记录,我没有看到。所以我来到了这篇文章的重点。我没有因为它是一个实验室,修复它,然后继续前进而忽略了这种不可能的情况,而是花了两分钟留下了\_代码\_ —— 不是注释,不是心理笔记,不是文档 —— 而是会一直运行的代码,涵盖了这种不可能的情况:

  - name: Fail if > 1 views returned
     fail:
       msg: "Got {{ _otherthing_search_result | length }} results from searching for VLAN view {{ vlan_parent_view_name }}. Otherthing allows this, but is not handled by this code."
     when: _otherthing_search_result | length > 1

我手动创建了失败条件,所以我可以手动测试这个条件。我希望它永远不会在实际使用中运行,但我觉得它会。

如果(当)这个错误发生在生产环境中,那么有人可以决定该怎么做。我希望他们能修复坏数据。如果它经常发生,我希望他们能追踪到另一个损坏的系统。如果他们要求删除这段代码,而这段代码做了未定义和错误的事情,那是他们的特权,也是我不想工作的地方。代码是不完美的,但它是完整的。这是匠人的工作。

现实世界中的自动化是一个迭代的过程,它与不完美的系统进行斗争,并平等地使用。它永远不会处理所有的特殊情况。它甚至可能无法处理所有的正常情况。通过 Lint、代码审查和验收测试的工作代码是处理安全和所需路径的代码。只要付出一点点努力,你就可以帮助下一个人,不仅仅是绘制安全路径,还可以对你发现的危险留下警告。


via: https://opensource.com/article/21/1/improve-ansible-play

作者:Jeff Warncia 选题:lujun9972 译者:wxy 校对:wxy

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