欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java网络编程之爬虫--计算机网络、应用层协议的综合应用

程序员文章站 2022-07-03 11:51:00
前言:前几天在B站上面,看到了一个使用C/C++实现的网络爬虫,我没有看视频,只是看了评论,这位up主应该就是只使用语言本身提供的包实现的爬虫。但是,我对这种方式很有兴趣,所以我就来实现一个Java版本的,正好也是综合运用自己学习的知识。具体效果:注意:爬取过程中,出现了几个time out,但是也不影响整个爬虫的工作(出现异常的图片,可能会损坏,但是几百张失败一两次还是可以接受的),所以这里也就不处理它了,我测试了一下,把超时时间调高一点,并且当前网络通畅的话,也就不会遇到这个问题了。推荐阅读...

前言:
前几天在B站上面,看到了一个使用C/C++实现的网络爬虫,我没有看视频,只是看了评论,这位up主应该就是只使用语言本身提供的包实现的爬虫。但是,我对这种方式很有兴趣,所以我就来实现一个Java版本的,正好也是综合运用自己学习的知识。

具体效果:Java网络编程之爬虫--计算机网络、应用层协议的综合应用

Java网络编程之爬虫--计算机网络、应用层协议的综合应用

注意:爬取过程中,出现了几个time out,但是也不影响整个爬虫的工作(出现异常的图片,可能会损坏,但是几百张失败一两次还是可以接受的),所以这里也就不处理它了,我测试了一下,把超时时间调高一点,并且当前网络通畅的话,也就不会遇到这个问题了。

Java网络编程之爬虫--计算机网络、应用层协议的综合应用

推荐阅读:简单的socket
这篇博客是基于上面这篇博客的一个扩展,上次只是实现了Socket下载网络资源,这里就更进一步了,变成一个小小的爬虫。

给爷爬

这里给它取一个有趣的名字吧——给爷爬

这里我主要是想引入一个URL队列的概念,它的工作模式如下:
1.初始添加一个种子URL入队列。
2.从URL队列中取出一个URL,进行处理,将获取到新的URL加入队列(如果有新的URL的话)。
3.重复步骤2,直到队列为空。

通常,我们刚学习爬虫(现在,我也没有学习多久,哈哈。),只是一股脑的爬,比如按照页码来一个一个的爬取,先遇到的先爬取,这里其实都是有一个队列的思想的(先进先出 FIFO)。

Java网络编程之爬虫--计算机网络、应用层协议的综合应用
所以,这里我分了几个模块来处理,下面就来依次介绍这几个部分。

Request类

一个简单的实体类,实现url和name属性的封装。

package dragon;

public class Request {
	
	private String url;   // url
	private String name;  // name
	
	
	public Request(String url, String name) {
		super();
		this.url = url;
		this.name = name;
		
	}
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	
	@Override
	public String toString() {
		return "Picture [name=" + name + ", url=" + url + "]";
	}
}

MessageUtil类

因为是使用语言本身提供的Socket来处理的,没有引入更高级的类库,所以处理起来相对麻烦一下。比如,需要自己手工构造HTTP请求报文、手工HTTP解析响应报文。不过,这里的请求报文,它的结构很简单,这个网站也没有什么反爬机制,用来练手很合适。

package dragon;

import java.nio.charset.StandardCharsets;

/**
 * 构造一个适当的请求报文
 * */
public class MessageUtil {
	
	private final static String UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) "
			+ "Chrome/86.0.4240.80 Safari/537.36 Edg/86.0.622.48";
	private final static char CR = '\r';
	private final static char LF = '\n';
	private final static char BLANK = ' ';
	
	public static byte[] getRequestMsg(String path, String host, String referer) {
		StringBuilder msgBuilder = new StringBuilder();
		msgBuilder.append("GET").append(BLANK).append(path).append(BLANK).append("HTTP/1.0").append(CR).append(LF)
		          .append("User-Agent:").append(BLANK).append(UA).append(CR).append(LF)
				  .append("Host:").append(BLANK).append(host).append(CR).append(LF)
				  .append("Upgrade-Insecure-Requests:").append(BLANK).append(1).append(CR).append(LF)
				  .append("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9").append("\r\n")
				  .append(CR).append(LF);
		
		String msg = msgBuilder.toString();
		return msg.getBytes(StandardCharsets.UTF_8);
	}
}

PageDownloader类

使用套接字发送请求报文,接收响应报文。因为HTTP是建立在TCP之上的,所以请求报文和响应报文,也就是对应了套接字的OutputStream和InputStream了。但是,要注意这里使用的不是Socket,而是SSLSocket。因为,这个图片网站使用的是https协议,如果使用Socket的话,是无法建立连接的。这里,只要这样使用就行了,关于SSLSocket的详细情况,我也不是太了解。因为平时处理网络,都是使用高级api,它们会屏蔽这些细节的。
这里需要了解HTTP和HTTPS的区别,SSLSocket就是安全层套接字。
HTTP:Hyper Text Transport Protocol
HTTPS:Hyper Text Transport Protocol Over SecureSocket Layer

