Java爬虫小练手

以前我在看 URLConnection 类的时候,无意间发现居然可以下载百度图片(虽然一开始下载的图片是坏的,我忘记关闭流了。),然后我就对爬虫产生了兴趣。
JavaIO流的简单应用–网络爬虫

最近正好看了点关于Java爬虫的知识,主要看了 HttpClient 和 Jsoup的简单使用,爬虫框架的知识没怎么涉及,本身也是准备先熟悉 上面这两个工具,这样以后学习爬虫框架的知识就比较轻松了。

如果光看书,不知道自己学得怎么 样了?所以,就准备找一个网站来练练手(爬取图片),刚好上学期选修了python课程,和别人合伙完成了一个基于falsk的简单图床web应用,就那它来练手了。本来,课程结束了,它也就没有什么用了,现在就让它来发挥点最后的作用吧。我选择它也是有很多原因的,因为这个网站我也是参与涉及开发的,我对它比较熟悉,这样碰到了问题,也很容易解决。



爬虫练手之旅

这个练手的web项目是我的作业,所以这篇博客主要记录我自己的思路和爬虫爬取的经历,如果自己想要尝试的话,可以看我上面那篇博客(URLConnection 和 Java的IO流),推荐你使用我这里使用的工具来改写(HttpClient、Jsoup)。

web项目简单介绍

两个人业余时间做的一个小图床web网站,功能很简单。主要功能有:
注册、登录、上传图片、分享图片链接、回收站、图片添加备注、删除图片等。

首先简单介绍一下我爬取的项目,它是很简单的,爬取的难度较低。

登录页
在这里插入图片描述

查看图片页
在这里插入图片描述

爬取思路

因为这是一个很小的项目,所以就遵循简单的原则来爬取图片。首先需要登录系统,然后获取自己上传的所有图片。(这个你可以使用别人上传的图片链接,但是只能看到自己的图片,因为别人没有给你分享你是不知道其他人上传的图片的。)

这里有一个登录拦截器,如果没有登录的话,会被拦截,不过这不影响。
大致就使用了这几个url,剩下的是图片的链接了。这里也不需要使用什么队列了,就是简单的爬取就行了。
主页的url:http://127.0.0.1:5000/
登录页的url:http://127.0.0.1:5000/login?next=%2F
图片页的url:http://127.0.0.1:5000/mypictures

1.首先使用浏览器抓包分析
这里主要关注 Form Data 这一项。
在这里插入图片描述
登录的逻辑很简单,需要三项数据:csrf_token、username、password。
这里的 csrf_token 是隐藏的表单域,主要是为了防止跨站攻击的,它是一个随机的字符串。我们首先请求主页,然后被拦截到了登录页,从登录页的html页面数据中就可以获取到这一项的值了。一般情况下,如果只需要账号和密码的话,那就可以直接发送post登录请求到服务器了,但是一般情况下,都不止账号和密码两项,通常还会有其它的东西。例如爬虫的障碍–验证码。

在这里插入图片描述

使用Jsoup的选择器来获取 csrf_token 的值。

Element element = ele.select("input#csrf_token").first();

下面这一段代码主要是获取 Cookie 值和 csrf_token 的值,用于接下来的登录。

	/**
	 * 这里有一个隐藏域的问题,我必须先请求这个登录页得到这个隐藏域。
	 * */
	public Map<String, String> getIndex(String url) throws ClientProtocolException, IOException {
		HttpGet get = new HttpGet(url);
		Map<String, String> loginMap = new HashMap<>();
		try (CloseableHttpResponse response = httpClient.execute(get)) {
			if (response.getStatusLine().getStatusCode() == 200) {
				Header[] headers = response.getAllHeaders();
				String session = this.getCookie(headers);
				
				loginMap.put("Cookie", session);
				HttpEntity entity = response.getEntity();
				if (entity != null) {
					String html = EntityUtils.toString(entity, "Utf-8");
					Element ele = Jsoup.parse(html);
					Element element = ele.select("input#csrf_token").first();
					String csrf_token = element.attr("value");
					loginMap.put("csrf_token", csrf_token);
					return loginMap;
				}
			}
		}
		return null;
	}

2.登录解决之后,就可以着手爬取图片了。
所有的图片都可以通过 http://127.0.0.1:5000/mypictures 获取到,具体的图片可以看最上面的介绍。然后通过图片的外链来依次下载每一张图片。但是,这里有一个麻烦的地方,是因为加了一个简单的分页功能,每一页只能显示8张图片,所以爬虫程序还要处理一个分页的功能(当然了,估计也是最简单的分页功能了)。

这里是一张完整图片的html数据。
在这里插入图片描述

3.下载图片
下载图片的思路也很简单,主要就是首先请求图片的第一页(分页,默认就是第一页),得到html数据(浏览器也是获取html页面,不过会自动解析、渲染)。然后从html数据中得到需要的图片链接。对于分页来说,我是在每一页添加一个上一页、下一页标签(当然了,第一页没有上一页,最后一页没有下一页)。所以,就通过当前页是否有下一页这个标签来判断是否有还有下一页,如果有就下载下一页,如果没有说明已经下载到了最后一页,下载完后程序可以退出了。使用一个循环,每次下载一页的图片,循环终止的条件是没有当前页下一页了。



