手把手教你搭建JAVA分散式爬蟲

2022-08-30 06:03:08

      在工作中,我們經常需要去獲取一些資料,但是這些資料可能需要從第三方平臺才可以獲取到。這個時候,爬蟲系統就可以幫助我們來完成這些事情。

      提到爬蟲系統,很多人都會想到使用python。但實際上,語言只是一種工具,其背後的設計思想和技術原理才是精髓,這篇關於Java分散式爬蟲的文章會帶著大家一步一步搭建一個適合Java開發者的爬蟲系統。

      第一部分:搭建一個簡單的爬蟲系統

  現在,我們就來嘗試下通過自動化方法來獲取https://www.cnblogs.com/的首頁內容。在正式開始編寫程式碼之前,我們需要安裝兩個重要的程式,一個是chromedriver,一個是chrome。

chrome瀏覽器的下載地址:https://chrome.en.softonic.com/
chromedriver下載地址:http://chromedriver.storage.googleapis.com/index.html

注意:在安裝這兩個軟體的時候,它們的版本需要對應起來才能正常work。

      接下來我要給大家介紹一下Selenium webdriver這個開源元件,Selenium是一個用於Web應用程式測試的工具。Selenium測試直接執行在瀏覽器中,就像真正的使用者在操作一樣。支援的瀏覽器包括IE(7, 8, 9, 10, 11),Mozilla FirefoxSafariGoogle ChromeOpera,Edge等。Selenium webdriver是程式語言和瀏覽器之間的通訊工具,它的工作流程如下圖所示。     

    環境搭建好之後,我們就開始進入實際開發環節。首先,我們建立一個WebDriverFactory。

@Service
public class WebDriverFactory {

    @Value("${chrome.path}")
    private String chromePath;
    
    @Autowired
    private ProxyPool proxyPool;

    public WebDriver createWebDriver(boolean useProxy) {
        System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, "/Users/****/Downloads/chromedriver");
        ArrayList<String> arguments = Lists.newArrayList("--no-sandbox",
                "--disable-dev-shm-usage",
                "--disable-web-security",
                "--ignore-certificate-errors",
                "--allow-running-insecure-content",
                "--allow-insecure-localhost",
                "--disable-images",
                "--disable-gpu",
                "--disable-blink-features=AutomationControlled",
                "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.16 Safari/537.36",
                "--cache-control=no-cache");
        ChromeOptions options = new ChromeOptions();
        options.setHeadless(true);
        options.addArguments(arguments);
        /** 設定使用代理 **/
        if (useProxy) {
            Proxy proxy = proxyPool.getProxy();
            options.setProxy(proxy);
        }
        Map<String, Object> prefs = Maps.newHashMap();
        prefs.put("profile.default_content_settings.popups", 1);
        prefs.put("profile.default_content_setting_values.notifications", 1);
        options.setExperimentalOption("prefs", prefs);
        ChromeDriver webDriver = new ChromeDriver(options);
        Map<String, Object> params = Maps.newHashMap();
//      params.put("source", "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})");
        params.put("source", "() => {" + 
                "      if (navigator.webdriver === false) {" + 
                "        continue" + 
                "      } else if (navigator.webdriver === undefined) {" + 
                "        continue" + 
                "      } else {" + 
                "        delete Object.getPrototypeOf(navigator).webdriver" + 
                "      }" + 
                "    }");
        webDriver.executeCdpCommand("Page.addScriptToEvaluateOnNewDocument", params);
        webDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS).pageLoadTimeout(20, TimeUnit.SECONDS)
                .setScriptTimeout(10, TimeUnit.SECONDS);
        return webDriver;
    }
}

上面具體到引數和設定,我們後續會進行詳細解釋,現在我們直接執行相關的程式碼獲取https://www.cnblogs.com/的首頁內容。

    @Test
    public void testGrabPage() {
        WebDriver webDriver = null;
        try {
            String currentPageUrl = "https://xiaozhuanlan.com/";
            webDriver =  webDriverFactory.createWebDriver(false);
            webDriver.get(currentPageUrl);
            Thread.sleep(1000);
            String html = webDriver.getPageSource();
            System.out.println(html);
        } catch (Exception e) {
        	e.printStackTrace();
        } finally {
            webDriver.quit();
        }
    }

通過上面的程式碼,我們可以列印出部落格園網站首頁的全部資訊。

第二部分:模擬使用者行為

  在上一個部分中,我們可以獲取到「部落格園」首頁的完整內容,在這一篇文章中我們將實現在百度網站自動化搜尋「部落格園「,並且跳轉到「部落格園」首頁。
  在實現模擬登入之前,我們需要掌握如何定位到自己關心的元素。Selenium中有8種方法可以定位到元素。具體的定位方法可以檢視org.openqa.selenium.By這個類。假設我們現在需要定位到如下一個元素:

