持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
本篇主要写于Java URL类的学习以及框架Jsoup的应用。 开发环境:Eclipse+Java
分析
初始思路:
1) 连接:通过URL类对象的openConnection方法可以得到一个对象,通过setRequestProperty设置请求头中用户代理User-Agent的属性来模拟浏览器访问页面。
2) 获取流:通过 URLConnection的getInputStream可以获取对应页面的输入流。从而利用,BufferedReader以及FileWriter写入文件中。
3) 切割:通过确定绝对路径与相对路径的正则表达式,匹配所有流的绝对或者相对路径,从而实现其他资源的下载。
改进思路: 利用Java中的爬虫框架jsoup,通过其与HTML类似的选择器,从而实现对HTML页面内容的截取。能够更全面且便利地实现页面内容的获取。但是使用该框架需要引入其jar包。
代码结构
//建立连接,并获取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文件成功");
}
该方法的实现如下:
- html页面内的src属性等如果是根目录转义后的情况,需要将其替换掉。
- 为了实现文件的名字,手动去截取url最后的“/”后面的内容,如果默认没有后缀的话,还得手动给它加上。
- 最后输出的时候需要注意用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页面内元素爬取
本步骤主要内容:
- 为了便于下载对应的内容,并放置在正确的本机相对路径上,这里预先定义并使用了FileController类。
- 利用Jsoup里面的
getElementsByTag
方法以及absUrl
方法可以快速地选取到我们需要的内容的url。 - 为什么要额外爬取页面中的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);
}
实现效果
小结
- 递归调用时,对应文件放置的相对位置是一个值得思考的问题,上述的fileController是基于本人思考得出的,可能有更佳的实现方式。
- 这次实践仅对递归深度做了优化,实际上未从根本上解决两个页面引用对方url的问题,比如递归深度是6,那么两个页面就会各下载三次。可以通过一个hashMap存储已下载的url,也可以在fileController中判定,若该文件存在,则直接跳过。