这个类的作用是下载html文件,它首先发送一个请求报文,然后接收响应报文。但是响应报文,是含有首部的,尽管我们平时是看不见的,所以这里返回的不只是html文件本身了,因此这里你需要了解一下HTTP报文的结构。

Java网络编程之爬虫--计算机网络、应用层协议的综合应用

所以我这里定义了一个方法:readMsgHeader(),直接读取首部并简单丢弃,不做处理。

package dragon;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
 * 下载页面
 * */
public class PageDownloader {
	
	private static SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();// 利用工厂来创建SSLSocket安全套接字
	private final static int TIMEOUT = 10*1000;
	
	public String getHtml(String link, String cs) {
		URL url = null;
		try {
			url = new URL(link);
		} catch (MalformedURLException e1) {
			e1.printStackTrace();
		}
		
		String host = url.getHost();            // 获取主机 Host
		int port = url.getPort() != -1 ? url.getPort() : 443;  // 获取端口号
		String path = url.getPath(); // 请求路径

		try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
			socket.setSoTimeout(TIMEOUT);   // 设置超时时间
			
			// 启用所有密码组
			String[] supported = socket.getSupportedCipherSuites();
			socket.setEnabledCipherSuites(supported);
			
			// 获取输出流和输入流
			OutputStream output = new BufferedOutputStream(socket.getOutputStream());
			InputStream input = new BufferedInputStream(socket.getInputStream());
			// 使用输出流发送请求数据
			byte[] requestMsg = MessageUtil.getRequestMsg(path, host, link);
			output.write(requestMsg);			
			output.flush();  // 刷新输出流,不然未发送请求,导致无法接收到响应
			
			// 过滤报文首部数据
			try {
				readMsgHeader(input);
			} catch (Exception e) {
				e.printStackTrace();
				System.out.println("连接发生异常,退出当前连接下载!");
				return null;
			}
			
			try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
				int len = 0;
				byte[] b = new byte[2048];
        		while ((len = input.read(b)) != -1) {
        			out.write(b, 0, len);
            	}
        		return out.toString(cs);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * 读取报文头部,但是不做处理,只是简单的过滤头部的信息
	 * @throws Exception 
	 * */
	private void readMsgHeader(InputStream in) throws Exception {
		StringBuilder sb = new StringBuilder();
		while (true) {
			int c = in.read();
			if (c != -1) {
				if (c != '\n') {
					sb.append((char)c);
				} else {
					int len = sb.length();
					if (len == 1) {
						break; // 读取到最后一行 \r\n,退出循环。
					} else {
						sb.delete(0, len);
					}
				}
			} else {
				throw new Exception("连接发生异常!");
			}
		}
	}
}

UrlDownloader类

用于下载图片的类,它的功能和前面的PageDownloader类基本相似,但是它是将获取的图片数据持久化到本地。

package 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.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

public class UrlDownloader implements Runnable {
	private static SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();// 利用工厂来创建SSLSocket安全套接字
	private final static int TIMEOUT = 10*1000;
	private Request request;
	private String filePath;

	public UrlDownloader(Request request, String filePath) {
		this.request = request;
		this.filePath = filePath;
	}
	
	@Override
	public void run() {
		String link = request.getUrl();
		URL url = null;
		try {
			url = new URL(link);
		} catch (MalformedURLException e1) {
			e1.printStackTrace();
		}
		
		String host = url.getHost();            // 获取主机 Host
		int port = url.getPort() != -1 ? url.getPort() : 443;  // 获取端口号
		String path = url.getPath(); // 请求路径

		try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
			socket.setSoTimeout(TIMEOUT);   // 设置超时时间
			
			// 启用所有密码组
			String[] supported = socket.getSupportedCipherSuites();
			socket.setEnabledCipherSuites(supported);
			
			// 获取输出流和输入流
			OutputStream output = new BufferedOutputStream(socket.getOutputStream());
			InputStream input = new BufferedInputStream(socket.getInputStream());
			// 使用输出流发送请求数据
			byte[] requestMsg = MessageUtil.getRequestMsg(path, host, "https://www.showmeizi.com");
			output.write(requestMsg);			
			output.flush();  // 刷新输出流,不然未发送请求,导致无法接收到响应
			
			// 过滤报文首部数据
			try {
				readMsgHeader(input);
			} catch (Exception e) {
				e.printStackTrace();
				System.out.println("连接发生异常,退出当前连接下载!");
				return ;
			}
			// 读取响应报文数据部,即图片本身的二进制数据
			try (OutputStream outputFile = new BufferedOutputStream(
					new FileOutputStream(new File(filePath, request.getName())))) {
				int len = 0;
				byte[] b = new byte[2048];
        		while ((len = input.read(b)) != -1) {
        			outputFile.write(b, 0, len);
            	}
			}
		} catch (UnknownHostException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("资源:" + link + " 抓取完成!");
	}
	
