Veiking百草园


/ 编程开发

老狗啃爬虫-动态页面爬取之Selenium

老狗啃骨头   @Veiking   2021-01-01

老狗啃爬虫-动态页面爬取之Selenium

摘要:

之前讲了很多关于webmagic的爬虫实现方法,都是基于静态网页的,我们只需考虑根据链接下载页面,然后解析html提取目标数据即可。然而,很多网站的页面数据是动态的,那么简单的下载解析将毫无意义,这时候我们就得借助额外的技术方案来达成目的,这里我们准备借助一个爬取动态网页信息比较实用的插件工具,即是Selenium,来实现我们的爬虫程序

  我们之前讲了很多关于webmagic的爬虫实现方法,但都是基于静态网页的,我们只需考虑根据链接下载页面,然后解析html提取目标数据即可。然而目前,很多网站的页面数据是动态生成的,那么简单的下载解析将毫无意义,这时候我们就得借助额外的技术方案来达成目的,这里我们准备借助一个爬取动态网页信息比较实用的插件工具,即是Selenium,来实现我们的爬虫程序。

Selenium

  Selenium原本是用于Web应用程序的测试工具,它可以结合浏览器一起运行,可以像真正的用户一样,去模拟操作行为。
  目前,Selenium支持包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等在内的各种主流浏览器,这也使其在浏览器兼容性测试、自动化测试领域大展风采。Selenium也支持诸如ava,Ruby,Python,Perl,PHP,C#等各种编程语言编写的用例脚本,所以其应用也越来越为广泛。
  这里我们就借助Selenium插件强大的功能特性,来实现动态网页信息的爬取

材料准备

安装chromedriver

  我们这里使用Selenium插件,一般是跟Chrome浏览器结合着使用的,程序编写开始之前,我们还须安装chromedriver,chromedriver类似于一个浏览器驱动,暴露一些浏览器的API,这样我们就可以通过Selenium去操作Chrome浏览器,来模拟用户行为。
  给大家推荐两个chromedriver的下载地址:

1、http://chromedriver.storage.googleapis.com/index.html
2、https://npm.taobao.org/mirrors/chromedriver/

  另外要注意chromedriver的版本与Chrome的版本一定要一致,不然就无法正常运行,我们可以在浏览器中输入 chrome://version/ 指令,根据我们使用的浏览器版本信息,去选取与之对应的chromedriver。

添加依赖

  chromedriver安装完毕之后,还要添加与Selenium相关的依赖,我们在pom.xml文件的中添加如下代码:

    <!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-selenium -->
    <dependency>
        <groupId>us.codecraft</groupId>
        <artifactId>webmagic-selenium</artifactId>
        <version>0.7.4</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>3.141.59</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-chrome-driver -->
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-chrome-driver</artifactId>
        <version>3.141.59</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-server -->
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-server</artifactId>
        <version>3.141.59</version>
    </dependency>

  这里添加的依赖包即是支持Selenium插件相关所需的,添加完毕,更新项目,下一步我们就可以去编写程序实现功能了。

前情提要

  开始编码之前,我们还是要先看看Webmagic关于Selenium的源码,于是我们找到webmagic-selenium-0.7.4.jar,点击去查看:



  看这里,原来,这里是有两个实现类,一个是SeleniumDownloader类,一个是WebDriverPool类。从名字上可以猜测,SeleniumDownloader是重新实现了Downloader的逻辑,WebDriverPool大概就是跟WebDriver相关的线程池子之类的。我们节省篇幅,我们直接看SeleniumDownloader的主要方法download:

    @Override
    public Page download(Request request, Task task) {
        checkInit();
        WebDriver webDriver;
        try {
            webDriver = webDriverPool.get();
        } catch (InterruptedException e) {
            logger.warn("interrupted", e);
            return null;
        }
        logger.info("downloading page " + request.getUrl());
        webDriver.get(request.getUrl());
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        WebDriver.Options manage = webDriver.manage();
        Site site = task.getSite();
        if (site.getCookies() != null) {
            for (Map.Entry cookieEntry : site.getCookies()
                    .entrySet()) {
                Cookie cookie = new Cookie(cookieEntry.getKey(),
                        cookieEntry.getValue());
                manage.addCookie(cookie);
            }
        }

        /*
         * TODO You can add mouse event or other processes
         * 
         * @author: bob.li.0718@gmail.com
         */

        WebElement webElement = webDriver.findElement(By.xpath("/html"));
        String content = webElement.getAttribute("outerHTML");
        Page page = new Page();
        page.setRawText(content);
        page.setHtml(new Html(content, request.getUrl()));
        page.setUrl(new PlainText(request.getUrl()));
        page.setRequest(request);
        webDriverPool.returnToPool(webDriver);
        return page;
    }

  可以看出来,SeleniumDownloader一番操作,旨在借助WebDriverPool来管理WebDriver,进行页面的读取处理,那我们就来研究研究WebDriverPool类:

