前情介绍
前段时间开发了一个功能,通过HttpClient访问某个页面,获取页面的全部html内容;之后通过抓取过来的整个页面展示在自己的网页中;但是过了一段时间之后,网页升级了,网页中的图片都变成了动态加载,直接通过HttpClient无法获取完整的页面内容,图片都是懒加载状态无法展示正确内容。
解决
思路
既然没办法直接抓取静态页面,那么可以尝试动态抓取页面;如何抓取动态页面呢,本文里采用了Selenium进行动态抓取页面。通过Selenium模拟正常打开一个浏览器,浏览网页的过程,当浏览到页面的最下方整个页面加载完毕,此时再抓取整个网页的内容就可以完整的获取页面内容。
Selenium的WebDriver可以模拟不同浏览器,本文采用的是Chrome浏览器,其他支持的浏览器大家可以通过查看WebDriver的实现类来获得答案。
编码
引用Selenium库的jar包
org.seleniumhq.selenium
selenium-java
3.141.59
因为要模拟Chrome浏览器,所以要下载chromedriver,到这基础的工作就做完了,接下来是开发代码。废话不多说,直接上代码。
public String selenium(String url) throws InterruptedException {
// 设置 chromedirver 的存放位置
System.getProperties().setProperty("webdriver.chrome.driver", "D:/lingy/chromedriver_win32/chromedriver.exe");
// 设置无头浏览器,这样就不会弹出浏览器窗口
ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.addArguments("--headless");
Long scrollSize = 1000L;
WebDriver webDriver = new ChromeDriver(chromeOptions);
webDriver.get(url);
//设置浏览器的宽高
Dimension dimension = new Dimension(1000, scrollSize.intValue());
webDriver.manage().window().setSize(dimension);
String html = "";
//获取JS执行器,可以执行js代码来控制页面
JavascriptExecutor driver_js= ((JavascriptExecutor) webDriver);
//获取页面的高度
Long scrollHeight = (Long) driver_js.executeScript("return document.body.scrollHeight");
logger.info("article hight is : {}",scrollHeight);
//因为要模拟鼠标滚动浏览网页的效果,所以设置了每次滚动的高度,然后通过整个页面的高度计算出股东的次数
Long loopNum = scrollHeight/scrollSize;
loopNum = loopNum+1;
logger.info("page need scroll times : {}",loopNum);
for(int i =0 ; i < loopNum; i++){
Long start = i*scrollSize;
Long end = (i+1)*scrollSize;
//根据滚动的次数,依次滚动页面,为了图片能加载完全,停留1秒钟
driver_js.executeScript("scroll("+start+","+end+")");
Thread.sleep(1000);
}
//返回页面全部内容
html = (String)driver_js.executeScript("return document.documentElement.outerHTML");
webDriver.close();
return html;
}
到此就可以完整的获取整个页面的内容了,但是这样获取原汁原味的内容,不一定能很好的展示在我们自己的页面上,下面是实际操作中遇到的两个问题。
解决过程中遇到的问题一
在展示抓取内容的过程中遇到图片跨域显示的或者防盗链的问题。刚开始的解决思路是通过Nginx反向代理的方式,来代理图片URL,但是因为源网页图片防盗链或者其他原因无法进行代理;
所以我用另外一种方式,通过将图片转换成Base64格式的内容替换原来的url来解决,这种方式确实可以解决这个问题但会引出新的问题,图片转换成Base64之后会将原网页内容变得非常大,在存储和展示过程中请求非常消耗存储空间和请求时间。
最后,我选择了将图片读取到本地,在通过Nginx代理的方式来处理。
//获取网页图片元素
List imgList = webDriver.findElements(By.tagName("img"));
// data:image/gif;base64,
for(WebElement img : imgList){
String imgSrc = img.getAttribute("src");
logger.info("img's src is : {}",imgSrc);
String pattern = "^((http)|(https)).*";
boolean imgSrcPattern = !StringUtils.isEmpty(imgSrc) && Pattern.matches(pattern, imgSrc);
if(imgSrcPattern){
// String strNetImageToBase64 = changImgUrlToBase64(imgSrc);
// driver_js.executeScript("arguments[0].setAttribute(arguments[1],arguments[2])", img, "src", "data:image/png;base64,"+strNetImageToBase64);
String imgUrlToImgFile = changImgUrlToImgFile(imgSrc);
//通过JS来替换页面图片
driver_js.executeScript("arguments[0].setAttribute(arguments[1],arguments[2])", img, "src", imgUrlToImgFile);
}
}
关键代码: driver_js.executeScript("arguments[0].setAttribute(arguments[1],arguments[2])", img, "src", imgUrlToImgFile);
将图片转换成Base64的代码
private String changImgUrlToBase64(String imgSrc){
String strNetImageToBase64 = "";
try {
URL imgURL = new URL(imgSrc);
final HttpURLConnection conn = (HttpURLConnection) imgURL.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(3000);
InputStream is = conn.getInputStream();
ByteArrayOutputStream data = new ByteArrayOutputStream();
// 将内容读取内存中
final byte[] by = new byte[1024];
int len = -1;
while ((len = is.read(by)) != -1) {
data.write(by, 0, len);
}
// 对字节数组Base64编码
BASE64Encoder encoder = new BASE64Encoder();
strNetImageToBase64 = encoder.encode(data.toByteArray());
// 关闭流
is.close();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return strNetImageToBase64;
}
将图片下载到本地的代码
private static String changImgUrlToImgFile(String imgSrc){
String suffix = ".png";
if(imgSrc.indexOf("gif") != -1){
suffix = ".gif";
}else if(imgSrc.indexOf("jpg") != -1){
suffix = ".jpg";
}else if(imgSrc.indexOf("jpeg") != -1){
suffix = ".jpeg";
}else if(imgSrc.indexOf("png") != -1){
suffix = ".png";
}
String dir = "E:/lingy/asmImg/";
String fileName = System.currentTimeMillis()+suffix;
try {
URL imgURL = new URL(imgSrc);
final HttpURLConnection conn = (HttpURLConnection) imgURL.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(3000);
InputStream is = conn.getInputStream();
File imgFile = new File(dir+fileName);
if(!imgFile.exists()){
imgFile.createNewFile();
}
// FileOutputStream fos = new FileOutputStream(imgFile);
// final byte[] by = new byte[1024];
// int len = -1;
// while ((len = is.read(by)) != -1) {
// fos.write(by, 0, len);
// }
// is.close();
// FileUtils.copyURLToFile(imgURL,imgFile);
FileUtils.copyInputStreamToFile(is,imgFile);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
String imagRootPath = "http://localhost/asmImg/";
return imagRootPath + fileName;
}
解决过程中遇到的问题二
在抓取页面过程中,由于需要模拟网页滚动浏览的效果整个抓取非常耗时(页面越长越耗时),在前端对响应时间有要求的情况下,建议大家讲抓取页面的过程做成异步的。
总结
Selenium更强大的用处是进行自动化测试,模拟浏览器行为,操作页面元素等;自动化测试领域更加专业,这里不做深入的探讨,只是简单的介绍一下如何动态抓取一个的过程。其中的关键是获取页面元素和执行JS脚本,主要使用webDriver.findElements()
、driver_js.executeScript()
两个方法。由于上边的介绍只是一个简单的尝试,其他场景中会遇到更复杂的情况,比如说有的页面需要点击一下才能展示全部内容,这就需要调用js去触发。
以上就是本次解决问题的一个回顾,希望能给大家带来一定的思路和帮助。