この記事では、 AnyEvent :: HTTPを使用して壊れたリンクを検索するスクリプトの例を使用して、関数型プログラミングを検討します 。 次のトピックについて説明します。
- 匿名ルーチン;
- 閉鎖
- コールバック
匿名ルーチン
無名サブルーチンは通常のサブルーチンと同じように宣言されますが、 sub
キーワードとプログラムブロックの開始ブラケットの間に名前はありません。 さらに、この形式の記述は式の一部とみなされるため、ほとんどの場合、匿名サブルーチンの宣言はセミコロンまたは他の式区切り文字で終了する必要があります。
sub { ... ... };
たとえば、渡される値を3倍にするサブルーチンを実装します。
my $triple = sub { my $val = shift; return 3 * $val; }; say $triple->(2); # 6
無名ルーチンの主な利点は、「データとしてのコード」の使用です。 つまり、コードを変数に保存し(たとえば、コールバックの場合は関数に渡します)、さらに実行します。
また、コールバックとの組み合わせを含め、匿名ルーチンを使用して再帰を作成できます。 たとえば、Perlバージョン5.16.0
で登場し、現在のサブルーチンへのリンクを取得できる__SUB__
トークンを使用して、階乗計算を実装します。
use 5.16.0; my $factorial = sub { my $x = shift; return 1 if $x == 1; return $x * __SUB__->($x - 1); }; say $factorial->(5); # 120
壊れたリンクを見つける問題を検討するとき、コールバックと組み合わせて再帰を使用する例を以下に示します。
閉鎖
ウィキペディアに記載されているとおり
クロージャーは最初のクラスの関数であり、その本体には、周囲のコードでこの関数の本体の外側で宣言され、そのパラメーターではない変数への参照があります。
実際、クロージャーはOOPのクラスの類似物です。機能とデータを接続し、一緒にパッケージ化します。 PerlのクロージャとC ++のクラスの例を考えてみましょう。
Perl
sub multiplicator { my $multiplier = shift; return sub { return shift() * $multiplier; }; }
C ++
class multiplicator { public: multiplicator(const int &mul): multiplier(mul) { } long int operator()(const int &n) { return n * multiplier; } private: int multiplier; };
以下のコードを分析してみましょう。
プライベート変数宣言:
- Perl:
最初に、レキシカル変数(
my
)を定義します$multiplier
(my $multiplier = shift;
);
- C ++:
private
アクセストークンの後にint
型のmultiplier
変数を宣言します。
プライベート変数の初期化:
- Perl:
変数を作成するときに、渡された値を初期化します。
- C ++:
コンストラクターをオーバーロードして、数値を
multiplier
し、初期化リストで変数のmultiplier
を初期化します。
渡された値と以前に初期化された変数を乗算するサブルーチンを作成します。
- Perl:
入力としてパラメーターを受け取り、それを以前に初期化された変数
$multiplier
し、結果の値を返す匿名サブルーチンを返します。
- C ++:
関数呼び出し演算子
()
をオーバーロードします。この演算子は、パラメーターn
を入力として受け取り、変数のmultiplier
して値を返します。
Perlでクロージャーを使用し、C ++でクラスを使用するには、それらを定義する必要があります。 オブジェクトを作成します:
Perl:
- オブジェクト定義:
my $doubled = multiplicator(2);
my $tripled = multiplicator(3);
- 使用法:
say $doubled->(3); # 6
say $tripled->(4); # 12
C ++:
- オブジェクト定義:
multiplicator doubled(2), tripled(3);
- 使用法:
cout << doubled(3) << endl; // 6
cout << tripled(4) << endl; // 12
C ++では、定義演算子()
定義されているクラスのオブジェクトは、しばしば機能オブジェクトまたはファンクターと呼ばれます。 機能オブジェクトは、一般的なアルゴリズムの引数として最もよく使用されます。 たとえば、ベクトルの要素を追加するには、for_eachアルゴリズムを使用できます。これは、シーケンスの各要素に渡される関数を適用し、オーバーロードされた演算子()
でSumクラスを適用します。これは、シーケンスのすべての要素を追加し、合計を返します。 また、Sumクラスの代わりに、C ++ 11で登場したラムダを使用できます。
C ++:
#include <iostream> #include <vector> #include <algorithm> using std::cout; using std::endl; using std::vector; class Sum { public: Sum() : sum(0) { }; void operator() (int n) { sum += n; } inline int get_sum() { return sum; } private: int sum; }; int main() { vector<int> nums{3, 4, 2, 9, 15, 267}; Sum s = for_each(nums.begin(), nums.end(), Sum()); cout << " Sum: " << s.get_sum() << endl; long int sum_of_elems = 0; for_each(nums.begin(), nums.end(), [&](int n) { sum_of_elems += n; }); cout << " : " << sum_of_elems << endl; return 0; }
Perl:
sub for_each { my($arr, $cb) = @_; for my $item (@$arr) { $cb->($item); } } my $sum = 0; for_each [3, 4, 2, 9, 15, 267], sub { $sum += $_[0]; }; say $sum;
例からわかるように、C ++では、以下を含むSum
クラスを宣言します。
- 標準コンストラクタで初期化されるプライベート変数
sum
。 - オーバーロードされた演算子
()
。非行の各値を受け取り、sum
ます。 - プライベート変数
sum
にアクセスするget_sum
メソッド。
Perlの例では、配列への参照と匿名関数を受け入れるfor_each
関数を作成します。 次に、配列を調べて匿名関数(クロージャー)を実行し、配列の次の要素をパラメーターとして渡します。
for_each
関数を使用する場合、最初にゼロに初期化される接辞変数$sum
定義します。 次に、配列参照とクロージャ関数をfor_each
関数for_each
渡します。この関数では、配列の各要素を$sum
変数に$sum
ます。 for_each
関数をfor_each
後、 $sum
変数には配列の合計が含まれます。
C ++でのPerlの例のクロージャー関数の類似物は、コードに示されているように、ラムダの使用です。 Perlの例では、関数に渡されるクロージャー関数はコールバックまたはコールバック関数とも呼ばれます。
コールバック関数
for_each
例がfor_each
に、コールバック関数は、実行可能コードを他のコードのパラメーターの1つとして渡すことです。 多くの場合、渡された関数はクロージャーのように機能します。 字句変数にアクセスでき、プログラムコードの他のコンテキストで定義でき、親関数(クロージャー/コールバックが渡された関数)からの直接呼び出しにアクセスできません。
実際、コールバック関数は、関数の多態性に類似しています。つまり、構造は同じですが、実行可能なサブタスクによって特定の場所でのみ異なる一連の関数を作成する代わりに、より汎用的な関数を作成できます。 ファイルから読み取り、ファイルに書き込むタスクの例を考えてみましょう。 これを行うには、Perlを使用して、リーダーとライターの2つの関数を作成します(例は、 異種データを解析するための Mikhail Ozerov Lazyイテレーターによるプレゼンテーションから取得しました )。C++を使用して、Reader_base、Writer_base、ReaderWriterクラスを作成します。
Perl
use strict; use warnings; sub reader { my ($fn, $cb) = @_; open my $in, '<', $fn; while (my $ln = <$in>) { chomp $ln; $cb->($ln); # } close $in; } sub write_file { my ($fn, $cb) = @_; open my $out, '>', $fn; $cb->(sub { # my $ln = shift; syswrite($out, $ln.$/); }); close $out; } write_file('./out.cvs', sub { my $writer = shift; # sub { my $ln = shift; syswrite() } reader('./in.csv', sub { my $ln = shift; my @fields = split /;/, $ln; return unless substr($fields[1], 0, 1) == 6; @fields = @fields[0,1,2]; $writer->(join(';', @fields)); # }); });
C ++
#pragma once #include <iostream> #include <string> #include <fstream> // - using std::ifstream; using std::getline; using std::cout; using std::runtime_error; using std::endl; using std::cerr; using std::string; class Reader_base { public: Reader_base(const string &fn_in) : file_name(fn_in) { open(file_name); } virtual ~Reader_base() { infile.close(); } virtual void open(const string &fn_in) { infile.open(fn_in); // , if (! infile.is_open()) throw runtime_error("can't open input file \"" + file_name + "\""); } virtual void main_loop() { try { while(getline(infile, line)) { rcallback(line); } } catch(const runtime_error &e) { cerr << e.what() << " Try again." << endl; } } protected: virtual void rcallback(const string &ln) { throw runtime_error("Method 'callback' must me overloaded!"); }; private: ifstream infile; string line; string file_name; };
#pragma once #include <iostream> #include <string> #include <fstream> // - using std::string; using std::ofstream; using std::cout; using std::runtime_error; using std::endl; using std::cerr; class Writer_base { public: Writer_base(const string &fn_out) : file_name(fn_out) { open(file_name); } virtual ~Writer_base() { outfile.close(); } virtual void open(const string &fn_out) { outfile.open(file_name); if (! outfile.is_open()) throw runtime_error("can't open output file \"" + file_name + "\""); } virtual void write(const string &ln) { outfile << ln << endl; } private: string file_name; ofstream outfile; };
#pragma once #include "Reader.hpp" #include "Writer.hpp" class ReaderWriter : public Reader_base, public Writer_base { public: ReaderWriter(const string &fn_in, const string &fn_out) : Reader_base(fn_in), Writer_base(fn_out) {} virtual ~ReaderWriter() {} protected: virtual void rcallback(const string &ln) { write(ln); } };
#include "ReaderWriter.hpp" int main() { ReaderWriter rw("charset.out", "writer.out"); rw.main_loop(); return 0; }
次のようにコンパイルします。
$ g++ -std=c++11 -o main main.cpp
コードを分析しましょう:
ファイルからの読み取り:
- Perl:
reader
関数では、reader
用のファイル名とコールバックを渡します。 まず、ファイルを読み取り用に開きます。 次に、ループでファイルを1行ずつ繰り返し、各繰り返しでコールバックを呼び出して次の行を渡します。 ループが完了したら、ファイルを閉じます。 OOPの観点から言えば、コンストラクターはファイルの初期化とオープンを担当し、main_loop
メソッドはメインループを担当します。メインループでは、コールバックの呼び出しでファイルがmain_loop
れます。 ファイルはデストラクタで閉じられます。 コールバックは基本的に、子孫でオーバーロードされ、親から呼び出される仮想メソッドです。 この類似性は、C ++の例で見ることができます。
- C ++:
Reader_base
クラスのコンストラクタでfile_name
変数を初期化し、読み取り用にファイルを開きます。 次に、仮想メンバー関数main_loop
を作成します。この関数では、ファイルを1行main_loop
ループして、その行を子孫にロードする必要があるメンバー関数rcallback
ます。
ファイルへの書き込み:
- Perl:
writer
関数では、書き込み用のファイル名とコールバックを渡します。 また、reader
関数の例のように、最初に書き込み用にファイルを開きます。 次に、別のコールバック(クロージャ)を渡すコールバックを呼び出します。コールバックでは、行を取得して、ファイルに書き込みます。 コールバックを終了した後、ファイルを閉じます。 OOPの観点から言えば、コンストラクターはファイルの初期化とオープンを担当します。 writeメソッドは、文字列を入力として受け取り、それをファイルに書き込むファイルへの書き込みを担当します。 次に、ファイルはデストラクタで閉じられます。 この類似性は、C ++の例で見ることができます。
- C ++:
Writer_base
クラスのコンストラクターでfile_name
変数を初期化し、書き込み用にファイルを開きます。 次に、ファイルに書き込む文字列が渡される仮想writer
メンバー関数を作成します。 次に、ファイルはデストラクタで閉じられます。
Perlで作成された関数とC ++でクラスを操作します。
- Perl:
最初に、書き込み用のファイル名とコールバックを渡す
writer
関数を呼び出します。 コールバックでは、$writer
変数で別のコールバックを取得します。この変数は、渡された文字列をファイルに書き込みます。 次に、reader
関数を呼び出します。reader
関数には、読み込むファイルの名前とコールバックを渡します。 リーダー関数のコールバックでは、ファイルから次の行を取得して操作し、$writer
コールバックを使用してファイルに書き込みます。 例からわかるように、リーダー関数のコールバックは本質的にクロージャーです。 字句変数$writer
への参照が含まれます。
- C ++:
複数の継承を使用し、
ReaderWriter
クラスとReaderWriter
クラスを継承するReaderWriter
クラスを作成しWriter_base
。 コンストラクターで、Writer_base
クラスとWriter_base
クラスReader_base
、それぞれ読み取りおよび書き込み用Reader_base
ファイル名で初期化Reader_base
ます。 次に、オーバーロードされたrcallback
メソッドを作成します。rcallback
メソッドは、次の行を受け取り、Writer_base
クラスのwrite
メソッドを使用してファイルにwrite
ます。 オーバーロードされたrcallback
メソッドはrcallback
main_loop
クラスのmain_loop
メソッドからrcallback
。 main.cppファイルの例からわかるように、クラスを操作するために、ReaderWriter
クラスのrw
オブジェクトがReaderWriter
。このコンストラクターは、読み取りと書き込みのためにファイル名を渡します。 次に、rw
オブジェクトのメンバー関数main_loop
を呼び出します。
次に、AnyEvent :: HTTPを使用して壊れたリンクを見つけるという複雑で実用的なタスクを検討します。これは、上記のトピック(匿名ルーチン、クロージャー、コールバック関数)を使用します。
壊れたリンクを見つけるタスク
壊れたリンク(応答コード4xxおよび5xxのリンク)を検索する問題を解決するには、サイトクロールを実装する方法を理解する必要があります。 実際、サイトはリンクグラフです。 URLは、外部ページと内部ページの両方にリンクできます。 サイトをクロールするには、次のアルゴリズムを使用します。
process_page(current_page): for each link on the current_page: if target_page is not already in your graph: create a Page object to represent target_page add it to to_be_scanned set add a link from current_page to target_page scan_website(start_page) create Page object for start_page to_be_scanned = set(start_page) while to_be_scanned is not empty: current_page = to_be_scanned.pop() process_page(current_page)
このタスクの実装は、 Broken link checkerリポジトリにありますchecker_with_graph.plスクリプトを検討してください。 まず、変数$start_page_url
(開始ページのURL)、 $cnt
(ダウンロードするURLの数)を初期化し、ハッシュ$to_be_scanned
とグラフ$g
作成します。
次に、 scan_website,
関数を作成します。 scan_website,
関数にscan_website,
ダウンロードおよびコールバック用のURLの最大数の制限を渡します。
sub scan_website { my ($count_url_limit, $cb) = @_;
最初に、開始ページ$to_be_scanned
ハッシュを初期化します。
# to_be_scanned = set(start_page) $to_be_scanned->{$start_page_url}{internal_urls} = [$start_page_url];
$to_be_scanned
構造の完全な分析はさらに進んでおり、リンクが内部(internal_urls)であることに注意する価値があります。
次に、匿名関数を作成して実行します。 レコードを見る
my $do; $do = sub { ... }; $do->();
は標準的なイディオムであり、クロージャから$do
変数にアクセスして、たとえば再帰を作成できます。
my $do; $do = sub { ...; $do->(); ... }; $do->();
または循環参照を削除する:
my $do; $do = sub { ...; undef $do; ... }; $do->();
$do
クロージャーで、 %urls
ハッシュを作成し、そこに$to_be_scanned
ハッシュからURLを追加します。
my %urls; for my $parent_url (keys %$to_be_scanned) { my $type_urls = $to_be_scanned->{$parent_url}; # $type_urls - internal_urls|external_urls push @{$urls{$parent_url}}, splice(@{$type_urls->{internal_urls}}, 0, $max_connects); while (my ($root_domain, $external_urls) = each %{$type_urls->{external_urls}}) { push @{$urls{$parent_url}}, splice(@$external_urls, 0, 1); } }
%urls
ハッシュ構造は次のとおりです。
{parent_url1 => [target_url1, target_url2, target_url3], parent_url2 => [...]}
次に、関数process_page
を実行し、 %urls
hash %urls
へのリンクとコールバックを渡します。
process_page(\%urls, sub { ... });
process_page
関数で、受信したハッシュとコールバックを保存します。
sub process_page { my ($current_page_urls, $cb) = @_;
その後、URLハッシュをループしてペア(parent_url => current_urls)
を取得し、現在のURLのリスト(current_urls)を(parent_url => current_urls)
ます
while (my ($parent_url, $current_urls) = each %$current_page_urls) { for my $current_url (@$current_urls) {
ページからのデータの受信を検討する前に、少し余談します。 ページを解析してURLを取得するための基本的なアルゴリズムは、このURLが内部か外部かに関係なく、1つのHTTP GETメソッドを想定しています。 この実装では、2つのHEADおよびGET呼び出しを使用して、サーバーの負荷を次のように削減しました。
- HEADリクエストは、すべての外部URLに対して実行されます(エラーがあるかどうかに関係なく)。 内部にエラーがあり、Webページではない場合。
- HEADおよびGET要求は、内部Webページに対してエラーなしで実行されます。
そのため、まずAnyEvent :: HTTPモジュールのhttp_head
関数を実行し、現在のURL、要求パラメーター、コールバックを渡します。
$cv->begin; http_head $current_url, %params, sub {
コールバックでは、ヘッダー(HTTPヘッダー)を取得します
my $headers = $_[1];
ここから実際のURL(リダイレクト後のURL)を取得します
my $real_current_url = $headers->{URL};
次に、ペア(current_url => real_current_url)
を%urls_with_redirects
ハッシュに%urls_with_redirects
ます。
$urls_with_redirects{$current_url} = $real_current_url if $current_url ne $real_current_url;
さらに、エラーが発生した場合(ステータスコード4xxおよび5xx)、ログにエラーを表示し、将来の使用のためにヘッダーをハッシュに保存します
if ( $headers->{Status} =~ /^[45]/ && !($headers->{Status} == 405 && $headers->{allow} =~ /\bget\b/i) ) { $warn_log->("$headers->{Status} | $parent_url -> $real_current_url") if $warn; $note_log->(sub { p($headers) }) if $note; $urls_with_errors{$current_url} = $headers; # }
それ以外の場合、サイトが内部でWebページである場合、
elsif ( # ($start_page_url_root eq $url_normalization->root_domain($real_current_url)) # - && ($headers->{'content-type'} =~ m{^text/html}) ) {
次に、 http_get
関数を実行します。 http_get
関数に、上記で受け取った実際の現在のURL、リクエストパラメータ、コールバックを転送します。
$cv->begin; http_get $real_current_url, %params, sub {
http_get関数のコールバックで、ページのヘッダーと本文http_get
取得し、ページをデコードします。
my ($content, $headers) = @_; $content = content_decode($content, $headers->{'content-type'});
Web :: Queryモジュールを使用して、ページ解析とURL取得を実行します。
wq($content)->find('a') ->filter(sub { my $href = $_[1]->attr('href'); # , $href !~ /^#/ && $href ne '/' && $href !~ m{^mailto:(?://)?[A-Z0-9+_.-]+@[A-Z0-9.-]+}i && ++$hrefs{$href} == 1 # if $href }) ->each(sub { # for each link on the current page
each
メソッドの各反復で、コールバックにリンクを取得します
my $href = $_->attr('href');
そしてそれを変換する
$href = $url_normalization->canonical($href); # '/', '/contact' (//dev.twitter.com/etc) if ($href =~ m{^/[^/].*}) { $href = $url_normalization->path($real_current_url, $href) ; } $href = $url_normalization->without_fragment($href);
次にチェックします-グラフにそのようなリンクがない場合
unless($g->has_vertex($href)) { # if tarteg_page is not already in your graph
次に、リンクのルートドメインを取得します(または「失敗」に入れます)
my $root_domain = $url_normalization->root_domain($href) || 'fails';
その後、 $new_urls
の構造を$new_urls
。これは、 $to_be_scanned
の構造に似ており、次の形式になります。
$new_urls = $to_be_scanned = { parent_url => { external_urls => { root_domain1 => [qw/url1 url2 url3/], root_domain2 => [qw/url1 url2 url3/], }, internal_urls => [qw/url url url/], }, };
$new_urls
構造体では、ペア(parent_url => target_url)
を作成しますが、 target_url
をいくつかの部分に分割します。つまり、配列に保存する内部URLとドメインに分割し、配列に保存する外部URLに分割します。 この構造により、次のようにサイトの負荷を減らすことができます。 %urls
ハッシュを構築する際の上記の$do
クロージャーに示すように、内部URLの$max_connects ( )
各ドメインごとに1つの外部URLを選択します。 したがって、 scan_website
関数の開始時に、開始ページを次のように保存しました。
$to_be_scanned = { $start_page_url => { internal_urls => [$start_page_url], }, };
つまり この場合、親ページと現在のページの両方が開始ページでした(他の場合、ページデータは異なります)。
この構造の構築は次のとおりです-サイトが内部の場合、構造を作成します
$new_urls->{$real_current_url}{internal_urls} //= []
それ以外の場合、サイトが内部の場合、構造
$new_urls->{$real_current_url}{external_urls}{$root_domain} //= []
そして、これらの構造の1つを$urls
変数に保存します。これを次に使用して、 $new_urls
構造に書き込みます。
push @$urls, $href; # add it to to_be_scanned set
この場合、リンクを使用して複雑なデータ構造を作成および操作します。 変数$urls
は$new_urls
の構造を参照するため、変数$urls
が変更されると、$new_urls
構造が変更され$new_urls
。 Perlでのデータ構造とアルゴリズムの詳細については、「Jon Orwant-Perlでアルゴリズムをマスターする」を参照してください。
次に、グラフにカップルを追加します(real_current_url (parent) => href (current))
。
$g->add_edge($real_current_url, $href);
その後、 $new_urls
の構造を確認します-配列internal_urls
またはexternal_urls
空でない場合は、データをログに出力してコールバックを実行し、構造$new_urls
渡します
if (is_to_be_scanned($new_urls)) { $debug_log->(($parent_url // '')." -> $real_current_url ".p($new_urls)) if $debug; $cb->($new_urls); }
オプション(エラーまたは内部ページの解析)のいずれにも該当しなかった場合、つまり サイトは外部でエラーが発生していないため、コールバックを実行します
else { $cb->(); }
この呼び出しは、すべての外部サイトが現在のURL $current_urls
リストにある場合に必要ですが、 $to_be_scanned
まだ$to_be_scanned
が$to_be_scanned
。 この呼び出しがなければ、 http_head
とhttp_head
実行して$current_urls
のリストをhttp_head
。
process_page
関数のコールバックで、結果の構造$new_urls
を保存し$new_urls
。
process_page(\%urls, sub { my $new_urls = shift;
それを変数$to_be_scanned
と組み合わせます。
$to_be_scanned = merge($to_be_scanned, $new_urls) if $new_urls;
次に、グラフ要素の数がURLの数の制限以上かどうかを確認し、匿名サブルーチンへのリンクを削除して$cv->send()
ます。
if (scalar($g->vertices) >= $count_url_limit) { undef $do; $cb->(); $cv->send; }
それ以外の場合、チェックするURLがあれば、
elsif (is_to_be_scanned($to_be_scanned)) {
その後、匿名サブルーチンを再帰的に呼び出します
$do->();
上記の課題が考慮されました。 $to_be_scanned
process_page
( ).
, GraphViz — svg, png .. :
$ perl bin/checker_with_graph.pl -u planetperl.ru -m 500 -c 5 \ -g -f svg -o etc/panetperl_ru.svg -l "broken link check" -r "http_//planetperl.ru/" $ perl bin/checker_with_graph.pl -u habrahabr.ru -m 500 -c 5 \ -g -f svg -o etc/habr_ru.svg -l "broken link check" -r "https_//habrahabr.ru/" $ perl bin/checker_with_graph.pl -u habrahabr.ru -m 100 -c 5 \ -g -f png -o etc/habr_ru.png -l "broken link check" -r "https_//habrahabr.ru/"
どこで
--url | -u --max_urls | -m --max_connects | -c --graphviz | -g --graphviz_log_level | -e , . perldoc Log::Handler --format | -f - png, svg, etc --output_file | -o --label | -l --root | -r - .. twopi
PERL_ANYEVENT_VERBOSE,
$ export PERL_ANYEVENT_VERBOSE=n
n:
- 5 (warn) — http
- 6 (note) — http ( $headers)
- 7 (info) — URLs
- 8 (debug) — ,
おわりに
Perl, , — , . Perl C++, (callbacks) Perl - C++. AnyEvent::HTTP, .