package us.codecraft.webmagic.downloader.selenium;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriver;
import org.openqa.selenium.phantomjs.PhantomJSDriverService;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author code4crafter@gmail.com 
* Date: 13-7-26
* Time: 下午1:41
*/ class WebDriverPool { private Logger logger = LoggerFactory.getLogger(getClass()); private final static int DEFAULT_CAPACITY = 5; private final int capacity; private final static int STAT_RUNNING = 1; private final static int STAT_CLODED = 2; private AtomicInteger stat = new AtomicInteger(STAT_RUNNING); /* * new fields for configuring phantomJS */ private WebDriver mDriver = null; private boolean mAutoQuitDriver = true; private static final String DEFAULT_CONFIG_FILE = "/data/webmagic/webmagic-selenium/config.ini"; private static final String DRIVER_FIREFOX = "firefox"; private static final String DRIVER_CHROME = "chrome"; private static final String DRIVER_PHANTOMJS = "phantomjs"; protected static Properties sConfig; protected static DesiredCapabilities sCaps; /** * Configure the GhostDriver, and initialize a WebDriver instance. This part * of code comes from GhostDriver. * https://github.com/detro/ghostdriver/tree/master/test/java/src/test/java/ghostdriver * * @author bob.li.0718@gmail.com * @throws IOException */ public void configure() throws IOException { // Read config file sConfig = new Properties(); String configFile = DEFAULT_CONFIG_FILE; if (System.getProperty("selenuim_config")!=null){ configFile = System.getProperty("selenuim_config"); } sConfig.load(new FileReader(configFile)); // Prepare capabilities sCaps = new DesiredCapabilities(); sCaps.setJavascriptEnabled(true); sCaps.setCapability("takesScreenshot", false); String driver = sConfig.getProperty("driver", DRIVER_PHANTOMJS); // Fetch PhantomJS-specific configuration parameters if (driver.equals(DRIVER_PHANTOMJS)) { // "phantomjs_exec_path" if (sConfig.getProperty("phantomjs_exec_path") != null) { sCaps.setCapability( PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY, sConfig.getProperty("phantomjs_exec_path")); } else { throw new IOException( String.format( "Property '%s' not set!", PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY)); } // "phantomjs_driver_path" if (sConfig.getProperty("phantomjs_driver_path") != null) { System.out.println("Test will use an external GhostDriver"); sCaps.setCapability( PhantomJSDriverService.PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY, sConfig.getProperty("phantomjs_driver_path")); } else { System.out .println("Test will use PhantomJS internal GhostDriver"); } } // Disable "web-security", enable all possible "ssl-protocols" and // "ignore-ssl-errors" for PhantomJSDriver // sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS, new // String[] { // "--web-security=false", // "--ssl-protocol=any", // "--ignore-ssl-errors=true" // }); ArrayList cliArgsCap = new ArrayList(); cliArgsCap.add("--web-security=false"); cliArgsCap.add("--ssl-protocol=any"); cliArgsCap.add("--ignore-ssl-errors=true"); sCaps.setCapability(PhantomJSDriverService.PHANTOMJS_CLI_ARGS, cliArgsCap); // Control LogLevel for GhostDriver, via CLI arguments sCaps.setCapability( PhantomJSDriverService.PHANTOMJS_GHOSTDRIVER_CLI_ARGS, new String[] { "--logLevel=" + (sConfig.getProperty("phantomjs_driver_loglevel") != null ? sConfig .getProperty("phantomjs_driver_loglevel") : "INFO") }); // String driver = sConfig.getProperty("driver", DRIVER_PHANTOMJS); // Start appropriate Driver if (isUrl(driver)) { sCaps.setBrowserName("phantomjs"); mDriver = new RemoteWebDriver(new URL(driver), sCaps); } else if (driver.equals(DRIVER_FIREFOX)) { mDriver = new FirefoxDriver(sCaps); } else if (driver.equals(DRIVER_CHROME)) { mDriver = new ChromeDriver(sCaps); } else if (driver.equals(DRIVER_PHANTOMJS)) { mDriver = new PhantomJSDriver(sCaps); } } /** * check whether input is a valid URL * * @author bob.li.0718@gmail.com * @param urlString urlString * @return true means yes, otherwise no. */ private boolean isUrl(String urlString) { try { new URL(urlString); return true; } catch (MalformedURLException mue) { return false; } } /** * store webDrivers created */ private List webDriverList = Collections .synchronizedList(new ArrayList()); /** * store webDrivers available */ private BlockingDeque innerQueue = new LinkedBlockingDeque(); public WebDriverPool(int capacity) { this.capacity = capacity; } public WebDriverPool() { this(DEFAULT_CAPACITY); } /** * * @return * @throws InterruptedException */ public WebDriver get() throws InterruptedException { checkRunning(); WebDriver poll = innerQueue.poll(); if (poll != null) { return poll; } if (webDriverList.size() < capacity) { synchronized (webDriverList) { if (webDriverList.size() < capacity) { // add new WebDriver instance into pool try { configure(); innerQueue.add(mDriver); webDriverList.add(mDriver); } catch (IOException e) { e.printStackTrace(); } // ChromeDriver e = new ChromeDriver(); // WebDriver e = getWebDriver(); // innerQueue.add(e); // webDriverList.add(e); } } } return innerQueue.take(); } public void returnToPool(WebDriver webDriver) { checkRunning(); innerQueue.add(webDriver); } protected void checkRunning() { if (!stat.compareAndSet(STAT_RUNNING, STAT_RUNNING)) { throw new IllegalStateException("Already closed!"); } } public void closeAll() { boolean b = stat.compareAndSet(STAT_RUNNING, STAT_CLODED); if (!b) { throw new IllegalStateException("Already closed!"); } for (WebDriver webDriver : webDriverList) { logger.info("Quit webDriver" + webDriver); webDriver.quit(); webDriver = null; } } }

  这里,可以看到,WebDriverPool的核心功能即是通过加载文件配置,实例化WebDriver,然后存入webDriverList集合,并由innerQueue队列,实现多线程的操作。
  可以看出来,WebDriverPool的主要目的是避免创建过多的WebDriver,即用完了的暂不销毁,先放进待用队列,用的时候再拿过来用。想想也是,每实例化一个WebDriver,即要对应的启动一个浏览器进程,多了可是吃不消。
  过了过代码,看来很多细节方面大佬们已经处理的不错了,对于我们使用者来说,这个configure()方法也是我们额外关心的。
  好了,根据我们学习和以后使用的需要,接下来我们就试着来写写代码,来实现我们想要的爬虫功能。

功能实现

  现在绝大多数移动端的页面,都是通过下拉等操作,来实现页面数据的动态加载,于是我们就选用百度新闻( https://news.baidu.com/news#/ ),作为我们的目标网页,然后模拟移动设备的浏览器,来实现目的。
  还有,我们此次的学习目的,是要利用Selenium,通过chrome浏览器实现动态页面内容的抓取,故源代码中出现的phantomjs、firefox等先暂不考虑,我们的重点是围绕chrome来实现功能,接下来进入重点。

第一步:配置文件的加载

  根据Springboot的文件结构习惯,我们在文件目录src/main/resources下,创建selenium.properties文件,其内容如下:

# WebDriver 参数配置
webdriver.driver=chrome
webdriver.chromePath=D:\\Google\\Chrome\\Application\\Chrome.exe
webdriver.chromedriverPath=D:\\Google\\chromedriver\\chromedriver.exe
webdriver.mobile=true

# WebDriver for Downloader 参数配置 
webdriver.sleepTime=1000
webdriver.thread=2

# PhantomJS 参数配置
#phantomjs_exec_path=
#phantomjs_driver_path=
#phantomjs_driver_loglevel=

  注意这个配置文件的位置,我们的建在application.yml同级的地方,这位置是springboot默认读取配置文件的地方。
  接着,既然我们是在用springboot这种技术,这里就不准备再用我们自己去读文件加载,直接对着selenium.properties文件中要用的属性内容,我们创建一个对应的java类:

package cn.veiking.base.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import lombok.Data;

/**
* @author    :Veiking
* @version    :2020年12月25日 下午10:03:40 
* 说明        :Selenium配置文件
*/

@Data
@Component
@PropertySource(value="classpath:selenium.properties", encoding = "utf-8")
@ConfigurationProperties(prefix="webdriver", ignoreUnknownFields = true) // 匹配properties前缀属性 'webdriver'
public class SeleniumConfig {
    // WebDriver 参数配置
    private String driver;    // 目前支持 chrome
    private String chromePath; 
    private String chromedriverPath;

    // WebDriver for Downloader 参数配置 
    private Integer sleepTime;
    private Integer thread;

    // WebDriver for mobile 参数配置
    private Boolean mobile;
}

  注意SeleniumConfig类的这几个标签:
  @Data不用解释了,getter、setter;@Component是用于spring容器扫描加载,用的时候可通过@Autowired标签直接实例;@PropertySource即是交代一下文件名称路径,注意这里,如果配置文件位置不是与application.yml同级,这里也要加上相应的相对文件路径;@ConfigurationProperties中的prefix则是匹配一下参数属性前缀,即同一个配置文件,也是可以根据的不同前缀参数,用不同的类来对应加载的。
  这种形式的配置文件,我们在用的时候,还需要在使用的类里,加上对应的标签,即要提前加载:

@EnableConfigurationProperties(SeleniumConfig.class)

  (注:这个加载标签写在哪里合适,在spring容器中,有个类加载顺序的概念,这里只要保证不耽误配置文件数据使用,写在使用类加载顺序之前,哪里都可以)
最后,别忘了在启动类StartTest.java那里,写个标签扫描一下:

@ComponentScan(value = {"cn.veiking.base.config"})

  好了,配置文件安排妥当,我们继续下一步。

第二步:实现PageProcessor

  这一步,我们实现的功能也比较简单,就是抓取移动端浏览器百度新闻的标题,然后打印出来。
  我们用浏览器打开网址 https://news.baidu.com/news#/ ,F12开启调试模式,然后用手机模式,我们选这样一条记录:



  然后右键,审查元素,看到如下代码:



  通过对比查看同列其他的新闻标题,得出结论,即根据红框框内的这些类属性,可确定我们所要的新闻题目。
  接下来就简单了,我们写一个PageProcessor,来获取新闻标题,代码如下:

package cn.veiking.processor;

import java.util.List;

import org.springframework.stereotype.Component;

import cn.veiking.base.common.logs.SimLogger;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;

/**
* @author    :Veiking
* @version    :2020年12月25日
* 说明        :Selenium测试Processor
*/
@Component
public class SeleniumTestProcessor implements PageProcessor {
    SimLogger logger = new SimLogger(this.getClass());
    @Override
    public Site getSite() {
        Site site = Site.me().setRetryTimes(5).setSleepTime(1000).setTimeOut(10000);
        return site;
    }
    // 重写process,获取titles
    @Override
    public void process(Page page) {
        String xpathStr = "//*[@class='index-list-item-container']//div[@class='index-list-main-title']/text()";
        List titles = page.getHtml().xpath(xpathStr).all();
        page.putField("titles", titles);
        logger.info("SeleniumTestProcessor titles[titles={}]", titles);
    }
}

  OK, Processor已完成,我们进行下一步。

第三步:重写WebDriver线程池类

  我们要写这个WebDriver线程池,重要还是他的configure()方法,我们这里主要是想模拟移动设备启用Chrome,所以也就不用考虑太多其他的情况,直接创建我们的VWebDriverPool.java文件,在原来的代码基础上开始修改,结果如下:

package cn.veiking.selenium;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

import cn.veiking.base.common.logs.SimLogger;
import cn.veiking.base.config.SeleniumConfig;

/**
* @author    :Veiking
* @version    :2020年12月25日
* 说明        :webDriver进程池(本类旨在少创建webDriver(程序进程,理论上每实例化一个即是打开一个浏览器,这个多了系统吃不消),用完先放进池子还可以再次利用,尽可能的复用已有的进程)    
*/

public class VWebDriverPool {
    private SimLogger logger = new SimLogger(this.getClass());
    // 用于测试的主界面,其作用类似于web浏览器
    private WebDriver webDriver = null;
    // 存放已创建的WebDriver
    private List webDriverList = Collections.synchronizedList(new ArrayList());
    // 将WebDriver置入队列
    private BlockingDeque innerQueue = new LinkedBlockingDeque();

    private SeleniumConfig seleniumConfig; // Selenium-WebDriver配置文件

    private final int poolSize;    // 线程容量
    private final static int STAT_RUNNING = 1;    // 线程状态
    private final static int STAT_CLODED = 2;    // 线程状态
    private AtomicInteger stat = new AtomicInteger(STAT_RUNNING);    // 线程状态

    private static final String DRIVER_CHROME = "chrome";        // chrome(谷歌)浏览器

    public VWebDriverPool(int poolSize , SeleniumConfig seleniumConfig) {
        this.poolSize = poolSize;
        this.seleniumConfig = seleniumConfig;
    }

    /**
     * WebDriver构建
     */
    public void configure() throws IOException {
        logger.info("VWebDriverPool load ... [seleniumConfig={}]", seleniumConfig);
        String driver = seleniumConfig.getDriver();    // 主驱方式
        // 只考虑chrome,默认chrome
        if(StringUtils.isEmpty(driver)) {
            driver = DRIVER_CHROME;
        }
        // 初始WebDriver
        if (driver.equals(DRIVER_CHROME)) {    
            // 浏览器启动设置
            ChromeOptions chromeOptions = new ChromeOptions();
            // chromeOptions.addArguments("--incognito"); // 设置隐身模式
            // chromeOptions.addArguments("--headless"); // 设置浏览器不弹窗
            if(seleniumConfig.getMobile()) {
                chromeOptions.addArguments("--user-agent=Galaxy S5"); // 设置手机设备-浏览器访问
            }
            webDriver = new ChromeDriver(chromeOptions);
            if(seleniumConfig.getMobile()) {
                webDriver.manage().window().setSize(new Dimension(500, 800)); // 浏览器size
            }
        }else {
            logger.info("VWebDriverPool load faild for configure ... [driver={}]", driver);
        }
    }

    // 池子里有就拿去用,已经没了,就新建,建到容量出会被锁住
    public WebDriver get() throws InterruptedException {
        checkRunning();
        WebDriver poll = innerQueue.poll();
        if (poll != null) {
            return poll;
        }
        if (webDriverList.size() < poolSize) {
            synchronized (webDriverList) {
                if (webDriverList.size() < poolSize) {
                    // 新增新的WebDriver
                    try {
                        this.configure();
                        innerQueue.add(webDriver);
                        webDriverList.add(webDriver);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return innerQueue.take();
    }

    // 用完还回来,继续放池子里(队列)
    public void returnToPool(WebDriver webDriver) {
        checkRunning();
        innerQueue.add(webDriver);
    }

    protected void checkRunning() {
        if (!stat.compareAndSet(STAT_RUNNING, STAT_RUNNING)) {
            throw new IllegalStateException("Already closed!");
        }
    }
    // 关闭
    public void closeAll() {
        boolean b = stat.compareAndSet(STAT_RUNNING, STAT_CLODED);
        if (!b) {
            throw new IllegalStateException("Already closed!");
        }
        for (WebDriver webDriver : webDriverList) {
            logger.info("Quit webDriver" + webDriver);
            webDriver.quit(); // 关闭所有相关窗口,退出
            webDriver = null;
        }
    }

}

  这里注意当参数配置判定是移动模式时,ChromeOptions设置“—user-agent”参数为“Galaxy S5”,即可模拟Galaxy S5手机设备来使用Chrome浏览器模拟操作。
  后边又顺便设置了下浏览器打开后的大小,这个是为了验证的时候看着协调一些。
  关于我们现在重写的这个线程池,本次测试学习可能体现不出其设计的初衷用意,毕竟我们URL队列里只有一个新闻列表的链接;当我们真正开始去抓取内容页的数据时候,Spider的队列里就会有很多待抓页面,这个线程池便可以达到多线程的效果,发挥其强大的功能。

第四步:重写Downloader类

  原本还想着复用一下webmagic自带的SeleniumDownloader,但看了看代码,它使用的这个WebDriverPool既没办法继承,代码里又没办法修改,是没办法偷懒省事儿的,于是,我们就新创建一个VSeleniumDownloader.java文件,拿着源码开始改造,完成如下:

package cn.veiking.selenium;

import java.io.Closeable;
import java.io.IOException;
import java.util.Map;

import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import cn.veiking.base.common.logs.SimLogger;
import cn.veiking.base.config.SeleniumConfig;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.downloader.Downloader;
import us.codecraft.webmagic.selector.PlainText;

/**
 * @author :Veiking
 * @version :2020年12月25日 说明 :使用Selenium调用浏览器进行渲染。目前仅支持chrome(需要下载Selenium
 *          driver支持)
 */
@Component
@EnableConfigurationProperties(SeleniumConfig.class)
public class VSeleniumDownloader implements Downloader, Closeable {
    private SimLogger logger = new SimLogger(this.getClass());

    private volatile VWebDriverPool webDriverPool;

    @Autowired
    SeleniumConfig seleniumConfig;

    private int sleepTime = 0; // 等待时间,等待处理成功
    private int thread = 1; // 并行个数

    @Override
    public Page download(Request request, Task task) {
        checkInit();
        // 实例化WebDriver前必须配置
        System.getProperties().setProperty("webdriver.chrome.driver", seleniumConfig.getChromedriverPath());
        WebDriver webDriver;
        try {
            webDriver = webDriverPool.get();
        } catch (InterruptedException e) {
            logger.info("WebDriver get Exception [exception={}]", e);
            return null;
        }
        logger.info("VSeleniumDownloader downloading page [url={}]", request.getUrl());
        webDriver.get(request.getUrl());
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        WebDriver.Options manage = webDriver.manage();
        Site site = task.getSite();
        if (site.getCookies() != null) {
            for (Map.Entry cookieEntry : site.getCookies().entrySet()) {
                Cookie cookie = new Cookie(cookieEntry.getKey(), cookieEntry.getValue());
                manage.addCookie(cookie);
            }
        }

        // 模拟下拉,刷新页面
        for (int i = 0; i < 5; i++) {
            logger.info("休眠1秒,进行下拉...");
            try {
                // 滚动到最底部
                ((JavascriptExecutor) webDriver).executeScript("window.scrollTo(0,document.body.scrollHeight)");
                // 暂歇3秒,等待页面加载
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        WebElement webElement = webDriver.findElement(By.xpath("/html"));
        String content = webElement.getAttribute("outerHTML");
        Page page = new Page();
        page.setRawText(content);
        page.setUrl(new PlainText(request.getUrl()));
        page.setRequest(request);
        webDriverPool.returnToPool(webDriver);
        return page;
    }

    // 初始线程池
    private void checkInit() {
        if (sleepTime == 0 && null != seleniumConfig.getSleepTime()) {
            this.setSleepTime(seleniumConfig.getSleepTime());
        }
        if (thread == 1 && null != seleniumConfig.getThread()) {
            this.setThread(seleniumConfig.getThread());
        }
        // 初始线程池
        if (webDriverPool == null) {
            synchronized (this) {
                webDriverPool = new VWebDriverPool(thread, seleniumConfig);
            }
        }
    }

    // 设置等待时间
    public void setSleepTime(int sleepTime) {
        this.sleepTime = sleepTime;
    }

    // 设置并行线程
    @Override
    public void setThread(int thread) {
        this.thread = thread;
    }

    // 关闭线程池
    @Override
    public void close() throws IOException {
        webDriverPool.closeAll();
    }

}

  这里注意下,由于在VSeleniumDownloader类里,我们要使用配置文件里的参数信息,所以这个类,我们要在类声明之前,加上标签:

@EnableConfigurationProperties(SeleniumConfig.class)

  这样我们在类里直接通过@Autowired就可以直接用配置文件里的参数了。
  好了,经过修改调整,VSeleniumDownloader、VWebDriverPool都写好了,接下来我们就运行一下看看效果。

第五步:测试

  测试就非常简单了,稍作改动,代码如下:

package cn.veiking.processor;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import cn.veiking.StartTest;
import cn.veiking.base.common.logs.SimLogger;
import cn.veiking.selenium.VSeleniumDownloader;
import us.codecraft.webmagic.Spider;

/**
* @author    :Veiking
* @version    :2020年12月25日
* 说明        :SeleniumTest 测试
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartTest.class)
// @EnableConfigurationProperties(SeleniumConfig.class)
public class SeleniumTest {
    SimLogger logger = new SimLogger(this.getClass());

    @Autowired
    private SeleniumTestProcessor seleniumTestProcessor;
    @Autowired
    VSeleniumDownloader seleniumDownloader;

    private static final String url = "https://news.baidu.com/news#/";

    @Test
    public void testSpider() {

        long startTime, endTime;
        logger.info("SeleniumTest testSpider [start={}] ", "开始爬取数据");
        startTime = System.currentTimeMillis();
        Spider.create(seleniumTestProcessor)
        .addUrl(url)
        .setDownloader(seleniumDownloader)
        .run();
        endTime = System.currentTimeMillis();
        logger.info("SeleniumTest testSpider [end={}] ", "爬取结束,耗时约" + ((endTime - startTime) / 1000) + "秒");
    }
}

  换上相应的SeleniumTestProcessor、VseleniumDownloader,然后右键运行。
  果不其然,魔法效果来了,计算机自动启动chrome浏览器,并打开了百度新闻的移动页面:



  随着程序设定的下滑、下滑…操作五次后关闭结束。
  接着我们去看窗口日志,额可以看到:



  一切都如预期,我们的程序成功的借助Selenium插件,模拟移动设备启动了Chrome浏览器,并执行了下拉操作,完美获得动态加载后的新闻标题数据。

结语

  本次学习我们基于Webmagic,通过结合Selenium技术运用,实现了动态网页的抓取。而现实中网页的具体情况可能要相对复杂的多,有时候甚至还需要我们去写一些脚本片段,来获取数据信息,这时候也将会涉及到更多的技术。比如咱们在源码是看到的Phantomjs,Phantomjs是一个功能强大的无界面的浏览器模拟插件,还有,我们看其名字,大概也可以才出来,它在编译解释执行JavaScript脚本必有过人之处。
  Phantomjs不仅是个隐形的浏览器,提供了诸如CSS选择器、支持Web标准、DOM操作、JSON、HTML5、Canvas、SVG等,同时也提供了处理文件I/O的操作,从而使你可以向操作系统读写文件等。在具体应用中,PhantomJS的用处可谓非常广泛,诸如网络监测、网页截屏、无需浏览器的 Web 测试、页面访问自动化等。
  通过Selenium结合PhantomJS技术,我们即可以实现基于webkit浏览器的丰富功能,这个在后面具体用到的地方,我们也总结整理一下。


老狗啃骨头



慷慨发言

(您提供的信息将用于后续必要的反馈联系,本站会恪守隐私)

潜影拾光

羊卓雍错

美丽羊湖,藏南明珠。

扫码转发

二维码
二维码
二维码
二维码
二维码
二维码

博文标签