老狗啃爬虫-动态页面爬取之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浏览器的丰富功能,这个在后面具体用到的地方,我们也总结整理一下。