1

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

 2 years ago
source link: http://www.veiking.cn/blog/1059-page.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

之前讲了很多关于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: [email protected]
         */

        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 [email protected] 
* 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 [email protected] * @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 [email protected] * @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浏览器的丰富功能,这个在后面具体用到的地方,我们也总结整理一下。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK