Java URL类及Jsoup框架使用实践

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

本篇主要写于Java URL类的学习以及框架Jsoup的应用。 开发环境:Eclipse+Java

分析

初始思路:

1)   连接:通过URL类对象的openConnection方法可以得到一个对象,通过setRequestProperty设置请求头中用户代理User-Agent的属性来模拟浏览器访问页面。

2)   获取流:通过 URLConnection的getInputStream可以获取对应页面的输入流。从而利用,BufferedReader以及FileWriter写入文件中。

3)   切割:通过确定绝对路径与相对路径的正则表达式,匹配所有流的绝对或者相对路径,从而实现其他资源的下载。

改进思路: 利用Java中的爬虫框架jsoup,通过其与HTML类似的选择器,从而实现对HTML页面内容的截取。能够更全面且便利地实现页面内容的获取。但是使用该框架需要引入其jar包。

56.png

代码结构

    //建立连接,并获取doc对象
    doc = Jsoup.connect("www.xx.com");
    
    //利用doc存储html文件内容
    saveHtml(doc.toString());
    
    //爬取doc内自己需要的元素,比如img
    eles = doc.getElementsByTag("img");
    //遍历eles 拿取对应的src属性,转化成绝对路径,得到一个url
    url = eles.absUrl("src"); 
    
    //根据URL下载对应的文件
    download(url);
    
    //扩展:为了获取与该页面相关的其他页面内容,可以爬取a标签下的href,递归实现爬取
    repeat(xxx);

实际过程

建立连接

通过url来连接对应的网页,可以设置请求的参数,需要注意使用userAgent设置模拟浏览器访问,否则会被不少网站拦截。

Document doc =Jsoup.connect(baseUrl)//目标网址
.data("query", "Java") //请求参数
.userAgent("Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)") //设置User-Agent
.timeout(5000) //设置连接超时时间
.post();//使用POST方法访问URL

html文件存储

我们获取到了doc对象后,通过toString就可以获取到html页面的全部代码,此时我们可以通过输入输出流的方式写入目标的路径中,这里我封装了一个方法:其中basePath表示本机文件夹,baseUrl可用作二级文件夹的名字,doc.toString表示html的内容。

if(saveAsUrlFile(baseUrl,doc.toString(),basePath)) {
	System.out.println("存储html文件成功");
}

该方法的实现如下:

  1. html页面内的src属性等如果是根目录转义后的情况,需要将其替换掉。
  2. 为了实现文件的名字,手动去截取url最后的“/”后面的内容,如果默认没有后缀的话,还得手动给它加上。
  3. 最后输出的时候需要注意用UTF-8的形式写入,否则读取的html内容会乱码的。
public static boolean saveAsUrlFile(String url , String html,String location ) {
	//替换所有以/开头的属性
	html = html.replaceAll("(src=\\\"/){1}","src=\"");
	html = html.replaceAll("(poster=\\\"/){1}","src=\"");
    try {
        //写入的文件,截取要存储的文件夹位置
        String filePath = url.substring(url.indexOf("/")+2,url.lastIndexOf("/"));
    	String filename = url.substring(url.lastIndexOf("/")+1);
        String type = url.substring(url.lastIndexOf(".")+1);
        //若识别到 正常的文件类型,则不管,否则后缀加上html
//            String[] allTypes = {"css","js","html","htm","xhtml","jpg","png","jpeg","ico","gif","webp","mp4","mp3","opp","pdf","pptx","ppt","dtd"};
        String[] allTypes = {"html","htm","xhtml"};

        boolean isRightType  = false;
        for(int i=0;i<allTypes.length;i++) {
        	if(type.equals(allTypes[i])) {
        		isRightType = true;
        	}
        }

        File dir = new File(location+filePath);
        if(!dir.exists()){
            dir.mkdirs();//创建目录
        }
        
        File file;
        if(!filename.equals("") && isRightType) {
        	file = new File(location + filePath , filename);	
        } else {
        	file = new File(location + filePath , filename    +"index.html");             
        }
        if(file.createNewFile()) {
                System.out.println("Q"+file.getAbsolutePath());
        }else {
                System.out.println("W"+file.getAbsolutePath());
        }
        
        //转成UTF-8格式输出
        FileOutputStream outputStream = new FileOutputStream(file);
        OutputStreamWriter outputWriter = new OutputStreamWriter(outputStream, "utf-8");         
        BufferedWriter writer  = new BufferedWriter(outputWriter);
        
        writer.write(html);
        writer.flush();
        writer.close();

    } catch(Exception e) {
    	System.out.println(e.toString());
		return false;        	
    }
	return true;
}

html页面内元素爬取

本步骤主要内容:

  1. 为了便于下载对应的内容,并放置在正确的本机相对路径上,这里预先定义并使用了FileController类。
  2. 利用Jsoup里面的getElementsByTag方法以及absUrl方法可以快速地选取到我们需要的内容的url。
  3. 为什么要额外爬取页面中的css和script标签?因为有页面的样式css资源以及脚本script资源可以使得我们在本地打开对应的html文件可以拥有实际网站的效果。
MyFileController fileController = new MyFileController("E://JavaOutput/","");
Element content = doc.body();//可以拿到body标签

