前言
今天早上在睡懒觉的时候在考虑了一个问题,我可以使用socket下载网络资源吗? 仔细考虑过之后发现这是一个很有意思的问题,它要运用到的知识都是我已经学习过了的或者是掌握了的。所以,简单思考之后就开始了工作,但是还是有许多考虑不周的地方,发现自己对于问题的理解还是过于简单化了,中间踩了不少坑!其实爬虫就是针对网络数据的采集和整理工作,我这里把它称为Socket爬虫应该也是合理的,但是这里使用Socket下载网络资源反而是次要问题,主要的问题是如何解决那些细节性的问题,从中你也可以看出网络分层的好处。 例如,发起 HTTP 请求一张图片,然后服务器响应一张图片。站在应用层(HTTP)的角度,响应是一张图片。但是如果从传输层的角度来看,响应是一个字节点,里面包含了响应的头部信息和响应的数据部分(即所请求的图片),如何从这个数据流中取出来图片也是令人头疼的问题!—— 因为它涉及到了对报文的解析,这里的解析是真的对报文进行解析,而不是网上大部分博客所谓的解析,他们大多是简单介绍一下报文的格式,更进一步是补充一两个真实的报文,但是基本上没有自己去解析报文中——从真实的报文中取出来图片。
阅读需要的知识(对以下内容系统性的学过即可):
JavaSE基础
计算机网络
Socket爬虫
使用socket爬虫,首先它要发送HTTP请求,其次是要解决如何接收响应。这里我们直接上代码,再来具体分析每部分的功能。
Socket发送HTTP请求,获取网络图片demo爬虫
代码
package com.dragon;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* @author Alfred
* @version 1.0
* */
public class SocketDownload {
private static final int TIMEOUT = 10*1000; // 超时时间
private static final String BLANK = " "; // 空格
private static final String CRLF = "\r\n"; // 回车换行符
/**
* 程序执行的入口
* */
public static void main(String[] args) {
SocketDownload socketDownload = new SocketDownload();
socketDownload.httpDownload("pic1.win4000.com", 80, ".jpg");
}
/**
* 下载方法
* @param ip 请求连接的ip地址
* @param port 端口号
* @param suffix 下载资源的后缀名
*
*
* 使用网络图片来进行测试,测试图片地址为:
* http://pic1.win4000.com/pic/d/d0/3b9aa42d07.jpg
* */
public void httpDownload(String ip, int port, String suffix){
try (Socket socket = new Socket(ip, port)) {
socket.setSoTimeout(TIMEOUT); // 设置超时时间
// 获取输出流和输入流
OutputStream output = new BufferedOutputStream(socket.getOutputStream());
InputStream input = new BufferedInputStream(socket.getInputStream());
// 使用输出流发送请求数据
StringBuilder msgBuilder = new StringBuilder();
// 构造简单的请求报文,这里报文非常简单
msgBuilder.append("GET").append(BLANK)
.append("/pic/d/d0/3b9aa42d07.jpg").append(BLANK).append("HTTP/1.1").append(CRLF)
.append("Host").append(": ").append(BLANK).append("pic1.win4000.com").append(CRLF)
.append("User-Agent").append(": ").append("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36").append(CRLF)
.append(CRLF);
// 请求报文转成字符串
String msg = msgBuilder.toString();
// 请求报文字符串转成字节数组,网络上的数据最终都是以字节数据发送的,
// 我们这是在TCP的层面看的,如果你往下一层,也可以说是比特流了。
byte[] request = msg.getBytes(Charset.forName("UTF-8"));
// 发送请求报文,这里其实可以一步到位的,但是为了表达清晰,还是分步来写
output.write(request);
output.flush(); // 刷新输出流,不然未发送请求,导致无法接收到响应
// 使用输入流接收请求数据,但是此处我无法直接读取请求流,不然就是将整个报文的内容读取下来了
// 注意整个报文指的是整个响应报文,包含报文头和报文体。虽然我也可以通过打开整个报文,
// 删除报文头,然后修改文件名,它就是一个图片了,但是这样感觉不是很规范。
char ch;
Map<String, String> headMap = new HashMap<>(); // 定义一个map结构,用于存储报文头部
StringBuilder statusLine = new StringBuilder(); // 响应报文的首行
StringBuilder key = new StringBuilder(); // 存储键
StringBuilder value = new StringBuilder(); // 存储键
boolean flag = true; // 定义一个标志标量,用于读取首行
/**
* 读取不到数据,都是 -1,但是没有退出循环,将 -1 转成 char,导致了变成 65535 了!
*
* */
// 读取响应报文头部数据
dragon: while (true) {
// 响应报文的首行需要单独处理
if (flag) {
while (true) {
ch = (char) input.read();
if (ch == '\n') {
flag = false; // 标志位置为假
break;
}
statusLine.append(ch);
}
}
// 读取一个 key
while (true) {
ch = (char) input.read();
if (ch == ':') {
break;
}
key.append(ch);
// 如果 ch 是\n字符,那么就认为读取到了头部和数据部分的分隔符 \r\n,
// 即头部已经读取完毕了,直接退出循环即可。
if (ch == '\n') {
break dragon;
}
}
// 读取一个键
while (true) {
ch = (char) input.read();
if (ch == '\n') {
break;
}
value.append(ch);
}
// 存储读取的key和value,并去除左右空格
headMap.put(key.toString().trim(), value.toString().trim());
// 清空 key 和 value,为下一次读取做准备。
key.delete(0, key.length());
value.delete(0, value.length());
}
String fileName = UUID.randomUUID().toString() + suffix;
String content_Length = headMap.get("Content-Length");
System.out.println("网络资源大小:" + content_Length);
int length = Integer.parseInt(content_Length);
// 读取响应报文数据部,即图片本身的二进制数据
try (OutputStream outputFile = new BufferedOutputStream(new FileOutputStream(new File("D:/DBC/socketPic", fileName)))) {
int hasRead = 0;
int len = 0;
byte[] b = new byte[1024];
while ((hasRead != length) && (len = input.read(b)) != -1) { // 这个如果是 || 非的话,永远也结束不了了!哈哈
outputFile.write(b, 0, len);
hasRead += len;
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("网络资源已经下载完成了!");
}
}
测试连接
http://pic1.win4000.com/pic/d/d0/3b9aa42d07.jpg
测试结果
控制台输出:
磁盘文件:
具体查看文件:
文件大小信息:
注意和上面的控制台进行比对大小,如果字节数少了一个,都说明这个程序是失败的!
程序运行条件
这里这个程序专门针对 HTTP 的网站编写的,如果是 HTTPS 的话,程序需要进行改动。但是我一开始也没有注意到,因此踩了一个坑,不过也接触到了一个新的知识。不过也不算是新知识吧,虽然以前听过,但是没有尝试过。
说明
上面代码已经是证明可以正常工作了的,我对这个要求是很严格的。因为感觉如果写的博客里面东西是错误的,就是一种误导了——虽然看得人也不多,即使是错误也传不远,哈哈!
创建 Socket 对象
首先我们来想一想,怎么请求这张图片?它的 url 如下:
http://pic1.win4000.com/pic/d/d0/3b9aa42d07.jpg
对于Socket来说,重要的只有三个点:protocol、host、port。即协议、主机、端口。
从这条url中可以即可以得到它们三个的信息了:
protocol 协议:http
host 主机:pic1.win4000.com
port 端口:80
那么,根据这些信息即可构造Socket对象了,它本身是很简单的。
这里的 ip 即是上面的 host,然后你可能会发现缺少了协议。你这个Socket使用的是什么协议呢?
因为 HTTP 是明文传输的,所以它就是使用了普通的 Socket,所以协议就隐含在你使用的 Socket 上面了。
Socket socket = new Socket(ip, port)
注意:不要出现这样的代码:Socket socket = new Socket(“http://pic1.win4000.com/”, 80);
这说明,你对计算机网络中的应用层和传输层不是很了解,需要复习或者补充知识了。
构造请求报文
然后对于HTTP来说,还需要 Request Method、Request Path、Request Parameters等,即请求方法、请求路径、请求参数。从上面的url可以得到如下信息:
Request Method 请求方法:GET
Request Path 请求路径:/pic/d/d0/
Request Parameter 请求参数:3b9aa42d07.jpg
我们都知道,HTTP 请求报文是有一个具体的结构的,它的结构如下:
因为 get 请求默认是没有实体部分的(默认不代表不能,具体可以参考我的博客get请求方式可以带方法体吗?),所以这里我们需要的报文形式其实很简单,但是还是需要一些必要信息的。我发现如果不带 Host 的话或者 Host错误的话,会直接被拒绝,导致 400 bad request。
所以,为了保险起见,我还是使用了两个头部:Host、User-Agent 即主机和用户代理。
StringBuilder msgBuilder = new StringBuilder();
// 构造简单的请求报文,这里报文非常简单
msgBuilder.append("GET").append(BLANK)
.append("/pic/d/d0/3b9aa42d07.jpg").append(BLANK).append("HTTP/1.1").append(CRLF)
.append("Host").append(": ").append(BLANK).append("pic1.win4000.com").append(CRLF)
.append("User-Agent").append(": ").append("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36").append(CRLF)
.append(CRLF);
// 请求报文转成字符串
String msg = msgBuilder.toString();
发送请求报文
发送请求报文,对于Socket来说,就是通过输出流把它写进去,这里是很简单的,也是很奇妙的,可以自己体会一下。 然后注意,需要进行flush操作,否则请求其实是没有发送的。这里主要和我使用的缓冲流的特性有关,它不会立刻发送,而是先缓存一下,等到积累到一定程度,再一次性发送数据。
默认的缓存大小是 8096 字节,所以我这里的报文是根本发不出去的,必须强制刷新一下!
// 请求报文字符串转成字节数组,网络上的数据最终都是以字节数据发送的,
// 我们这是在TCP的层面看的,如果你往下一层,也可以说是比特流了。
byte[] request = msg.getBytes(Charset.forName("UTF-8"));
// 发送请求报文,这里其实可以一步到位的,但是为了表达清晰,还是分步来写
output.write(request);
output.flush(); // 刷新输出流,不然未发送请求,导致无法接收到响应
接收响应报文并解析
这是这里最难的一个部分了,我以前也写过关于Socket和Http的代码,但是最难以处理的,就是对报文格式的解析——我只是知道它会进行解析,但是真的遇到的时候,还是感觉具有挑战性的。这里我想到一个方法 —— 使用了4个while循环来解决了整个问题,至少保证了在此处是可以正常使用的。
读取响应头部
这是我今天写的最后的代码了,尽管它可能还是有一些问题,但至少在这里是可以使用的,而且我觉得处理得很优雅!哈哈
以下是我的主要思路,可以参考上那张图片来理解:
首先使用一个大 while 循环来读取整个头部信息,一直读取到最后一个 \r\n 的 \n 为止,不能多也不能少!
1.因为报文的第一行是首部,它的结构比较特殊,所以优先处理它,使用一个小的 while 循环,然后是定义一个标志变量flag,如果flag 为true,就先读取第一行,然后置flag 为false,之后的循环不再使用此处代码。
2. 然后是请求的头部了 ,头部的结构都是一样的——典型的键值对结构,格式如下:
key: value\r\n
这里我设计了一个感觉很巧妙的代码结构,使用两个 while 循环来依次读取它们,首先是读取 key,使用一个 StringBuilder 对象来存储它,然后是读取 value,使用相同的方法存储它。因为除了首行和最后一行 \r\n,就是头部了。这样处理起来,我感觉很优雅。依次执行两个 while 循环后,一个完整的首部就读取出来了,然后就可以存储它们了:headMap.put(key.toString().trim(), value.toString().trim());
,这里注意去除左右空格。然后就清空存储 key 和 value 的 StringBuilder 对象,再次执行代码即可,这样每一行首部都会按照同样的方式读取并存储起来。
3.以上可以很好的解决问题了,但是现在需要考虑什么时候结束整个循环了——即整个大循环的终止条件。这里需要考虑到报文头部的结束符号是以CRLF——\r\n。所以当所有的首部读取完毕之后,又会开始 key 的读取,这样就会读取到 \r\n,所以可以在读取 key 的时候,如果遇到 \n,就认为已经读取到了报文头部的最后一个字符。 但是这里需要跳出整个循环,即跳出两层循环。这里我使用我刚学过的 Label Grammar 标签语法来跳出整个循环。它的使用方法很简单,简单了解一下即可,这里不再继续介绍了。
// 使用输入流接收请求数据,但是此处我无法直接读取请求流,不然就是将整个报文的内容读取下来了
// 注意整个报文指的是整个响应报文,包含报文头和报文体。虽然我也可以通过打开整个报文,
// 删除报文头,然后修改文件名,它就是一个图片了,但是这样感觉不是很规范。
char ch;
Map<String, String> headMap = new HashMap<>(); // 定义一个map结构,用于存储报文头部
StringBuilder statusLine = new StringBuilder(); // 响应报文的首行
StringBuilder key = new StringBuilder(); // 存储键
StringBuilder value = new StringBuilder(); // 存储键
boolean flag = true; // 定义一个标志标量,用于读取首行
/**
* 读取不到数据,都是 -1,但是没有退出循环,将 -1 转成 char,导致了变成 65535 了!
*
* */
// 读取响应报文头部数据
dragon: while (true) {
// 响应报文的首行需要单独处理
if (flag) {
while (true) {
ch = (char) input.read();
if (ch == '\n') {
flag = false; // 标志位置为假
break;
}
statusLine.append(ch);
}
}
// 读取一个 key
while (true) {
ch = (char) input.read();
if (ch == ':') {
break;
}
key.append(ch);
// 如果 ch 是\n字符,那么就认为读取到了头部和数据部分的分隔符 \r\n,
// 即头部已经读取完毕了,直接退出循环即可。
if (ch == '\n') {
break dragon;
}
}
// 读取一个键
while (true) {
ch = (char) input.read();
if (ch == '\n') {
break;
}
value.append(ch);
}
// 存储读取的key和value,并去除左右空格
headMap.put(key.toString().trim(), value.toString().trim());
// 清空 key 和 value,为下一次读取做准备。
key.delete(0, key.length());
value.delete(0, value.length());
}
Socket发送HTTPS请求,获取网络图片demo爬虫
说完了http的部分,接下来补充一个https的内容。因为现在https的网站使用已经非常广泛了。实际上我一开始使用的图片链接就是https的,但是我仍然使用的发送 http 请求的Socket,导致我卡了很久。后来才发现,我不应该使用 Socket,而是使用 SSLSocket。我虽然以前听过它,但是却从来没有使用过。不过,即使是换成它,上面的主要代码也是不需要改动的。只是把Socket这部分换掉即可了。
public void httpsDownload(String ip, int port, String suffix){
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();// 利用工厂来创建SSLSocket安全套接字
try (SSLSocket socket = (SSLSocket) factory.createSocket(ip, port)) {
socket.setSoTimeout(TIMEOUT); // 设置超时时间
// 启用所有密码组
String[] supported = socket.getSupportedCipherSuites();
socket.setEnabledCipherSuites(supported);
// 获取输出流和输入流
OutputStream output = new BufferedOutputStream(socket.getOutputStream());
InputStream input = new BufferedInputStream(socket.getInputStream());
// 使用输出流发送请求数据
StringBuilder msgBuilder = new StringBuilder();
// 构造简单的请求报文,这里报文非常简单,甚至没有考虑到反爬虫的措施!
msgBuilder.append("GET").append(BLANK)
.append("/edpic_source/17/4e/1d/174e1db844a7eac328fa4695edcb8158.jpg").append(BLANK).append("HTTP/1.1").append(CRLF)
.append("Upgrade-Insecure-Requests").append(": ").append("1").append(CRLF)
.append("Host").append(": ").append("up.enterdesk.com").append(CRLF)
.append("User-Agent").append(": ").append("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
+ " AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36").append(CRLF)
.append(CRLF);
// 请求报文转成字符串
String msg = msgBuilder.toString();
// 请求报文字符串转成字节数组,网络上的数据最终都是以字节数据发送的,
// 我们这是在TCP的层面看的,如果你往下一层,也可以说是比特流了。
byte[] request = msg.getBytes(Charset.forName("UTF-8"));
// 发送请求报文,这里其实可以一步到位的,但是为了表达清晰,还是分步来写
output.write(request);
output.flush(); // 刷新输出流,不然未发送请求,导致无法接收到响应
// 使用输入流接收请求数据,但是此处我无法直接读取请求流,不然就是将整个报文的内容读取下来了
// 注意整个报文指的是整个响应报文,包含报文头和报文体。虽然我也可以通过打开整个报文,
// 删除报文头,然后修改文件名,它就是一个图片了,但是这样感觉不是很规范。
char ch;
Map<String, String> headMap = new HashMap<>(); // 定义一个map结构,用于存储报文头部
StringBuilder statusLine = new StringBuilder(); // 响应报文的首行
StringBuilder key = new StringBuilder(); // 存储键
StringBuilder value = new StringBuilder(); // 存储键
boolean flag = true; // 定义一个标志标量,用于读取首行
// 读取响应报文头部数据
dragon: while (true) {
// 响应报文的首行需要单独处理
if (flag) {
while (true) {
ch = (char) input.read();
if (ch == '\n') {
flag = false; // 标志位置为假
break;
}
statusLine.append(ch);
}
}
// 读取一个 key
while (true) {
ch = (char) input.read();
if (ch == ':') {
break;
}
key.append(ch);
// 如果 ch 是\n字符,那么就认为读取到了头部和数据部分的分隔符 \r\n,
// 即头部已经读取完毕了,直接退出循环即可。
if (ch == '\n') {
break dragon;
}
}
// 读取一个键
while (true) {
ch = (char) input.read();
if (ch == '\n') {
break;
}
value.append(ch);
}
// 存储读取的key和value,并去除左右空格
headMap.put(key.toString().trim(), value.toString().trim());
// 清空 key 和 value,为下一次读取做准备。
key.delete(0, key.length());
value.delete(0, value.length());
}
String fileName = UUID.randomUUID().toString() + suffix;
String content_Length = headMap.get("Content-Length");
System.out.println("网络资源大小:" + content_Length);
int length = Integer.parseInt(content_Length);
// 读取响应报文数据部,即图片本身的二进制数据
try (OutputStream outputFile = new BufferedOutputStream(new FileOutputStream(new File("D:/DBC/socketPic", fileName)))) {
int hasRead = 0;
int len = 0;
byte[] b = new byte[1024];
// 先判断相等,在判断读取,因为读取会导致阻塞!
while ((hasRead != length) && (len = input.read(b)) != -1) { // 这个如果是 || 非的话,永远也结束不了了!哈哈
outputFile.write(b, 0, len);
hasRead += len;
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("下载完成了!");
}
主要改动地方
但是对于启用所有密码组这个部分,我也不是很清楚。我对于SSL 安全套接字层不是很了解。这里就先知道如何使用即可,以后有时间再去了解吧。
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();// 利用工厂来创建SSLSocket安全套接字
try (SSLSocket socket = (SSLSocket) factory.createSocket(ip, port)) {
socket.setSoTimeout(TIMEOUT); // 设置超时时间
// 启用所有密码组
String[] supported = socket.getSupportedCipherSuites();
socket.setEnabledCipherSuites(supported);
// 其它操作
}
测试https图片爬虫
public static void main(String[] args) {
SocketDownload socketDownload = new SocketDownload();
socketDownload.httpsDownload("up.enterdesk.com", 443, ".jpg");
}
测试结果
测试说明
这里我们不严格比较一下两种请求的时间:
public static void main(String[] args) {
SocketDownload socketDownload = new SocketDownload();
long start = System.currentTimeMillis();
// socketDownload.httpsDownload("up.enterdesk.com", 443, ".jpg");
socketDownload.httpDownload("pic1.win4000.com", 80, ".jpg");
System.out.println("耗时:" + (System.currentTimeMillis()-start) + "ms");
}
HTTPS:
HTTP:
注意:这里没有严格控制变量,因为我也没有一个是http和https链接的同样大小图片,但是即使http请求的图片大,它的整体耗时还是远低于https,这就说明了https保证了安全,但是对于性能的代价也是很高的!
总结
通过自己动手去做这些事情,感觉对于计算机网络和Java的编程有了更多的理解。这样的形式也是很有趣的,最后再写一个博客总结一下。这里我并没有对代码进行简化,比如对于请求和响应都分开处理,那样虽然会显得简洁一些,但是我觉得这样一步一步得表达,更利于理解整个代码。
注意:这里需要说明一下,这些程序都是针对服务器的响应头部是具有 Content-Length 这个首部的,如果服务器采用的是 Chunked 编码,程序应该就无法正常运行了。但是我根据浏览器抓包,显示服务器确实是 Chunked 编码,但是我这里确实是获取到了 Content-Length。
PS 附:使用更加简单的方式下载图片
使用URLConnection 类来下载图片
package com.dragon;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
public class URLConnectionTest {
public static void main(String[] args) {
try {
URL url = new URL("https://up.enterdesk.com/edpic_source/17/4e/1d/174e1db844a7eac328fa4695edcb8158.jpg");
URLConnection connection = url.openConnection();
connection.setConnectTimeout(5*1000);
InputStream input = new BufferedInputStream(connection.getInputStream());
int length = connection.getContentLength();
try (OutputStream output = new BufferedOutputStream(new FileOutputStream("D:/DBC/url/pic.jpg"))) {
int hasRead = 0;
int len = 0;
byte[] b = new byte[1024];
while (hasRead != length && (len = input.read(b)) != -1) {
output.write(b, 0, len);
hasRead += len;
}
}
System.out.println("网络资源大小:" + length);
System.out.println("网络资源下载完成了");
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用 HttpsURLConnection 类来下载图片
package com.dragon;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
public class URLConnectionTest {
public static void main(String[] args) {
try {
URL url = new URL("https://up.enterdesk.com/edpic_source/17/4e/1d/174e1db844a7eac328fa4695edcb8158.jpg");
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setConnectTimeout(5*1000);
InputStream input = new BufferedInputStream(connection.getInputStream());
int length = connection.getContentLength();
try (OutputStream output = new BufferedOutputStream(new FileOutputStream("D:/DBC/url/pic02.jpg"))) {
int hasRead = 0;
int len = 0;
byte[] b = new byte[1024];
while (hasRead != length && (len = input.read(b)) != -1) {
output.write(b, 0, len);
hasRead += len;
}
}
System.out.println("网络资源大小:" + length);
System.out.println("网络资源下载完成了");
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用 HttpURLConnection 类来下载图片
按照我的理解,这个类应该是不可以的,但是它居然也可以用来下载https,相比是内部进行了处理。
package com.dragon;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class URLConnectionTest {
public static void main(String[] args) {
try {
URL url = new URL("https://up.enterdesk.com/edpic_source/17/4e/1d/174e1db844a7eac328fa4695edcb8158.jpg");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5*1000);
InputStream input = new BufferedInputStream(connection.getInputStream());
int length = connection.getContentLength();
try (OutputStream output = new BufferedOutputStream(new FileOutputStream("D:/DBC/url/pic03.jpg"))) {
int hasRead = 0;
int len = 0;
byte[] b = new byte[1024];
while (hasRead != length && (len = input.read(b)) != -1) {
output.write(b, 0, len);
hasRead += len;
}
}
System.out.println("网络资源大小:" + length);
System.out.println("网络资源下载完成了");
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用较为成熟的Http框架HttpClient来下载图片
如果写爬虫的话,当然是推荐使用它了,但是你应该也可以看出来,从底层到高层,使用的类越来越复杂,处理的事情反而越来越少了。可见这些东西对于生产力的提高是重要的,但是不利于学习底层的知识。所以为了更好的使用这些框架、类库,还是要从基础学起。
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
public class HttpClientTest {
public static void main(String[] args) {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet get = new HttpGet("https://up.enterdesk.com/edpic_source/17/4e/1d/174e1db844a7eac328fa4695edcb8158.jpg");
try (CloseableHttpResponse response = httpClient.execute(get)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) {
HttpEntity entity = response.getEntity();
entity.writeTo(new BufferedOutputStream(new FileOutputStream("D:/DBC/url/pic04.jpg")));
}
System.out.println("网络资源下载完成");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用NIO的api下载图片
java 也是有很简洁的api,这个使用起来就很方便。
package com.dragon;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
public class NIOTest {
public static void main(String[] args) {
try {
URL url = new URL("https://up.enterdesk.com/edpic_source/17/4e/1d/174e1db844a7eac328fa4695edcb8158.jpg");
try (InputStream in = url.openStream()) {
File file = new File("D:/DBC/url", "pic05.jpg");
Files.copy(in, file.toPath());
}
System.out.println("下载完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}