用Raku观察冠状病毒大流行
每隔几年,一种新的未知病毒就会出现并开始在全球传播。今年,COVID-19的情况不仅因为病毒的性质,还因为互联网而有所不同。当我们可以立即获取新信息(通常语气偏警觉)时,我们也有能力自己获取数据。
约翰霍普金斯大学系统工程与应用科学中心从不同来源综合了COVID-19数据,并在他们的在线仪表板上显示。他们还在GitHub上发布每日更新的CSV文件。
我决定摄取他们的CSV数据,并使用不同的可视化来减少恐慌,并提供一种快速查看实际数据和趋势的方法。结果是网站covid.observer。源文件可在GitHub 存储库中找到。
多年来,Perl因其BioPerl而闻名。让我们看看Raku能为社会带来什么,因为它在处理文本数据方面非常出色。网站的核心是一个Raku程序和几个模块,用于解析数据并创建静态HTML页面。
我将向您展示Raku为开发者提供的几个最实用的功能。
MAIN
函数
该程序以三种模式运行:解析人口数据、从COVID原始数据获取更新以及生成HTML文件。Raku通过定义不同的MAIN
函数变体为我们提供了一种处理命令行参数的非常方便的方法。每个变体都映射到不同的命令行参数,Raku自动将它们调度到匹配的变体,这有助于我以所需模式运行程序。
以下是变体
multi sub MAIN('population') {
. . .
}
multi sub MAIN('fetch') {
. . .
}
multi sub MAIN('generate') {
. . .
}
我们不需要自己解析命令行选项,也不需要使用Getopt::Long
等模块来为我们完成这项工作。此外,如果程序以错误或缺失的参数运行,Raku会发出“用法”帮助文本
$ ./covid.raku
Usage:
./covid.raku population
./covid.raku fetch
./covid.raku generate
Salve J. Nilsen 建议添加另一个打印初始化数据库的SQL命令的MAIN
函数。此示例显示了如何为命令行选项定义布尔标志
multi sub MAIN('setup', Bool :$force=False, Bool :$verbose=False) {
. . .
}
注意参数名前面的:
。我们稍后会再次看到它。
可以在函数的每个版本之前添加额外的POD注释,以打印更好的用法描述,例如
#| Fetch the latest data and rebuild the database
multi sub MAIN('fetch') {
. . .
}
现在程序打印了更有用的用法消息
Usage:
./covid.raku population -- Parse population CSV files
./covid.raku fetch -- Fetch the latest data and rebuild the database
./covid.raku generate -- Generate the website
减少操作符
减少操作符非常有用。让我提醒您什么是减少操作符。它实际上是一个元操作符:一个被方括号包围的中缀操作符。
在程序中,减少操作符广泛用于跨数据集计算总计(例如,对于世界或中国各省)。让我们检查几个越来越复杂的案例
首先有一个简单的散列,我们需要将其值相加
my %data =
IT => 59_138,
CN => 81_397,
ES => 28_768;
my $total = [+] %data.values;
say $total; # 169303
这是减少操作符的经典用例。我在工作期间发现,[-]
构造在您需要通过几个其他值减少某些值时非常有帮助
my %data =
confirmed => 100,
failed => 1,
recovered => 70;
my $active = [-] %data<confirmed recovered failed>;
say "$active active cases";
my $active = %data<confirmed> - %data<failed> - %data<recovered>;
过滤数据
在我们的第二个案例中,散列值不是标量,而是散列本身。《>>》超操作符可用于提取深层次的数据。让我在简化的数据片段上展示这一点
my %data =
IT => {
confirmed => 59_138,
population => 61, # millions
},
CN => {
confirmed => 81_397,
population => 1434
},
ES => {
confirmed => 28_768,
population => 47,
};
my $total = [+] %data.values>><confirmed>;
say $total; # 169303
使用map
方法访问数据的替代和更干净的方法
my $total2 = [+] %data.values.map: *<confirmed>;
say $total2; # 169303
最后,要排除某个国家/地区的结果,可以在原地对键进行grep
my %x = %data.grep: *.key ne 'CN';
my $excl2 = [+] %x.values.map: *<confirmed>;
say $excl2; # 87906
请注意,调用哈希的 grep
方法比尝试循环遍历键并过滤它们要方便得多。
my $excluding-china =
[+] %data{%data.keys.grep: * ne 'CN'}.values.map: *<confirmed>;
say $excluding-china; # 87906
超运算符
在前一节中,我展示了如何使用 >>
运算符对列表的每个元素执行相同的操作。现在让我们看看我如何使用超运算符 >>->>
来计算数列的增量。
my @confirmed = 10, 20, 40, 70, 150;
my @delta = @confirmed[1..*] >>->> @confirmed;
say @delta; # [10 20 30 80]
该数组包含给定时间段的一系列值。任务是计算每天发生的新病例数。而不是使用循环,可以简单地“减去”一个数组与自身向右移动一个元素。
>>->>
运算符接受两个数据序列:原始数据切片 @confirmed[1..*]
(不带第一个元素),以及原始的 @confirmed
数组。对于给定的二元运算符(本例中的 -
),您可以构建四个超运算符:>>-<<
、>>->>
、<<->>
、<<-<<
。所选形式允许我们在将其应用于 @confirmed[1..*]
时忽略 @confirmed
的末尾额外项。
连接符
让我演示一下我最近发现的一种使用连接符 |
的方法。它选择给定序数的结尾。
for 1..31 -> $day {
my $ending = do given $day {
when 1|21|31 {'st'}
when 2|22 {'nd'}
when 3|23 {'rd'}
default {'th'}
}
say "$day$ending";
}
when
块捕获需要特殊结尾的对应数字。例如,1|21|31
的连接符比正则表达式或比较链更优雅。
可选和命名参数
Raku 中的参数处理很简单。这个函数接受两个位置哈希参数
sub chart-daily(%countries, %totals) {
. . .
}
我可以轻松添加可选命名参数
sub chart-daily(%countries, %totals, :$cc?, :$cont?, :$exclude?) {
. . .
}
名称前面的冒号使参数成为命名参数,而问号使其成为可选参数。我使用它来修改相同统计函数的行为,用于在整个世界、大陆上聚合数据,或者排除单个国家或地区
生成全球数据
chart-daily(%countries, %per-day);
对于单个国家
my $cc = 'CN';
chart-daily(%countries, %per-day, :$cc);
对于大陆
my $cont = 'Europe';
chart-daily(%countries, %per-day, :$cont);
排除中国的全球数据
chart-daily(%countries, %per-day, exclude => 'CN');
获取不含最受影响省份的中国数据
chart-daily(%countries, %per-day, cc => 'CN', exclude => 'CN/HB');
内置模板引擎
该项目生成了200多个HTML文件,因此模板是其重要部分。幸运的是,Raku 有一个强大的内置模板机制,它比简单的变量插值更强大。
一个最小示例是替换变量
return qq:to/HTML/;
<div id="countries-list">
$html
</div>
HTML
顺便说一下,请注意,Raku 允许您通过简单地缩进其闭合符号来保留多行字符串的缩进。结果中不会出现行首的额外空格。
更有趣的是,您可以将Raku代码块嵌入到字符串中,这些块可以包含您在任何模板中间需要做出正确决策的逻辑
my $content = qq:to/HTML/;
<h1>Coronavirus in {$country-name}</h1>
<div class="affected">
{
if $chart2data<confirmed> {
'Affected 1 of every ' ~
fmtnum((1_000_000 * $population /
$chart2data<confirmed>).round())
}
else {
'Nobody affected'
}
}
</div>
HTML
在这里,字符串根据数据自己构建。对于生成的每个国家,字符串“选择”嵌入哪个短语以及如何格式化数字。这个 if
块是相对较大的Raku代码块,它生成一个字符串,该字符串用作花括号内整个块的替代。因此,在这个嵌入的代码块内部,您可以自由地操作外部代码中的数据。
后记
我必须说,使用Raku进行实际项目非常令人兴奋。正如您从示例中看到的,许多它的“奇怪”功能在不同的环境中都非常有用。请查看 GitHub 存储库 中的代码,并关注我博客上关于该网站的更新 在这里。
标签
安德鲁·希托夫
自2000年起,安德鲁一直热衷于Perl 6和Raku,是多本书籍的作者,也是包括三个年度欧洲会议在内的许多Perl和Raku活动的组织者。目前正在撰写他的新书《使用Raku创建编译器》。
浏览他们的文章
反馈
这篇文章有什么问题吗?请在GitHub上打开问题或拉取请求以帮助我们。