提取O'Reilly动物的列表

现在我想抓取O’Reilly封面动物的整个列表,而Mojolicious将帮助我完成这个任务。

我的大部分书籍都是由O’Reilly Media出版的,该公司以其封面上的动物而闻名。Edie Freedman在O’Reilly动物简史中解释了她如何想出这个知名的设计。我认为我第一次是在sed & awk的封面上看到这个设计;那些细尾猴有点可怕,但不足以让我远离命令行。

sed & awk

虽然Perl程序员应该闭嘴,因为Larry Wall选择了一只骆驼:它又丑又笨,但在恶劣条件下能完成任务。而且,对于我自己的书籍来说,羊驼有点可爱,但它们也是令人讨厌的生物。

O’Reilly 列出了几乎所有封面上的动物,即使“动物”这个词有点宽泛,包括了“天主教牧师”(大数据伦理)或“士兵或侦察兵,带有步枪”(SELinux)。您可以一次浏览20个结果,或搜索它。但是,正如我在网上看到的大多数列表一样,我想一次性抓取整个列表。给我展示分页资源,我会给你展示自动化取消分页的程序的。

对于Perl来说,抓取大量页面不是问题,特别是有Mojolicious(正如我在Mojo Web Clients中提到的)的帮助。我很快编写了一个脚本,不久就将所有的动物都放在了一个JSON文件中

我的编程没有特别之处,尽管我确实使用了Mojo::Promise,这样我就可以并发地发出请求。这并不是我特别关心的事情,但我刚回答了一个关于Promises的StackOverflow问题,所以它在我脑海中。我设置了所有的网络请求,但不会立即运行它们。一旦我有了它们,我会通过all() Promise一次性运行它们。

#!perl
use v5.26;
use experimental qw(signatures);

use Mojo::JSON qw(encode_json);
use Mojo::Promise;
use Mojo::UserAgent;
use Mojo::Util qw(dumper);

my @grand;
END {
	# Since the results come out of order,
	# sort by animal name then title
	@grand = sort {
		$a->{animal} cmp $b->{animal}
			or
		$a->{title} cmp $b->{title}
		} @grand;

	my $json = encode_json( \@grand );
	say $json;
	}

my $url = 'https://www.oreilly.com/animals.csp';
my( $start, $interval, $total );

my $ua = Mojo::UserAgent->new;

# We need to get the first request to get the total number of
# requests. Note that that number is actually larger than the
# number of results there will be, by about 80.
my $first_page_tx = $ua->get_p( $url )->then(
	sub ( $tx ) {
		push @grand, parse_page( $tx )->@*;
		( $start, $interval, $total ) = get_pagination( $tx );
		},
	sub ( $tx ) { die "Initial fetch failed!" }
	)->wait;

my @requests =
	map {
		my $page = $_;
		$ua->get_p( $url => form => { 'x-o' => $page } )->then(
			sub ( $tx ) { push @grand, parse_page( $tx )->@* },
			sub ( $tx ) { warn "Something is wrong" }
			);
		}
	map {
		$_ * $interval
		}
	1 .. ($total / $interval)
	;

Mojo::Promise->all( @requests )->wait;

sub get_pagination ( $tx ) {
	# 1141 to 1160 of 1244
	my $pagination = $tx
		->result
		->dom
		->at( 'span.cs-prevnext' )
		->text;

	my( $start, $interval, $total ) = $pagination =~ /
		(\d+) \h+ to \h+ (\d+) \h+ of \h+ (\d+) /x;
	}

sub parse_page ( $tx ) {
=pod

<div class="animal-row">
    <a class="book" href="..." title="">
      <img class="book-cvr" src="..." />
      <p class="book-title">Perl 6 and Parrot Essentials</p>
    </a>
    <p class="animal-name">Aoudad, aka Barbary sheep</p>
  </div>

=cut

	my $results = eval {
		$tx
			->result
			->dom
			->find( 'div.animal-row' )
			->map( sub {
				my %h;
				$h{link}      = $_->at( 'a.book' )->attr( 'href' );
				$h{cover_src} = $_->at( 'img.book-cvr' )->attr( 'src' );
				$h{title}     = $_->at( 'p.book-title' )->text;
				$h{animal}    = $_->at( 'p.animal-name' )->text;
				\%h;
				} )
			->to_array
		} or do {
			warn "Could not process a request!\n";
			[];
			};
	}

并发请求使这个程序比逐个请求运行要快得多,尽管如果不小心可能会真正打击服务器。大多数网络请求时间都是简单地等待,我将所有这些请求都放在同一时间等待。现在,这并不是真正的并行,因为一旦一个请求有了要处理的事情,比如读取数据,其他请求仍然需要等待它们的轮次。也许我以后会重新编写这个程序,使用Minion,这是一个基于Mojo的工作队列,可以在不同的进程中执行操作。

程序的其余部分是数据提取。在parse_page中,我使用了各种CSS选择器来提取所有的div.animal-row,并将每个动物转换为一个散列(再次,我在Mojo Web Clients中有大量的例子)。每个Promise将它的结果添加到@grand数组中。最后,我将它转换成一个JSON文件,这个文件我也已经作为一个gist上传了。

