第十五天 - 实用的Web内容Munging

img

继brian d foy 从第5天开始使用Mojo::DOM选择器的精彩文章,我认为谈论我最近使用Mojo::UserAgentMojo::DOM构建的一些网站迁移脚本会很有趣,以便展示这些模块的一些基本实际用法。我之前从未真正使用过Mojo,但最近我需要迁移一个大约15年没有重新设计的网站,这似乎非常适合我的内容修改需求。在过去,我会使用正则表达式,并且可能花费至少与我编写代码时手动按摩输入或输出到正确形状的时间。Mojo::DOM让我很容易,一个 Mojolicious 初学者,很快就能得到我想要的结果。

从静态站点到静态站点生成器

我打算解决的问题是使用曾经在SourceForge.net上托管的旧静态网站,并将其迁移到令人兴奋的新…嗯…静态网站。但是,这一次,它将成为静态网站的现代版本。而不是手动编辑HTML并使用自制的页面munging脚本来执行诸如使用正则表达式在内容div顶部插入新闻项目或更改日志条目,我将使用现代静态网站生成器。有几个可供选择,包括用Ruby编写的着名的Jekyll,用Go构建的Hugo,以及在Perl中运行此站点的Statocles。对于我的项目,我选择了Hugo,因为它的速度和成熟度。

与大多数现代静态站点生成器一样,Hugo希望内容采用Markdown格式,文件顶部有一些元数据。我想把我们硬梆梆的旧HTML转换成下面的例子,看起来有点像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
title: "Frobnitz 3.141593 released"
date: 2016-10-10
description: "This release includes a fog-flavored bauble of three equal sides, providing the restless..."
categories: []
aliases: []
toc: false
draft: false
---
This release includes a fog-flavored bauble of three equal sides, providing
the restless digital spirits a brief respite from their painful awareness of
impermanence.

You can find the new version under the usual shadowy bridge.

所以,首先,我需要获取旧网站。我本可以打扰网站的旧维护者,并获得原始资源,但我决定只是从网上获取它们。一种选择是使用wget或curl; 我们稍后用Mojo::DOM做的事情可以从任何地方使用HTML,包括本地文件。但是,在Perl中完成所有操作似乎更简单。因此,我们将获取页面路径列表,然后对它们进行处理。Mojo::UserAgent努力工作。最简单的示例可能如下所示:

1
2
3
4
5
use Mojo::UserAgent;

my $url = 'intro';
my $ua = Mojo::UserAgent->new;
my $tx = $ua->get("example.tld/$url.html");

而已!我们刚刚获取了一个网页。您可能想要打印 $tx 以查看其中的内容(这就是我所做的,而不是首先阅读文档)。但是,它是一个Mojo::Transaction::HTTP对象。我们必须深入了解层次结构,首先查看res属性,它是一个Mojo::Message::Response对象,它有一个body方法:

1
print $tx->res->body();

这将显示响应正文及其HTML内容。但是,这并不是很有趣。但是,我们可以通过Mojo响应轻松地完成更多有趣和强大的事情。

CSS选择器

res响应对象提供了一个dom对象,给出了选择使用CSS选择HTML文档的部件的能力。所以,如果我有一个带#maindiv 的文档,我可以使用以下内容检索该div的内容:

1
my $main = $tx->res->dom->find("#main");

当然,如果你熟悉CSS选择器,你知道它可以比那更精确。那么,让我们谈谈具体的事情。就我而言,我有几种不同类型的页面。一个是新闻项目的页面,其中有许多部分明确地描述为人类读者,但它们不在他们自己的div中,或者在HTML解析器可以分辨的情况下分组。它们只是一堆模糊的HTML形状。

我想将这些新闻项目分成各自的页面,然后可以按照我喜欢的方式进行聚合,例如将它们放在分页列表中,或者将最新项目包含在首页的div中。网站。

这些新闻在旧网站上看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<h1>Latest News</h1>

<h3>Frobnitz 4.5 released</h3>
<p>
This release improves castigation of the widely formed sonterols.
<br>
Current users may upgrade by applying coil oil to application points A, C, W,
DF, Y0, IN34, RS232, and Frank, then gently inserting the conjubilant apparatus
into the ferulic treeble socket.
</p>
<p class="post-footer align-right">
<span class="date">December 2, 2018</span>
</p>

<h3>Frobnitz 4.4 released</h3>
<p>
This release is effulgent and wavering gently.
<br>
Becomes bees.
</p>
<p class="post-footer align-right">
<span class="date">November 30, 2018</span>
</p>

请注意,这种结构是常规的,但不能用任何一个div或一个标记来选择。我可以使用选择器h3来获取标题,但每个新闻项的文本只是一个段落,我们也想分别获取日期。

因此,我想抓住所有标题,标题后面的段落和日期,并将它们全部放入某种数据结构中,这样我就可以将它们吐出自己的页面。

让我们从头衔开始吧,因为它会显示一个巧妙的技巧Mojo已经袖手旁观。

1
2
my $main = $tx->res->dom->at('#main');
my @headers = $main->find('h3')->map('text')->each;

