數據結構思維筆記(八)到達哲學

本章的目標是開發一個Web爬蟲,同時驗證之前提到的 到達哲學

1.起步

首先介紹本章中幫你起步的代碼:

  • WikiNodeExample.java包含前一章的代碼,展示了 DOM 樹中深度優先搜索(DFS)的遞歸和迭代實現。
  • WikiNodeIterable.java包含Iterable類,用於遍歷 DOM 樹。我將在下一節中解釋這段代碼。
  • WikiFetcher.java包含一個工具類,使用jsoup從維基百科下載頁面。爲了幫助你遵守維基百科的服務條款,此類限制了你下載頁面的速度;如果你每秒請求許多頁,在下載下一頁之前會休眠一段時間。
  • WikiPhilosophy.java包含你爲此練習編寫的代碼的大綱。我們將在下面進行說明。

2.可迭代對象和迭代器

在前一章中,我展示了迭代式深度優先搜索(DFS),並且認爲與遞歸版本相比, 迭代版本的優點在於, 它更容易包裝在Iterator對象中(Iterable是一種數據結構,Iterator是用來迭代的迭代器)。在本節中,我們將看到如何實現它。

外層的類WikiNodeIterable實現Iterable<Node>接口,所以我們可以在一個for循環中使用它:

Node root = ...
Iterable<Node> iter = new WikiNodeIterable(root);
for (Node node: iter) {
    visit(node);
}

其中 root爲樹的根節點,visit() 是到Node節點時,你想做的任意的事.

WikiNodeIterable的實現遵循以下慣例:

  • 構造函數接受並存儲根Node的引用。
  • iterator方法創建一個返回一個Iterator對象。

下邊是它的樣子

public class WikiNodeIterable implements Iterable<Node> {
    private  Node root;
    
    // 從給定的節點開始創造一個迭代器
    public WikiNodeIterable(Node root) {
        this.root = root;
    }

    @Override
    public Iterator<Node> iterator() {
        return new WikiNodeIterator(root);
    }

}

內部類 WikiNodeIterator執行所有實際工作

//實現Iterator的內部類
private class WikiNodeIterator implements Iterator<Node> {

    // 這個堆棧跟蹤等待訪問的節點
    Deque<Node>  stack;

    // 使用堆棧上的根節點初始化Iterator。
    public WikiNodeIterator(Node node) {
        stack = new ArrayDeque<Node>();
        stack.push(node);
    }

    @Override
    public boolean hasNext() {
        return !stack.isEmpty();
    }

    @Override
    public Node next() {
        if (stack.isEmpty()) {
            throw new NoSuchElementException();
        }
        // 出棧
        Node node = stack.pop();
        // 反轉後將child節點放入堆棧中
        List<Node> nodes = new ArrayList<Node>(node.childNodes());
        Collections.reverse(nodes);
        for (Node child : nodes) {
            stack.push(child);
        }
        return node;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

}

該代碼與 DFS 的迭代版本幾乎相同(DFS是寫在一塊的),但現在分爲三個方法:

  • 構造函數初始化棧(使用一個ArrayDeque實現)並將根節點壓入這個棧。
  • isEmpty檢查棧是否爲空。
  • nextNode棧中彈出下一個節點,按相反的順序壓入子節點,並返回彈出的Node。如果有人在空Iterator上調用next,則會拋出異常。

可能不明顯的是,值得使用兩個類和五個方法,來重寫一個完美的方法。但是現在我們已經完成了,在需要Iterable的任何地方,我們可以使用WikiNodeIterable,這使得它的語法整潔,易於將迭代邏輯(DFS)與我們對節點的處理分開。

3.WikiFetcher

編寫 Web 爬蟲時,很容易下載太多頁面,這可能會違反你要下載的服務器的服務條款。爲了幫助你避免這種情況,我提供了一個WikiFetcher類,它可以做兩件事情:

  • 它封裝了我們在上一章中介紹的代碼,用於從維基百科下載頁面,解析 HTML 以及選擇內容文本
  • 它測量請求之間的時間,如果我們在請求之間沒有足夠的時間,它將休眠直到經過了合理的間隔。默認情況下,間隔爲1秒(防止過度請求)。

這裏是WikiFetcher的定義:

public class WikiFetcher {

    private long lastRequestTime = -1;
    private long minIterval = 1000;


    /**
     * @Author Ragty
     * @Description  找到並解析數據
     * @Date 10:15 2019/4/16
     **/
    public Elements fetchWikiPedia(String url) throws IOException {
        sleepIfNeed();
        Connection connection = Jsoup.connect(url);
        Document doc = connection.get();
        Element content = doc.getElementById("mw-content-text");
        Elements para = content.select("p");
        return para;
    }


    /**
     * @Author Ragty
     * @Description 通過最小訪問時間來限制範圍訪問頻率
     * @Date 10:43 2019/4/16
     **/
    private void sleepIfNeed() {
        if (lastRequestTime != -1) {
            long currentTime = System.currentTimeMillis();
            long nextRequestTime = lastRequestTime + minIterval;
            if (currentTime < lastRequestTime) {
                try {
                    Thread.sleep(nextRequestTime - currentTime);
                } catch (InterruptedException e) {
                    System.err.println("Warning: sleep interrupted in fetchWikipedia.");
                }
            }
        }
        lastRequestTime = System.currentTimeMillis();
    }

}

新的代碼是sleepIfNeeded,它檢查自上次請求以來的時間,如果經過的時間小於minInterval(毫秒),則休眠。

4.練習

WikiPhilosophy.java中,你會發現一個簡單的main方法,展示瞭如何使用這些部分。從這個代碼開始,你的工作是寫一個爬蟲:

  1. 獲取維基百科頁面的 URL,下載並分析。
  2. 它應該遍歷所得到的 DOM 樹來找到第一個 有效的鏈接。我會在下面解釋“有效”的含義。
  3. 如果頁面沒有鏈接,或者如果第一個鏈接是我們已經看到的頁面,程序應該指示失敗並退出。
  4. 如果鏈接匹配維基百科頁面上的哲學網址,程序應該提示成功並退出。
  5. 否則應該回到步驟1

該程序應該爲它訪問的 URL 構建List並在結束時顯示結果(無論成功還是失敗)

那麼我們應該認爲什麼是“有效的”鏈接?

  • 這個鏈接應該在頁面的內容文本中,而不是側欄或彈出框。
  • 不應該是斜體或括號
  • 你應該跳過外部鏈接,當前頁面的鏈接和紅色鏈接。
  • 在某些版本中,如果文本以大寫字母開頭,則應跳過鏈接。

如果你有足夠的信息來起步,請繼續。或者你可能想要閱讀這些提示:

  • 當你遍歷樹的時候,你將需要處理的兩種NodeTextNodeElement。如果你找到一個Element,你可能需要轉換它的類型,來訪問標籤和其他信息。
  • 當你找到包含鏈接的Element通過向上跟蹤父節點鏈,可以檢查是否是斜體。如果父節點鏈中有一個<i><em>標籤,鏈接爲斜體。
  • 爲了檢查鏈接是否在括號中,你必須在遍歷樹時掃描文本,並跟蹤開啓和閉合括號(理想情況下,你的解決方案應該能夠處理嵌套括號(像這樣))。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章