	/**
	 * 读取报文头部,但是不做处理,只是简单的过滤头部的信息
	 * @throws Exception 
	 * */
	private void readMsgHeader(InputStream in) throws Exception {
		StringBuilder sb = new StringBuilder();
		while (true) {
			int c = in.read();
			if (c != -1) {
				if (c != '\n') {
					sb.append((char)c);
				} else {
					int len = sb.length();
					if (len == 1) {
						break; // 读取到最后一行 \r\n,退出循环。
					} else {
						sb.delete(0, len);
					}
				}
			} else {
				throw new Exception("连接发生异常!");
			}
		}
	}

}

DataCrawler类

整个给爷爬的主要逻辑都在这个类里面了。这部分,就是关于网页结构的知识了。

爬虫的轨迹是如下的:首先从根路径开始,获取根路径所在html页面的所有标题页(只获取第一页的所有标题页用于演示),然后对于每一个标题页提取所有的图片链接信息,最后依次下载每一条链接,总体上逻辑还是很简单的。

package dragon;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 准备不使用外部依赖,仅仅依靠语言本身的功能完成一个简单的数据采集任务。
 * 用来检验自己这段时间的网络学习的成果,并加深对于 网络、协议、web的理解。
 * */
public class DataCrawler {
	
	// 用于存储待抓取数据url的队列
	private static Queue<Request> queue = new LinkedList<>();
	private static String filePath = "D:/DBC/Test";
	
	public static void main(String[] args) {
		System.out.println("启动 --> 给爷爬!");
		// 添加一个根路径,然后开始url调度
		queue.offer(new Request("https://www.showmeizi.com/category/qingchun", "qingchun"));
		
		// 下载对应的html页面
		PageDownloader pageDownloader = new PageDownloader();
		String html = null;
		
		// 创建一个线程池
		ExecutorService pool = Executors.newFixedThreadPool(2);  // 没有使用代理ip,而且只是验证,慢慢爬也无所谓了
		
		while(!queue.isEmpty()) {
			Request request = queue.poll();
			String url = request.getUrl();
			System.out.println("开始处理url:" + url + " --> " + request.getName());
			if (url.contains("category")) {  // 这个url是爬虫的起点
				html = pageDownloader.getHtml(url, "UTF-8");
				resolveRoot(html);
			} else if (url.contains("detail")) {  // 对应标题页的url
				html = pageDownloader.getHtml(url, "UTF-8");
				resolveChild(html);
			} else {  // 图片对应的url
				pool.submit(new UrlDownloader(request, filePath));
			}
		}
		pool.shutdown();
		
		// 采用轮询方式,当爬虫结束时打印该语句
		while (true) {
			if (pool.isTerminated()) {
				System.out.println("结束 --> 给爷爬");
				break;  
			}
		}
	}
	
	public static void resolveRoot(String html) {
		html = html.replace(' ', '_');   // 由于标题字符串中含有空格,所以先替换掉空格,再匹配
		String regex = "/detail/\\d{4}";                     // 匹配URL
		String regex1 = "/(\\S+|\\s*|\\S+)-thumbnail.jpg";   // 匹配对应的标题
		Pattern urlPattern = Pattern.compile(regex);
		Pattern titlePattern = Pattern.compile(regex1);
		Matcher urlMatcher = urlPattern.matcher(html);
		Matcher titleMatcher = titlePattern.matcher(html);
		while(urlMatcher.find() && titleMatcher.find()) {
			String url = urlMatcher.group();
			String title = titleMatcher.group();
			queue.offer(new Request("https://www.showmeizi.com" + url, title));   // 添加新的request对象进入队列,等待处理
		}
	}
	
	public static void resolveChild(String html) {
		html = html.replace(' ', '_');   // 由于标题字符串中含有空格,所以先替换掉空格,再匹配		
		
		String regex = "/(\\S+)/\\d{2}[a-z]\\d{2}.jpg";                     // 匹配URL
		Pattern pattern = Pattern.compile(regex);
		Matcher matcher = pattern.matcher(html);
		while(matcher.find()) {
			String url = matcher.group();
			String title = url.substring(url.lastIndexOf("/")+1);
			url = url.replace('_', ' ');  // 这里需要注意,有的路径含有空格,我将其替换了,现在再替换回去
			queue.offer(new Request("https://www.showmeizi.com" + url, title));   // 添加新的request对象进入队列,等待处理
		}
	}
	
}

总结

这里使用Java网络编程+计算机网络的知识,而且是从一个底层开始处理(相对于应用层的底层,不是计算机网络的底层),具有很好的学习价值。特别是刚刚接触计算机网络的人,可以给人一种很直观感受,而不只是单纯书本上面的概念,并且学以致用,也是一种比较有效的学习方式。

本文地址:https://blog.csdn.net/qq_40734247/article/details/109268318