//爬取页面中所有的link标签引入文件(css文件)
Elements referenceArr = doc.getElementsByTag("link");
for(Element reference : referenceArr) {
	String referenceUrl = reference.absUrl("href");
	if(referenceUrl.equals("") || referenceUrl.charAt(referenceUrl.length()-1)=='/') continue;
	fileController.setUrl(referenceUrl);
	fileController.addFile();
}

//爬取页面中所有的script标签的引入文件(script文件)
Elements scripts = doc.getElementsByTag("script");
for(Element script : scripts) {
	String scriptUrl = script.absUrl("src");
	if(scriptUrl.equals("") || scriptUrl.charAt(scriptUrl.length()-1)=='/') continue;
	fileController.setUrl(scriptUrl);
	fileController.addFile();
}

//选取页面中所有的img标签
Elements imgs = content.getElementsByTag("img");
for(Element img : imgs) {
	String imgUrl = img.absUrl("src");
	if(imgUrl.equals("") ||imgUrl.charAt(imgUrl.length()-1)=='/') continue;
	fileController.setUrl(imgUrl);
	fileController.addFile();
}

//选取页面中所有的video标签
Elements videos = content.getElementsByTag("video");
for(Element video : videos) {
	String videoUrl = video.absUrl("src");
	if(videoUrl.equals("") || videoUrl.charAt(videoUrl.length()-1)=='/') continue;
	fileController.setUrl(videoUrl);
	fileController.addFile();
}

这里的fileController主要是根据url,实现下载对应的url到本机文件上。

package com.hyy.socket1;

import java.io.File;
/*
 * 通过传入的文件基准位置以及绝对URL路径,拼接成对应的文件夹位置 
 * 依赖DownloadFile.DownloadUrlIntoFile 下载对应的文件
 * 
 * absUrl:https://www.apple.com.cn/images/overview/smart.jpg
 */
public class MyFileController {
	private String baseLocation;
	private String absUrl;
	public MyFileController(String baseLocation,String absUrl) {
		this.baseLocation = baseLocation ;
		this.absUrl = absUrl;
	}
	public void setUrl(String url) {
		this.absUrl = url;
	}
	/**
     * 通用文件添加操作
     * @param
     * @return canbeAdd
     */
    public Boolean addFile(){
        //1. 剔除绝对路径前的http/https协议 http://xxx
    	int index = this.absUrl.indexOf("/");
    	if(index == -1) {
    		System.out.println(this.absUrl + " is not an absolute url ");
    		return false;
    	}
    	
    	//2. 取最后一个/ 分隔路径以及文件名    	
    	int fileIndex = this.absUrl.lastIndexOf("/"); 
    	String location = this.baseLocation +  this.absUrl.substring(index+1,fileIndex);
    	String fileName = this.absUrl.substring(fileIndex+1);
    	//如果绝对路径末尾为 / ,则表示该链接指向该目录下的index.html
    	if(fileName.equals("")) {
    		fileName = "index.html";
    		//后续修改:将html文件单独从JsoupController中下载,设置编码,此处直接结束
    		//return false;
    	}
    	//3. 尝试创建该文件
    	try {
            File dir = new File(location);
            if(!dir.exists()){
                dir.mkdirs();//创建目录
            }
            File f = new File(location,fileName);
            //如果文件已存在,不重复下载
//            if(f.exists()) {
//            	return false;
//            }
            DownloadFile.DownloadUrlIntoFile(this.absUrl, f);//将目的资源下载到文件中
 
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
}

递归实现爬取其他相关页面

如果我们需要进行递归的爬取其他页面,需要注意的一点是我们的递归深度。如果两个页面不断引用对方的页面,那么就会进入死循环。

为此,我们需要设置一个递归深度限制:

public static int DepthLimit = 5;//递归上限	

在我们上述的连接(Jsoup.connect)开端,将其封装为一个函数,格式如下:

//下载 初始的URL + 存储的磁盘位置 + 递归深度
public static void downloadUrl(String baseUrl,String basePath , int depth) {//设置deep深度,防止递归深度过大
	//检测递归深度
	if(depth > DepthLimit) {
		return ;
	}
	depth++;
        try{
            //连接url
            //存储html
            //爬取元素
            //递归调用,爬取其他页面(待实现)
        }catch (IOException e) {
            e.printStackTrace();
        } 

当然,如何获取其他页面的url呢?只需爬取对应的a标签下的href即可实现。

//选取页面中所有的超链接
Elements links = content.getElementsByTag("a");
System.out.println("a number : "+ links.size() );
for (Element link : links) {
  String linkUrl = link.absUrl("href");
  if(linkUrl.equals("")) continue;
  System.out.println("getLinkUrl : " + linkUrl);
  //递归调用,其中JsoupCrawler是自己取的类名
  JsoupCrawler.downloadUrl(linkUrl, basePath, depth);			  
}

实现效果

123.png

小结

  1. 递归调用时,对应文件放置的相对位置是一个值得思考的问题,上述的fileController是基于本人思考得出的,可能有更佳的实现方式。
  2. 这次实践仅对递归深度做了优化,实际上未从根本上解决两个页面引用对方url的问题,比如递归深度是6,那么两个页面就会各下载三次。可以通过一个hashMap存储已下载的url,也可以在fileController中判定,若该文件存在,则直接跳过。

猜你喜欢

转载自juejin.im/post/7103427495306723358