你看到了吗?find这里的方法是返回一个Mojo::Collection。“集合”是一种说“列表”的奇特方式,但这些列表有一堆有用的实用方法,类似于在列表上运行的一些核心Perl函数,以及在其中找到的方法List::Util。它具有通常的嫌疑人,像joingrepmap,和each。因此,收藏品很花哨,它们应该得到一个奇特的名字。在上面,在返回的每个项目上map调用方法,并将结果作为列表返回。text`findeach`

在此之后,@headers将包含所有标题。我无法用简单的正则表达式来做到这一点(并且,我们可以将所有这些链接起来,包括找到#main一行,但我再次重新使用#main,所以我将它放入一个变量中)。

现在,与正则表达式相关的更棘手的事情就是找到这些标题的后续兄弟。但是,使用Mojo::DOM,我们可以通过几行代码来获取它(可能有一种方法可以用更少的代码来实现它,但这是我在几分钟的实验中提出的)。

1
2
3
4
my @paras;
for my $header ($main->find('h3')->each) {
push (@paras, $header->next->content);
}

这将再次选择h3元素,并迭代生成的DOM对象集合,将每个对象放入其中$header。然后,我们挑选出content的的next元素(这在我的情况下,始终是一个段落,有时包含一个或多个br标签),并把它们全部推入@paras

所以,现在我们有一个标题数组,以下段落的数组,我们只需要获取日期。这个实际上非常简单,因为HTML模板使用date类标记日期。

1
my @dates = $main->find('.date')->map('text')->each;

啪!我们完成了。好的,不太好。我们还没有完成这篇文章标题的“重复”部分。我们拥有来自我们糟糕的旧HTML网站的数据,现在让我们用它做点什么。

Munging the Dates

如上面的示例Hugo内容项所示,我想在元数据中包含日期。幸运的是,我们有与每个新闻项目相关的日期。不幸的是,它们不符合Hugo期望的格式。我对CPAN进行了一些挖掘,发现了Time::Piece,它是一个聪明的模块,可以解析和转换大多数常见格式的时间和日期。

我需要我的日期看起来像2017-09-30,所以我使用了下面的代码(假设这是一个循环,它将每个后续日期放在@dates我们上面的数组中$date):

1
2
3
use Time::Piece;
my $tp = Time::Piece->strptime($date, "%B %d, %Y");
my $fixed = $tp->strftime("%Y-%m-%d");

Munging进入Markdown

我还需要转换为Markdown。我已经使用HTML::WikiConverter完成任务。

在最简单的形式中,我们可以做这样的事情(再次假设我们在一个循环中$para@paras每次迭代获取一个值:

1
2
3
use HTML::WikiConverter;
my $wc = new HTML::WikiConverter(dialect => 'Markdown');
my $md = $wc->html2wiki( $para );

完成!

生成元数据

正如我们之前看到的,Hugo帖子在Markdown内容之前有元数据,并包含诸如作者信息,发布日期,描述等信息。有些是可选的,但有些是强制性的(我需要日期以便我可以显示最新的新网站首页上的新闻。我需要根据从原始HTML收集的信息自动生成所有这些内容。

我将着眼于如何@entries构建数据结构,但我会提到它是一个包含上面我们发现的三个数据的哈希数组。如果你想看到坚韧不拔的细节,我还会在最后用真实世界的代码链接到GitHub仓库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Mojo::File;
use String::Truncate qw(elide);

for my $e (@entries) {
my $desc = elide($e->{'text'}, 100, {at_space => 1});
my $md = <<"EOF";
---
title: "$e->{'title'}"
date: $e->{'date'}
description: "$desc"
categories: []
aliases: []
toc: false
draft: false
---
$e->{'text'}
EOF

my $filename = lc $e->{'title'};
$filename =~ s/\s/-/g;
$filename =~ s/[!,()'"\/]//g;
my $file = Mojo::File->new("content/news/$filename.md");
$file->spurt($md);
}

这里有很多内容,我只会简单地解释一下,因为它与Mojo无关。

循环的第一行创建描述,通常是摘要或其他。在我的情况下,主站点将显示描述为可点击链接,因此用户将在主页面上获得新闻项目的简短摘要,然后单击它以查看整个项目。我正在使用String::Truncate模块,该模块有一个elide方法将截断字边界上的字符串并添加省略号以指示文本被遗漏。

然后,在这里的文档中,我使用from填写所有元数据$e,每个元数据只是对散列的引用。然后我们使用Mojo::Filespurt方法将其写入文件。而已!当这是在一个包含预期格式的任意数量新闻项的页面循环中完成时,我们得到了一堆不错的新Hugo帖子。

为了清晰和简洁(并且因为它是基本的Perl而不是与Mojo相关的),我遗漏了循环并构建了我在生成元数据时使用的数据结构。如果你想在一个地方看到这一切,有一些丑陋的地方来解决破损的日期以及在Markdown中表现不佳的事情(比如表格),你可以看到代码(仍在进行中,但几乎准备好迁移我们胡思乱想的旧网站!)在GitHub上存储库中。我没有做得漂亮,因为它只需要运行一次,但它可以完成这项工作,而且构建起来并不需要太多时间,这要归功于Mojolicious以及CPAN的其他一些模块。