<tagName attributeName='attributeValue'></tagName>

  那麼我們可以根據以下的方法進行定位:

  1. driver.findElement(By.name("attributeName"),根據元素的屬性名稱進行定位
  2. driver.findElement(By.tagName("tagName"),根據元素的名稱來進行定位
  3. driver.findElement(By.xpath("tagName[@attributeName='attributeValue']")),根據元素的xpath表示式來進行定位
  4. driver.findElement(By.cssSelector("tagName[attributeName='attributeValue']")),根據元素的CSS選擇器來進行定位

上述介紹的元素定位方法如果發現有多個元素可以匹配的,則會選擇該頁面中第一個符合條件的元素。

      接下來,我們編寫模擬使用者搜尋「部落格園」行為的程式碼,

    @Test
    public void searchTest() {
        FenbiChromeDriver webDriver = null;
        try {
            webDriver = (FenbiChromeDriver) webDriverFactory.createWebDriver(false);
            String currentPageURL = "http://www.baidu.com";
            webDriver.get(currentPageURL);
            Thread.sleep(2000);
            WebElement searchInputElem = webDriver.findElement(By.xpath("//*[@id=\"kw\"]"));
            searchInputElem.sendKeys("部落格園");
            WebElement searchButtonElem = webDriver.findElement(By.xpath("//*[@id=\"su\"]"));
            searchButtonElem.click();
            Thread.sleep(2000);
            WebElement searchResultList = webDriver.findElement(By.xpath("//*[@id=\"content_left\"]"));
            WebElement xiaozhuanlanElem = searchResultList.findElement(By.xpath("//*[@id=\"1\"]/div/div[1]/h3/a"));
            xiaozhuanlanElem.click();
            Thread.sleep(2000);
            System.out.println(webDriver.getPageSource());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            webDriver.quit();
        }
    }

結合上一部分中的WebDriverFactory,並執行上面的程式碼,我們就可以自動跳轉到部落格園網站的首頁了。

第三部分:判斷元素是否載入完畢

當我們需要判斷我們關注的元素是否載入完畢的時候,在Selenium框架下有隱式等待和顯式等待兩種方式。

隱式等待是在建立webdriver的時候設定的超時時間,在整個的webdriver生命週期內都是有效的。設定了隱式等待後,Selenium在執行findElement的DriverCommand時候會一直等待,直到獲取到對應的元素。設定隱式等待的方法如下:

webDriver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)
.pageLoadTimeout(20, TimeUnit.SECONDS)
.setScriptTimeout(10, TimeUnit.SECONDS);
webDriver.findElement(By.xpath("//*[@id=\"kwddddd\"]"));

接下來,我們跟隨Selenium的隱式等待模式來看看Selenium抓取網頁的處理流程是什麼樣的。

從上面的處理時序圖我們可以看出,Selenium與webdriver的互動主要是通過RemoteWebDriver,DriverCommandExecutor和HttpCommandExecutor這三個類來完成的。另外一個比較重要的interface是DriverCommand,這個介面裡面列舉了webdriver支援的所有命令。

顯示等待是使用WebDriverWait通過不斷輪詢的方式來完成的,範例程式碼如下所示,

    @Test
    public void webDriverWaitTest() {
        FenbiChromeDriver webDriver = null;
        try {
            webDriver = (FenbiChromeDriver) webDriverFactory.createWebDriver(false);
            new WebDriverWait(webDriver, 20).until((Function<WebDriver, Boolean>) driver -> {
                String currentPageURL = "http://www.baidu.com";
                driver.get(currentPageURL);
                String html = driver.getPageSource();
                if(html.contains("hello word")) {
                	return true;
                } else {
                	return false;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            webDriver.quit();
        }
    }

顯示等待的處理邏輯主要是在FluentWait類中的until方法中來完成的,預設情況下until方法會每隔500ms去執行Function實現類中的邏輯,檢查執行結果是否為True,如果為True,則返回。如果為False,則sleep 500ms,直到結果為True,或者直到超時。

      上面until方法的引數比較有意思,它需要是Function介面的實現類,範型介面Function<F, T>是google公司的開源元件Guava中的一個介面。這個介面只有一個內部方法apply,其中的F是apply方法的輸入引數,T是apply方法的返回值。通過Function介面,Selenium就為WebDriverWait提供了一個很好的擴充套件點。我們在日常的開發中也可以借鑑這樣的開發方法。

      好了,今天就先和大家聊到這裡吧,一個完善的爬蟲還有很多其他的處理邏輯需要新增和處理。例如:如何應對反爬蟲機制,如何實現使用者的自動登入,如何對頁面進行截圖等等。感興趣的小夥伴兒可以加我們的技術交流群或者加我的微信。