䟋の関数型Perlプログラミング

この蚘事では、 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でクロヌゞャヌを䜿甚し、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クラスを宣蚀したす。



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


read_write_file.pl
 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 ++


Reader_base.hpp
 #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; }; 

Writer_base.hpp
 #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; }; 

ReaderWriter.hpp
 #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); } }; 

main.cpp
 #include "ReaderWriter.hpp" int main() { ReaderWriter rw("charset.out", "writer.out"); rw.main_loop(); return 0; } 

次のようにコンパむルしたす。


 $ g++ -std=c++11 -o main main.cpp 

コヌドを分析したしょう



次に、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呌び出しを䜿甚しお、サヌバヌの負荷を次のように削枛したした。



そのため、たず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:



おわりに


Perl, , — , . Perl C++, (callbacks) Perl - C++. AnyEvent::HTTP, .



Source: https://habr.com/ru/post/J326028/


All Articles