作为一名从事这种提取工作很长时间的人,我一直很满意 Mojolicious 让这个过程变得多么简单。我需要的所有东西都已经准备好了,使用的是相同的惯用语法,并且协同工作得很好。我获取页面并选择一些元素。很久以前,我会有长串的替换、正则表达式和其他低级文本处理。Perl 在文本处理方面确实很出色,但这并不意味着我想要在每个程序中都在这级工作。做几次强大的事情后,它似乎就不再那么酷了,尽管 Mojolicious 的下一步可能就是像《少数派报告》那样的预取,它在我之前就知道我想什么。

一个巧妙的技巧

我确实使用了一些有趣的技巧,因为这是我的习惯。最近,在这些程序中,我将东西收集到数据结构中,然后在最后展示。通常这意味着我在程序文件的顶部进行设置,在最后进行输出。然而,在我定义了 @grand 变量之后,我立即定义了一个 END 块来指定在所有其他事情发生后对 @grand 要做什么

my @grand;
END {
	# Since the results come out of order,
	# sort by animal name then title
	@grand = sort {
		$a->{animal} cmp $b->{animal}
			or
		$a->{title} cmp $b->{title}
		} @grand;

	my $json = encode_json( \@grand );
	say $json;
	}

这保持了数据结构的细节在一起。整个程序的目的就是将这些数据输出到 JSON 文件中。

我也可以用正常的 Perl 子例程将其保持在一起,但 END 是一个不需要显式调用的子例程。这只是我最近一直在做的事情,我可能以后会改变主意。

一次小冒险

我留给你一个小冒险,供你娱乐。我的动物有羊驼、阿尔帕卡、羊驼、骆驼和哈姆达尔蝶。在 O'Reilly 列表(或我的 JSON)中搜索这些标题。其中一些已经丢失,一些结果令人惊讶。

这里有几个有趣的 jq 命令来玩转 Animals JSON 文件

# get all the title
$ jq -r '.[].title' < animals.json | sort | head -10
.NET & XML
.NET Compact Framework Pocket Guide
.NET Framework Essentials
.NET Gotchas
.NET Windows Forms in a Nutshell
20 Recipes for Programming MVC 3
20 Recipes for Programming PhoneGap
21 Recipes for Mining Twitter
25 Recipes for Getting Started with R
50 Tips and Tricks for MongoDB Develope

# tab-separated list of animals and titles
$ jq -r '.[] | "\(.animal) => \(.title)"' < animals.json | sort
12-Wired Bird of Paradise	Mobile Design and Development
3-Banded Armadillo	Windows PowerShell for Developers
Aardvark	Jakarta Commons Cookbook
Aardwolf	Clojure Cookbook
Addax, aka Screwhorn Antelope	Ubuntu: Up and Running
Adjutant (Storks)	Social eCommerce
Aegina Citrea, narcomedusae, jellyfish	BioBuilder
African Civet	JRuby Cookbook
African Crowned Crane aka Grey Crowned Crane	C# 5.0 Pocket Reference
African Crowned Crane aka Grey Crowned Crane	Programming C# 3.0

# find a title by exact match of animal
$ jq -r '.[] | select(.animal=="Llama") | .title' < animals.json
Randal Schwartz on Learning Perl

# find a title with a regex match against the animal
$ jq -r '.[] | select(.animal|test("ama")) | .title' < animals.json | sort
Access Cookbook
Access Database Design & Programming
ActionScript for Flash MX Pocket Reference
ActionScript for Flash MX: The Definitive Guide
Ajax on Java
Appcelerator Titanium: Up and Running
Embedding Perl in HTML with Mason
Fluent Python
Identity, Authentication, and Access Management in OpenStack
Introduction to Machine Learning with Python
Learning Perl 6
PDF Explained
Randal Schwartz on Learning Perl
SQL Pocket Guide
SQL Tuning
Solaris 8 Administrator's Guide
The Little Book on CoffeeScript
Writing Game Center Apps in iOS

# find an animal with a regex match against the title
$ jq -r '.[] | select(.title|test("Perl")) | .animal' < animals.json | sort
Alpaca
Aoudad, aka Barbary sheep
Arabian Camel, aka Dromedary
Arabian Camel, aka Dromedary
Arabian Camel, aka Dromedary, Head
Badger
Bighorn Sheep
Black Leopard
Blesbok (African antelope)
Camel, aka Dromedary
Cheetah
Emu, large and fluffy
Emu, young
Fan-footed Gecko, aka Wall Gecko
Flying Dragon (lizard)
Flying Dragon (lizard)
Greater Honeyguide
Green Monkey 1 (adult holding a baby)
Hamadryas Baboon
Hamadryas Butterfly
Llama
Mouse
North American Bullfrog
Proboscis Monkey
Red Colobus Monkey
Sea Otter
Staghound
Tadpole of a Greenfrog (sketch)
Thread-winged Lacewing, aka Antlion
White-tailed Eagle
Wolf

标签

brian d foy

brian d foy 是 Perl 训练师和作家,Perl.com 的资深编辑。他是《精通 Perl》、《Mojolicious Web 客户端》、《学习 Perl 练习》的作者,以及《编程 Perl》、《学习 Perl》、《中级 Perl》和《高效 Perl 编程》的合著者。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在 GitHub 上打开一个问题或拉取请求来帮助我们。