jsoup - 基本的なWebクローラーの例

jsoup –基本的なWebクローラーの例

web-crawler-spider-logo

Web Crawlerは、Webをナビゲートし、索引付けのために新しいページまたは更新されたページを見つけるプログラムです。 クローラーは、シードWebサイトまたはさまざまな人気のあるURL(frontierとも呼ばれます)から開始し、抽出するハイパーリンクを深さと幅で検索します。

Webクローラーは、親切で堅牢でなければなりません。 クローラーの親切さは、robots.txtによって設定されたルールを尊重し、Webサイトへの頻繁なアクセスを回避することを意味します。 堅牢性とは、スパイダートラップやその他の悪意のある動作を回避する能力のことです。 Webクローラーのその他の優れた属性は、複数の分散マシン間の分散性、拡張性、継続性、ページ品質に基づいて優先順位を付ける機能です。

1. Webクローラーを作成する手順

Webクローラーを作成する基本的な手順は次のとおりです。

  1. フロンティアからURLを選ぶ

  2. HTMLコードを取得する

  3. HTMLを解析して、他のURLへのリンクを抽出します

  4. 既にURLをクロールしているか、以前に同じコンテンツを見たことがあるかどうかを確認してください

    • インデックスに追加しない場合

抽出された各URLについて

  • チェックに同意することを確認します(robots.txt、クロール頻度)

正直なところ、インターネット上のすべてのページで1つのWebクローラーを開発および保守することは、不可能ではないにしても、現在オンラインに1 billion websitesを超えていることを考えると困難です。 この記事を読んでいる場合、WebクローラーではなくWebスクレーパーを作成するためのガイドを探している可能性があります。 では、なぜ記事は「Basic Web Crawler」と呼ばれるのですか? えっと…キャッチーだから…本当に! クローラーとスクレーパーの違いを知っている人はほとんどいないので、オフラインのデータスクレイピングを含め、すべてに「クロール」という言葉を使用する傾向があります。 また、Web Scraperを構築するにはクロールエージェントも必要です。 そして最後に、この記事は実行可能な例を提供するだけでなく、情報を提供することを意図しているためです。

2. クローラーのスケルトン

HTMLの解析には、jsoupを使用します。 以下の例は、jsoupバージョン1.10.2を使用して開発されました。

pom.xml

        
            org.jsoup
            jsoup
            1.10.2
        

Webクローラーの基本的なコードから始めましょう。

BasicWebCrawler.java

package com.example.basicwebcrawler;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.util.HashSet;

public class BasicWebCrawler {

    private HashSet links;

    public BasicWebCrawler() {
        links = new HashSet();
    }

