以前我在看 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作业来做测试。这样比较容易上手,不要上来就去爬取哪些反爬比较高级的网站。这里推荐去爬取百度图片,因为它比较容易,也很简单上手。
更多推荐
Java爬虫小练手
发布评论