用Raku观察冠状病毒大流行

每隔几年,一种新的未知病毒就会出现并开始在全球传播。今年,COVID-19的情况不仅因为病毒的性质,还因为互联网而有所不同。当我们可以立即获取新信息(通常语气偏警觉)时,我们也有能力自己获取数据。

约翰霍普金斯大学系统工程与应用科学中心从不同来源综合了COVID-19数据,并在他们的在线仪表板上显示。他们还在GitHub上发布每日更新的CSV文件。

我决定摄取他们的CSV数据,并使用不同的可视化来减少恐慌,并提供一种快速查看实际数据和趋势的方法。结果是网站covid.observer。源文件可在GitHub 存储库中找到。

多年来,Perl因其BioPerl而闻名。让我们看看Raku能为社会带来什么,因为它在处理文本数据方面非常出色。网站的核心是一个Raku程序和几个模块,用于解析数据并创建静态HTML页面。


covid-observer

我将向您展示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";

使用%h形式的散列切片也有助于使代码更紧凑。与直接方法相比

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上打开问题或拉取请求以帮助我们。