完整代码及运行结果

代码中添加了很多注释。

这里有几点说明:登录之后,通常是请求主页,因为我们使用浏览器它也是会将我们重定向到首页的,但是因为这里是爬虫,如果不处理重定向的话,就无法到首页。但是登录之后,是可以直接请求到查看图片页的,所以下面登录之后,我只是打印一下重定向的html页面,通常都是浏览器帮我们做了这一步,所以很少看到。

完整代码

package com.dragonfly;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

/**
 * 登录逻辑很简单,首先先模拟表单提交进行登录。
 * 然后把用户的所有图片下载下来,但是由于只能
 * 爬取用户自己上传的图片,所以意义不是很大,
 * 但是对于爬虫学习还是很有帮助的。
 * */
public class Spider {
	private String rootPath;  //用于拼接url使用
	private CloseableHttpClient httpClient;
	
	public Spider(String rootPath) {
		this.rootPath = rootPath;
		httpClient = HttpClients.createDefault();
	}
	
	
	/**
	 * 这里有一个隐藏域的问题,我必须先请求这个登录页得到这个隐藏域。
	 * */
	public Map<String, String> getIndex(String url) throws ClientProtocolException, IOException {
		HttpGet get = new HttpGet(url);
		Map<String, String> loginMap = new HashMap<>();
		try (CloseableHttpResponse response = httpClient.execute(get)) {
			if (response.getStatusLine().getStatusCode() == 200) {
				Header[] headers = response.getAllHeaders();
				String session = this.getCookie(headers);
				
				loginMap.put("Cookie", session);
				HttpEntity entity = response.getEntity();
				if (entity != null) {
					String html = EntityUtils.toString(entity, "Utf-8");
					Element ele = Jsoup.parse(html);
					Element element = ele.select("input#csrf_token").first();
					String csrf_token = element.attr("value");
					loginMap.put("csrf_token", csrf_token);
					return loginMap;
				}
			}
		}
		return null;
	}
	
	private String getCookie(Header[] headers) {
		String session = null;
		for (Header header : headers) {
			if (header.getName().compareTo("Set-Cookie") == 0) {
				String cookie = header.getValue();
				System.out.println(cookie);
				int begin = cookie.indexOf("=")+1;
				int end = cookie.indexOf(";");
				session = cookie.substring(begin, end);
				return session;
			}
		}
		return session;
	}
	
	
	/**
	 * 模拟表单登录:
	 * 利用浏览器抓包,简单分析表单提交的数据。
	 * @throws IOException 
	 * @throws ClientProtocolException 
	 * */
	public void login(String url, Map<String, String> loginMap) throws ClientProtocolException, IOException {
		if (loginMap == null) {
			throw new IOException("无法得到登录需要的信息!");
		}
		HttpPost post = new HttpPost(url);
		post.setHeader("Cookie", loginMap.get("Cookie"));
		
		//登录需要的参数
		List<BasicNameValuePair> loginInfo = new ArrayList<>();
		loginInfo.add(0, new BasicNameValuePair("csrf_token", loginMap.get("csrf_token")));
		loginInfo.add(1, new BasicNameValuePair("username", "Tom"));
		loginInfo.add(2, new BasicNameValuePair("password", "123"));
		//创建表单实体
		UrlEncodedFormEntity entity = new UrlEncodedFormEntity(loginInfo);
		//设置Post方式的实体
		post.setEntity(entity);
		//执行请求
		try (CloseableHttpResponse response = httpClient.execute(post)) {
			int statusCode = response.getStatusLine().getStatusCode();
			//请求成功,继续处理。
			if (statusCode == 302) {
				
				Header[] headers = response.getAllHeaders();
				String session = this.getCookie(headers);
				System.out.println(session);
				
				HttpEntity httpEntity = response.getEntity();
				if (httpEntity != null) {
					System.out.println("========================重定向===========================");
					System.out.println(EntityUtils.toString(httpEntity, "UTF-8"));
					System.out.println("================================================================");

				}
			} 
		}
	}
	
