简单的Socket爬虫

前言

今天早上在睡懒觉的时候在考虑了一个问题,我可以使用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();
		}
	}
}

在这里插入图片描述

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值