    public void getPageLinks(String URL) {
        //4. Check if you have already crawled the URLs
        //(we are intentionally not checking for duplicate content in this example)
        if (!links.contains(URL)) {
            try {
                //4. (i) If not add it to the index
                if (links.add(URL)) {
                    System.out.println(URL);
                }

                //2. Fetch the HTML code
                Document document = Jsoup.connect(URL).get();
                //3. Parse the HTML to extract links to other URLs
                Elements linksOnPage = document.select("a[href]");

                //5. For each extracted URL... go back to Step 4.
                for (Element page : linksOnPage) {
                    getPageLinks(page.attr("abs:href"));
                }
            } catch (IOException e) {
                System.err.println("For '" + URL + "': " + e.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        //1. Pick a URL from the frontier
        new BasicWebCrawler().getPageLinks("http://www.example.com/");
    }

}

Note
このコードを長時間実行させないでください。 終了せずに数時間かかる場合があります。

出力例:

前述したように、Webクローラーはリンクの幅と深さを検索します。 ツリーのような構造のWebサイト上のリンクを想像すると、ルートノードまたはレベル0が最初のリンクになり、次のレベルはレベル0などで見つかったすべてのリンクになります。

3. クロールの深さを考慮に入れる

前の例を修正して、リンク抽出の深さを設定します。 この例と前の例の唯一の真の違いは、再帰的なgetPageLinks()メソッドには、if...elseステートメントの条件としても追加されるリンクの深さを表す整数引数があることです。

WebCrawlerWithDepth.java

package com.example.depthwebcrawler;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.IOException;
import java.util.HashSet;

public class WebCrawlerWithDepth {
    private static final int MAX_DEPTH = 2;
    private HashSet links;

    public WebCrawlerWithDepth() {
        links = new HashSet<>();
    }

    public void getPageLinks(String URL, int depth) {
        if ((!links.contains(URL) && (depth < MAX_DEPTH))) {
            System.out.println(">> Depth: " + depth + " [" + URL + "]");
            try {
                links.add(URL);

                Document document = Jsoup.connect(URL).get();
                Elements linksOnPage = document.select("a[href]");

                depth++;
                for (Element page : linksOnPage) {
                    getPageLinks(page.attr("abs:href"), depth);
                }
            } catch (IOException e) {
                System.err.println("For '" + URL + "': " + e.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        new WebCrawlerWithDepth().getPageLinks("http://www.example.com/", 0);
    }
}

Note
上記のコードを自由に実行してください。 深さを2に設定したラップトップでは数分しかかかりませんでした。 深さが深いほど、終了までに時間がかかることに注意してください。

出力例:

...
>> Depth: 1 [https://docs.gradle.org/current/userguide/userguide.html]
>> Depth: 1 [http://hibernate.org/orm/]
>> Depth: 1 [https://jax-ws.java.net/]
>> Depth: 1 [http://tomcat.apache.org/tomcat-8.0-doc/index.html]
>> Depth: 1 [http://www.javacodegeeks.com/]
For 'http://www.javacodegeeks.com/': HTTP error fetching URL
>> Depth: 1 [http://beust.com/weblog/]
>> Depth: 1 [https://dzone.com]
>> Depth: 1 [https://wordpress.org/]
>> Depth: 1 [http://www.liquidweb.com/?RID=example]
>> Depth: 1 [http://www.example.com/privacy-policy/]

4. データスクレイピングvs. データのクロール

これまでのところ、この問題に関する理論的なアプローチに適しています。 事実、一般的なクローラーを作成することはほとんどないため、「実際の」クローラーが必要な場合は、既存のクローラーを使用する必要があります。 平均的な開発者が行うことのほとんどは、特定のWebサイトから特定の情報を抽出することです。これには、Webクローラーの構築が含まれますが、実際にはWebスクレイピングと呼ばれます。

Data Scraping vs. Data Crawlingに関するPromptCloudに関するArpan Jhaの非常に優れた記事があり、個人的にこの区別を理解するのに大いに役立ちました。それを読むことをお勧めします。

この記事から取った表でそれを要約するには:

データスクレイピング データのクロール

Webを含むさまざまなソースからデータを抽出する必要があります

Webからページをダウンロードすることを指します

あらゆる規模で行うことができます

主に大規模に行われます

重複排除は必ずしも一部ではありません

重複排除は重要な部分です

クロールエージェントとパーサーが必要

クロールエージェントのみが必要

イントロで約束されているように、理論から実行可能な例に移行する時間。 example.comからJava8に関連する記事のすべてのURLを取得するシナリオを想像してみましょう。 私たちの目標は、可能な限り短時間でその情報を取得し、ウェブサイト全体をクロールしないようにすることです。 また、このアプローチはサーバーのリソースを浪費するだけでなく、時間も浪費します。

5. ケーススタディ–example.comで「Java8」のすべての記事を抽出します

5.1 First thing we should do is look at the code of the website. example.comをざっと見てみると、フロントページのページングと、各ページの/page/xxパターンに従っていることが簡単にわかります。

jsoup-web-crawler-example-1

これにより、/page/を含むすべてのリンクを取得することで、探している情報に簡単にアクセスできることがわかります。 したがって、Webサイト全体を実行する代わりに、document.select("a[href^="http://www.example.com/page/"]")を使用して検索を制限します。 このcss selectorを使用して、http://example.com/page/で始まるリンクのみを収集します。

5.2 Next thing we notice is that the titles of the articles -which is what we want- are wrapped in <h2></h2> and <a href=""></a> tags.

jsoup-web-crawler-example-2

したがって、記事のタイトルを抽出するために、selectメソッドをその正確な情報に制限するcss selectorを使用してその特定の情報にアクセスします:document.select("h2 a[href^="http://www.example.com/"]");

5.3 Finally, we will only keep the links in which the title contains ‘Java 8’ and save them to a file.

Extractor.java

package com.example.extractor;

package com.example;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

public class Extractor {
    private HashSet links;
    private List> articles;

    public Extractor() {
        links = new HashSet<>();
        articles = new ArrayList<>();
    }

    //Find all URLs that start with "http://www.example.com/page/" and add them to the HashSet
    public void getPageLinks(String URL) {
        if (!links.contains(URL)) {
            try {
                Document document = Jsoup.connect(URL).get();
                Elements otherLinks = document.select("a[href^=\"http://www.example.com/page/\"]");

                for (Element page : otherLinks) {
                    if (links.add(URL)) {
                        //Remove the comment from the line below if you want to see it running on your editor
                        System.out.println(URL);
                    }
                    getPageLinks(page.attr("abs:href"));
                }
            } catch (IOException e) {
                System.err.println(e.getMessage());
            }
        }
    }

    //Connect to each link saved in the article and find all the articles in the page
    public void getArticles() {
        links.forEach(x -> {
            Document document;
            try {
                document = Jsoup.connect(x).get();
                Elements articleLinks = document.select("h2 a[href^=\"http://www.example.com/\"]");
                for (Element article : articleLinks) {
                    //Only retrieve the titles of the articles that contain Java 8
                    if (article.text().matches("^.*?(Java 8|java 8|JAVA 8).*$")) {
                        //Remove the comment from the line below if you want to see it running on your editor,
                        //or wait for the File at the end of the execution
                        //System.out.println(article.attr("abs:href"));

                        ArrayList temporary = new ArrayList<>();
                        temporary.add(article.text()); //The title of the article
                        temporary.add(article.attr("abs:href")); //The URL of the article
                        articles.add(temporary);
                    }
                }
            } catch (IOException e) {
                System.err.println(e.getMessage());
            }
        });
    }

    public void writeToFile(String filename) {
        FileWriter writer;
        try {
            writer = new FileWriter(filename);
            articles.forEach(a -> {
                try {
                    String temp = "- Title: " + a.get(0) + " (link: " + a.get(1) + ")\n";
                    //display to console
                    System.out.println(temp);
                    //save to file
                    writer.write(temp);
                } catch (IOException e) {
                    System.err.println(e.getMessage());
                }
            });
            writer.close();
        } catch (IOException e) {
            System.err.println(e.getMessage());
        }
    }

    public static void main(String[] args) {
        Extractor bwc = new Extractor();
        bwc.getPageLinks("http://www.example.com");
        bwc.getArticles();
        bwc.writeToFile("Java 8 Articles");
    }
}

出力:

jsoup-web-crawler-example-3