在学习爬虫的时候,我是从python入门的。爬虫的原理也不难,获取服务器返回的html文件,然后通过正则表达式对html字符串进行解析,至于想要获得什么信息,则是看自己的业务逻辑,比如获取网站上所有邮箱,所有QQ号等。而我则是想爬取网站上所有图片。
一开始学习爬虫,想要获取网站上一些元素就需要自己写正则表达式筛选了,比如获取所有链接元素,(".*")。但是随着学习的深入,了解到python有Beautiful Soup这个库,提供了很多方便的方法来获取网站的元素,因本人对python了解不多,浅尝辄止,就不再妄论。
此时我就想,难道java不能获取html进行解析吗,就没有实用的工具类进行调用吗?毕竟java是做网站开发的利器啊。果然,java提供了jsoup工具来做爬虫开发,而且个人觉得很好用。
想要深入学习jsoup的同学可以去jsoup官网查看文档学习,使用非常简单,不难看懂。这里我先简单介绍一下。
jsoup是用于处理实际HTML的Java库。它提供了使用DOM,CSS和类似jquery的最好方法提取和处理数据的非常方便的API。
jsoup实现WHATWG HTML5规范,并将HTML解析为与现代浏览器相同的DOM。
<dependency>
<!-- jsoup HTML parser library @ https://jsoup.org/ -->
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.12.1</version>
</dependency>
// jsoup HTML parser library @ https://jsoup.org/
compile 'org.jsoup:jsoup:1.12.1'
当然你也可以通过自行下载jsoup-1.12.1.jar,并把它放入你的项目中。
Document是jsoup解析完html之后返回的一个对象,由Elements和TextNode组成,封装好了很多方法,可以通过它很方便地获取指定元素。获取Document对象也很简单。
Document doc = Jsoup.connect("http://example.com")
.data("query", "Java")
.userAgent("Mozilla")
.cookie("auth", "token")
.timeout(3000)
.post();//只需要connect()就行了,后面的方法指定http请求的一些属性,使用默认的亦可
String html = "First parse "
+ "Parsed HTML into a doc.
";
Document doc = Jsoup.parse(html);
File input = new File("/tmp/input.html");
Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/");
该parse(File in, String charsetName, String baseUri)方法加载并解析HTML文件。如果在加载文件时发生错误,它将抛出一个IOException,您应该适当地处理它。
baseUri解析器使用该参数在找到元素之前解析文档中的相对URL 。如果您不担心此问题,可以改为传递一个空字符串。
有一种姐妹方法parse(File in, String charsetName),该方法使用文件的位置作为baseUri。如果您在本地文件系统站点上工作,并且指向该站点的相对链接也在文件系统上,则此功能很有用。
获得了Document之后,我们可以通过它做一些更有趣的事情,比如获取特定元素。Element就是这些元素在java的特定实现,而Elements可以理解为Element的一个容器。获取元素方法也很简单,使用了jQuery和css的选择器,对前端熟悉的很容易理解这一点,不熟悉前端技术的也不用害怕,因为这并不难。
//获取id为content的元素
Element content = doc.getElementById("content");
//获取class为confirmButton的所有元素
Elements confirmButtons= doc.getElementsByClass("confirmButton");
//获取所有链接
Elements links = doc.getElementsByTag("a");
获取Element后,我们就可以使用Element封装的一些方法获取元素中的信息,比如链接<\a>的地址href。
Elements links = content.getElementsByTag("a");
for (Element link : links) {
String linkHref = link.attr("href");
String linkText = link.text();
}
这些是Element的常用方法:
熟悉了jsoup之后,我们就可以很方便的实现自己的爬虫工具了。
//这里我们没有通过jsoup的方法获取,而是使用HttpClient,效果是一样的
public String getHtml(String myURL) {
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String html="";
HttpGet request = new HttpGet(myURL);
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36");
try {
//3.执行get请求,相当于在输入地址栏后敲回车键
response = httpClient.execute(request);
//4.判断响应状态为200,进行处理
if(response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
//5.获取响应内容
HttpEntity httpEntity = response.getEntity();
html = EntityUtils.toString(httpEntity, "utf-8");
} else {
//如果返回状态不是200,比如404(页面不存在)等,根据情况做处理,这里略
System.out.println("返回状态不是200");
System.out.println(EntityUtils.toString(response.getEntity(), "utf-8"));
}
} catch (ParseException | IOException e) {
e.printStackTrace();
} finally {
//6.关闭
HttpClientUtils.closeQuietly(response);
HttpClientUtils.closeQuietly(httpClient);
}
return html;
}
public void getImages(String html,String myURL) {
Document document = Jsoup.parse(html);
//像js一样,通过标签获取title
//获取所有图片
Elements imgs=document.getElementsByTag("img");
for(Element img:imgs) {
String imgUrl = img.attr("src");
//无协议添加协议
if(imgUrl.startsWith("//")) {
imgUrl="http:"+imgUrl;
}
//相对地址转绝对地址,getHostName()是获得网站域名
else if(imgUrl.startsWith("/")){
imgUrl=getHostName(myURL)+imgUrl;
}
//如果网址为空或者下载过这张图片(HashMap中已经有URL)就跳过
if(imgUrl==null||imgUrl.equals("")||imgURLMap.containsKey(imgUrl)) {
continue;
}
System.out.println(imgUrl);
//通过url下载图片
if(imgUrl.startsWith("http")) {
downImagesByHttp(imgUrl);
}
//通过base64解码下载图片
else {
downImagesByBase64(imgUrl);
}
}
}
private String getHostName(String myURL) {
return myURL.substring(0,myURL.indexOf("/",8));
}
private void downImagesByHttp(String imgUrl){
imgURLMap.put(imgUrl, mapValue);
String fileName = imgUrl.substring(imgUrl.lastIndexOf("."));
HttpURLConnection connection=null;
InputStream is = null;
File file=null;
FileOutputStream out = null;
try {
URL url = new URL(imgUrl);
connection = (HttpURLConnection)url.openConnection();
if(connection.getContentLength()>imgSize) {
is = connection.getInputStream();
if(fileName.matches(".+?((png)|(jpg)|(jpeg)|(gif)|(svg))$")) {
file=new File(filePath+"zsy"+UUID.randomUUID().toString().substring(28)+fileName);
}
else {
file=File.createTempFile("zsy", ".png",new File(filePath));
}
out = new FileOutputStream(file);
int i = 0;
while((i = is.read()) != -1){
out.write(i);
}
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
try {
connection.disconnect();
out.close();
is.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//这里的Base64类来自Apache Commons Codec,可以通过Maven获取,用于解码Base64
/*
这个是它的gav
commons-codec
commons-codec
1.12
*/
private void downImagesByBase64(String imgUrl) {
// TODO Auto-generated method stub
String fileName="."+imgUrl.substring(imgUrl.indexOf('/')+1, imgUrl.indexOf(';'));
String fileBase64=imgUrl.substring(imgUrl.indexOf(',')+1);
File file=null;
FileOutputStream out = null;
try {
file=new File(filePath+"zsy"+UUID.randomUUID().toString().substring(28)+fileName);
out = new FileOutputStream(file);
byte[] b=Base64.decodeBase64(fileBase64);
out.write(b);
} catch (Exception e) {
e.printStackTrace();
}
finally {
try {
out.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
如果只是获得当前URL的图片,那程序就结束了。但是爬虫的厉害之处在于应当能够通过链接爬取其他网页的内容,所以应该获取当前页面的所有链接,但是注意不能重复,同样通过HashMap来存储已经访问过的URL。
public void getAllImages(String myURL) {
System.out.println(myURL);
String html=getHtml(myURL);
getImages(html,myURL);
URLMap.put(myURL,mapValue);
String hostName=getHostName(myURL);
LinkedList<String> curURLs=new LinkedList<>();
Document document = Jsoup.parse(html);
Elements links=document.getElementsByTag("a");
for(Element link:links) {
String nextLink=link.attr("href");
if(nextLink.startsWith("http")||nextLink.startsWith("/")) {
if(nextLink.startsWith("/")) {
nextLink=hostName+nextLink;
}
if(!URLMap.containsKey(nextLink)) {
curURLs.add(nextLink);
}
}
}
while(!curURLs.isEmpty()) {
String curURL=curURLs.pollFirst();
pool.submit(()->{
getAllImages(curURL);
});
}
}
通过调用getAllImage(URL),即可把图片全部下载下来,我们实现GUI界面更友好的接受数据(URL,文件存储位置)。
public void init() {
JFrame uiFrame=new JFrame("世缘科技");
JPanel panelURL=new JPanel();
JTextField urlField=new JTextField(40);
JButton start=new JButton("开始");
JButton stop=new JButton("结束");
panelURL.add(urlField);
panelURL.add(start);
panelURL.add(stop);
//urlField.setText("");
start.addActionListener(startEve->{
pool=Executors.newFixedThreadPool(threadNum);
pool.submit(()->{
if(!urlField.getText().startsWith("http")) {
urlField.setText("http://"+urlField.getText());
}
getAllImages(urlField.getText());
});
});
stop.addActionListener(stopEve->{
if(!pool.isShutdown()) {
pool.shutdownNow();
}
URLMap.clear();
imgURLMap.clear();
});
JPanel panelFilePath=new JPanel();
JTextField filePathField=new JTextField(44);
JButton choose=new JButton("选择文件");
panelFilePath.add(filePathField);
panelFilePath.add(choose);
filePathField.setText(filePath);
choose.addActionListener(chooseEve->{
JFileChooser fileChooser=new JFileChooser(".");
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int result=fileChooser.showDialog(uiFrame,"选择存储路径");
if(result==JFileChooser.APPROVE_OPTION){
filePath=fileChooser.getSelectedFile().getPath()+"\\";
filePathField.setText(filePath);
}
});
uiFrame.add(panelURL);
uiFrame.add(panelFilePath,BorderLayout.SOUTH);
uiFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
uiFrame.pack();
uiFrame.setLocation(500, 200);
uiFrame.setVisible(true);
}
可以从我的git仓库中获取源码https://github.com/noblegongzi/imgSpider.git
整体而言,爬虫并不是很难,但是需要处理的细节很多,因为每种元素属性会出现很多种情况,比如URL有//开头,有/开头,也有完整URL,需要分别处理,还要记录已经访问过的URL,防止重复访问。img的src也是如此,而且还有通过base64传输的图片,也需要另外处理。
《jsoup使用说明》–作者:jsoup开发人员
《Java爬虫系列三:使用Jsoup解析HTML》–作者:JAVA开发老菜鸟