	/**
	 * Http://127.0.0.1:5000/mypictures
	 * 原始的URL,然后按照分页来下载图片。 
	 * 
	 * */
	public void download(String url) {
		String next_url = null;
		try {
			System.out.println("当前页前:" + url);
			next_url = this.downloadPicture(url);
			//如果还有下一页,那么就一直往下下载,
			//一直到下载完最后一页。
			while (next_url != null) {
				System.out.println("当前页前:" + next_url);
				next_url = this.downloadPicture(next_url);
				System.out.println("下一页:" + next_url);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	
	/**
	 * 登录之后,开始下载图片,但是由于当初这里做了一个分页的功能,
	 * 每页最多显示8张图片,所以爬全部的图片还需要分页来爬取。
	 * 
	 * @throws IOException 
	 * @throws ParseException 
	 * */
	private String downloadPicture(String url) throws ParseException, IOException {
		//执行请求后会重定向到主页	
		HttpGet getPcitures = new HttpGet(url);
		String next_url = null;
		try (CloseableHttpResponse response = httpClient.execute(getPcitures)) {
			int statusCode = response.getStatusLine().getStatusCode();
			//请求成功,继续处理。
			if (statusCode == 200) {
				HttpEntity httpEntity = response.getEntity();
				if (httpEntity != null) {
					String html = EntityUtils.toString(httpEntity, "UTF-8");
					//获取 Document 对象,根据是否有下一页的超链接来下载。
					Document doc = Jsoup.parse(html);
					Element page = doc.getElementsByAttributeValue("class", "next_url").first();
					if (page != null) { //说明当前页还有下一页,可以继续下载,否则就只下载当前页的图片。
						//获取下一页的超链接,注意获取的是相对路径,必须拼接才能得到完整的链接。
						next_url = rootPath+page.attr("href");
						System.out.println("下一页含有图片:" + next_url);
					} 
					//获取所有图片的div
					Elements elements = doc.select("div.picdiv");  
					System.out.println("当前页含有图片:" + elements.size() + " 张");
					//使用流遍历每一个 元素,获取图片链接,并下载图片
					this.downloadPicture(elements);
				}
			}
		}
		return next_url;
	}
	
	
	/**
	 * 使用函数式编程的流来处理这一块。
	 * */
	private void downloadPicture(Elements elements) {
		elements.stream().forEach(div->{
			String link = div.getElementsByAttributeValue("target", "_blank").attr("href");  //class="pictures" 使用这个也可以匹配
			String spanText = div.getElementsByAttributeValue("class", "link").first().text();
			String comment = spanText.split("\\s+")[1];   //图片的备注  这里把转义字符页转义了
			//下载图片,命名格式为:UUID+备注+后缀名。
			String filename = UUID.randomUUID().toString() + "_" + comment + link.substring(link.lastIndexOf("."));
			System.out.println(filename);
			HttpGet getPicture = new HttpGet(link);
			
			try (CloseableHttpResponse resposne = httpClient.execute(getPicture)) {
				HttpEntity entityPic = resposne.getEntity();
				
				//下载后要关闭流(强制刷新),否则图片可能会损坏,这与缓冲流的特点有关。
				try (OutputStream out = new BufferedOutputStream(new FileOutputStream("./src/images/"+filename))) {
					entityPic.writeTo(out);
				}

				EntityUtils.consume(entityPic);  //关闭资源。
			} catch (IOException e) {
				e.printStackTrace();
			}
		});
	}
}

测试代码

package com.dragonfly;

import java.io.IOException;

import org.apache.http.client.ClientProtocolException;

/**
 * 爬取和同学一起合作的期末大作业。
 * 一个简单的基于flask的小图床。
 * 
 * */
public class Main {
	public static void main(String[] args) throws ClientProtocolException, IOException {
		String rootUrl = "http://127.0.0.1:5000"; //这里如果带上 斜杠的话,后面拼接就会拼接成两个斜杠,处理很麻烦。
		String loginUrl = "http://127.0.0.1:5000/login?next=%2F";
		String downloadUrl = "Http://127.0.0.1:5000/mypictures";
		Spider spider = new Spider(rootUrl);
		spider.login(loginUrl, spider.getIndex(rootUrl));
		spider.download(downloadUrl);
	}
}

运行结果

总共22张测试图片,第一页8张,第二页8张,第三页6张。
在这里插入图片描述

图片全部是完好的,没有损坏。
在这里插入图片描述

关于下载图片的代码

HttpGet getPicture = new HttpGet(link);
try (CloseableHttpResponse resposne = httpClient.execute(getPicture)) {
	HttpEntity entityPic = resposne.getEntity();
	
	//下载后要关闭流(强制刷新),否则图片可能会损坏,这与缓冲流的特点有关。
	try (OutputStream out = new BufferedOutputStream(new FileOutputStream("./src/images/"+filename))) {
		entityPic.writeTo(out);
	}

	EntityUtils.consume(entityPic);  //关闭资源。
} catch (IOException e) {
	e.printStackTrace();
}

一开始我没有手动关闭输出流,导致图片下载失败(图片显示不完整,出现错误。),如下图所示。因为我一开始对这个 writeTo 方法不熟悉,我以为它会帮我关闭流呢。后来才发现,不过这确实是自己的疏忽了。这里我使用自动关闭资源的 try-with-resource 语句来关闭资源,这样写代码显得比较简洁。
下载失败的图片

总结

爬虫程序主要就是网络数据采集和分析(那张邪恶的用来攻击别人的爬虫,还是不要涉及为好)。从这个博客可以看出来,主要就是发起请求、解析数据两种方式,大致上就是这样操作,当然了我这个是很简单的尝试了。
这个爬虫程序难度很适合,推荐大家如果学习爬虫的话,可以尝试用自己的web作业来做测试。这样比较容易上手,不要上来就去爬取哪些反爬比较高级的网站。这里推荐去爬取百度图片,因为它比较容易,也很简单上手。

发布了27 篇原创文章 · 获赞 43 · 访问量 3612

猜你喜欢

转载自blog.csdn.net/qq_40734247/article/details/104453425