零基础写Java知乎爬虫之进阶篇

转载自 零基础写Java知乎爬虫之进阶篇

前面几篇文章,我们都是简单的实现了java爬虫抓取内容的问题,那么如果遇到复杂情况,我们还能继续那么做吗?答案当然是否定的,之前的仅仅是入门篇,都是些基础知识,给大家练手用的,本文我们就来点高大上的东西

说到爬虫,使用Java本身自带的URLConnection可以实现一些基本的抓取页面的功能,但是对于一些比较高级的功能,比如重定向的处理,HTML标记的去除,仅仅使用URLConnection还是不够的。

在这里我们可以使用HttpClient这个第三方jar包。

接下来我们使用HttpClient简单的写一个爬去百度的Demo:微笑

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
/*** * @author CallMeWhy* */
public class Spider {private static HttpClient httpClient = new HttpClient();/*** @param path*            目标网页的链接* @return 返回布尔值,表示是否正常下载目标页面* @throws Exception*             读取网页流或写入本地文件流的IO异常*/public static boolean downloadPage(String path) throws Exception {// 定义输入输出流InputStream input = null;OutputStream output = null;// 得到 post 方法GetMethod getMethod = new GetMethod(path);// 执行,返回状态码int statusCode = httpClient.executeMethod(getMethod);// 针对状态码进行处理// 简单起见,只处理返回值为 200 的状态码if (statusCode == HttpStatus.SC_OK) {input = getMethod.getResponseBodyAsStream();// 通过对URL的得到文件名String filename = path.substring(path.lastIndexOf('/') + 1)+ ".html";// 获得文件输出流output = new FileOutputStream(filename);// 输出到文件int tempByte = -1;while ((tempByte = input.read()) > 0) {output.write(tempByte);}// 关闭输入流if (input != null) {input.close();}// 关闭输出流if (output != null) {output.close();}return true;}return false;}public static void main(String[] args) {try {// 抓取百度首页,输出Spider.downloadPage("http://www.baidu.com");} catch (Exception e) {e.printStackTrace();}}
}

但是这样基本的爬虫是不能满足各色各样的爬虫需求的。

先来介绍宽度优先爬虫。

宽度优先相信大家都不陌生,简单说来可以这样理解宽度优先爬虫。

我们把互联网看作一张超级大的有向图,每一个网页上的链接都是一个有向边,每一个文件或没有链接的纯页面则是图中的终点:


宽度优先爬虫就是这样一个爬虫,爬走在这个有向图上,从根节点开始一层一层往外爬取新的节点的数据。

宽度遍历算法如下所示:

(1) 顶点 V 入队列。
(2) 当队列非空时继续执行,否则算法为空。
(3) 出队列,获得队头节点 V,访问顶点 V 并标记 V 已经被访问。
(4) 查找顶点 V 的第一个邻接顶点 col。
(5) 若 V 的邻接顶点 col 未被访问过,则 col 进队列。
(6) 继续查找 V 的其他邻接顶点 col,转到步骤(5),若 V 的所有邻接顶点都已经被访问过,则转到步骤(2)。

按照宽度遍历算法,上图的遍历顺序为:A->B->C->D->E->F->H->G->I,这样一层一层的遍历下去。

而宽度优先爬虫其实爬取的是一系列的种子节点,和图的遍历基本相同。

我们可以把需要爬取页面的URL都放在一个TODO表中,将已经访问的页面放在一个Visited表中:


则宽度优先爬虫的基本流程如下:

(1) 把解析出的链接和 Visited 表中的链接进行比较,若 Visited 表中不存在此链接, 表示其未被访问过。
(2) 把链接放入 TODO 表中。
(3) 处理完毕后,从 TODO 表中取得一条链接,直接放入 Visited 表中。
(4) 针对这个链接所表示的网页,继续上述过程。如此循环往复。

下面我们就来一步一步制作一个宽度优先的爬虫。

首先,对于先设计一个数据结构用来存储TODO表, 考虑到需要先进先出所以采用队列,自定义一个Quere类:

import java.util.LinkedList;
/*** 自定义队列类 保存TODO表*/
public class Queue {/*** 定义一个队列,使用LinkedList实现*/private LinkedList<Object> queue = new LinkedList<Object>(); // 入队列/*** 将t加入到队列中*/public void enQueue(Object t) {queue.addLast(t);}/*** 移除队列中的第一项并将其返回*/public Object deQueue() {return queue.removeFirst();}/*** 返回队列是否为空*/public boolean isQueueEmpty() {return queue.isEmpty();}/*** 判断并返回队列是否包含t*/public boolean contians(Object t) {return queue.contains(t);}/*** 判断并返回队列是否为空*/public boolean empty() {return queue.isEmpty();}
}

还需要一个数据结构来记录已经访问过的 URL,即Visited表。

考虑到这个表的作用,每当要访问一个 URL 的时候,首先在这个数据结构中进行查找,如果当前的 URL 已经存在,则丢弃这个URL任务。

这个数据结构需要不重复并且能快速查找,所以选择HashSet来存储。

综上,我们另建一个SpiderQueue类来保存Visited表和TODO表:

import java.util.HashSet;
import java.util.Set;
/*** 自定义类 保存Visited表和unVisited表*/
public class SpiderQueue {/*** 已访问的url集合,即Visited表*/private static Set<Object> visitedUrl = new HashSet<>();/*** 添加到访问过的 URL 队列中*/public static void addVisitedUrl(String url) {visitedUrl.add(url);}/*** 移除访问过的 URL*/public static void removeVisitedUrl(String url) {visitedUrl.remove(url);}/*** 获得已经访问的 URL 数目*/public static int getVisitedUrlNum() {return visitedUrl.size();}/*** 待访问的url集合,即unVisited表*/private static Queue unVisitedUrl = new Queue();/*** 获得UnVisited队列*/public static Queue getUnVisitedUrl() {return unVisitedUrl;}/*** 未访问的unVisitedUrl出队列*/public static Object unVisitedUrlDeQueue() {return unVisitedUrl.deQueue();}/*** 保证添加url到unVisitedUrl的时候每个 URL只被访问一次*/public static void addUnvisitedUrl(String url) {if (url != null && !url.trim().equals("") && !visitedUrl.contains(url)&& !unVisitedUrl.contians(url))unVisitedUrl.enQueue(url);}/*** 判断未访问的 URL队列中是否为空*/public static boolean unVisitedUrlsEmpty() {return unVisitedUrl.empty();}
}

上面是一些自定义类的封装,接下来就是一个定义一个用来下载网页的工具类,我们将其定义为DownTool类:

package controller;
import java.io.*;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.httpclient.params.*;
public class DownTool {/*** 根据 URL 和网页类型生成需要保存的网页的文件名,去除 URL 中的非文件名字符*/private String getFileNameByUrl(String url, String contentType) {// 移除 "http://" 这七个字符url = url.substring(7);// 确认抓取到的页面为 text/html 类型if (contentType.indexOf("html") != -1) {// 把所有的url中的特殊符号转化成下划线url = url.replaceAll("[\\?/:*|<>\"]", "_") + ".html";} else {url = url.replaceAll("[\\?/:*|<>\"]", "_") + "."+ contentType.substring(contentType.lastIndexOf("/") + 1);}return url;}/*** 保存网页字节数组到本地文件,filePath 为要保存的文件的相对地址*/private void saveToLocal(byte[] data, String filePath) {try {DataOutputStream out = new DataOutputStream(new FileOutputStream(new File(filePath)));for (int i = 0; i < data.length; i++)out.write(data[i]);out.flush();out.close();} catch (IOException e) {e.printStackTrace();}}// 下载 URL 指向的网页public String downloadFile(String url) {String filePath = null;// 1.生成 HttpClinet对象并设置参数HttpClient httpClient = new HttpClient();// 设置 HTTP连接超时 5shttpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);// 2.生成 GetMethod对象并设置参数GetMethod getMethod = new GetMethod(url);// 设置 get请求超时 5sgetMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 5000);// 设置请求重试处理getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,new DefaultHttpMethodRetryHandler());// 3.执行GET请求try {int statusCode = httpClient.executeMethod(getMethod);// 判断访问的状态码if (statusCode != HttpStatus.SC_OK) {System.err.println("Method failed: "+ getMethod.getStatusLine());filePath = null;}// 4.处理 HTTP 响应内容byte[] responseBody = getMethod.getResponseBody();// 读取为字节数组// 根据网页 url 生成保存时的文件名filePath = "temp\\"+ getFileNameByUrl(url,getMethod.getResponseHeader("Content-Type").getValue());saveToLocal(responseBody, filePath);} catch (HttpException e) {// 发生致命的异常,可能是协议不对或者返回的内容有问题System.out.println("请检查你的http地址是否正确");e.printStackTrace();} catch (IOException e) {// 发生网络异常e.printStackTrace();} finally {// 释放连接getMethod.releaseConnection();}return filePath;}
}

在这里我们需要一个HtmlParserTool类来处理Html标记:

package controller;
import java.util.HashSet;
import java.util.Set;
import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.filters.NodeClassFilter;
import org.htmlparser.filters.OrFilter;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;
import model.LinkFilter;
public class HtmlParserTool {// 获取一个网站上的链接,filter 用来过滤链接public static Set<String> extracLinks(String url, LinkFilter filter) {Set<String> links = new HashSet<String>();try {Parser parser = new Parser(url);parser.setEncoding("gb2312");// 过滤 <frame >标签的 filter,用来提取 frame 标签里的 src 属性NodeFilter frameFilter = new NodeFilter() {private static final long serialVersionUID = 1L;@Overridepublic boolean accept(Node node) {if (node.getText().startsWith("frame src=")) {return true;} else {return false;}}};// OrFilter 来设置过滤 <a> 标签和 <frame> 标签OrFilter linkFilter = new OrFilter(new NodeClassFilter(LinkTag.class), frameFilter);// 得到所有经过过滤的标签NodeList list = parser.extractAllNodesThatMatch(linkFilter);for (int i = 0; i < list.size(); i++) {Node tag = list.elementAt(i);if (tag instanceof LinkTag)// <a> 标签{LinkTag link = (LinkTag) tag;String linkUrl = link.getLink();// URLif (filter.accept(linkUrl))links.add(linkUrl);} else// <frame> 标签{// 提取 frame 里 src 属性的链接, 如 <frame src="test.html"/>String frame = tag.getText();int start = frame.indexOf("src=");frame = frame.substring(start);int end = frame.indexOf(" ");if (end == -1)end = frame.indexOf(">");String frameUrl = frame.substring(5, end - 1);if (filter.accept(frameUrl))links.add(frameUrl);}}} catch (ParserException e) {e.printStackTrace();}return links;}
}
最后我们来写个爬虫类调用前面的封装类和函数:package controller;
import java.util.Set;
import model.LinkFilter;
import model.SpiderQueue;
public class BfsSpider {/*** 使用种子初始化URL队列*/private void initCrawlerWithSeeds(String[] seeds) {for (int i = 0; i < seeds.length; i++)SpiderQueue.addUnvisitedUrl(seeds[i]);}// 定义过滤器,提取以 http://www.xxxx.com开头的链接public void crawling(String[] seeds) {LinkFilter filter = new LinkFilter() {public boolean accept(String url) {if (url.startsWith("http://www.baidu.com"))return true;elsereturn false;}};// 初始化 URL 队列initCrawlerWithSeeds(seeds);// 循环条件:待抓取的链接不空且抓取的网页不多于 1000while (!SpiderQueue.unVisitedUrlsEmpty()&& SpiderQueue.getVisitedUrlNum() <= 1000) {// 队头 URL 出队列String visitUrl = (String) SpiderQueue.unVisitedUrlDeQueue();if (visitUrl == null)continue;DownTool downLoader = new DownTool();// 下载网页downLoader.downloadFile(visitUrl);// 该 URL 放入已访问的 URL 中SpiderQueue.addVisitedUrl(visitUrl);// 提取出下载网页中的 URLSet<String> links = HtmlParserTool.extracLinks(visitUrl, filter);// 新的未访问的 URL 入队for (String link : links) {SpiderQueue.addUnvisitedUrl(link);}}}// main 方法入口public static void main(String[] args) {BfsSpider crawler = new BfsSpider();crawler.crawling(new String[] { "http://www.baidu.com" });}
}

运行可以看到,爬虫已经把百度网页下所有的页面都抓取出来了:

以上就是java使用HttpClient工具包和宽度爬虫进行抓取内容的操作的全部内容,稍微复杂点,小伙伴们要仔细琢磨下哦,希望对大家能有所帮助


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329240.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL导入冲突保留两者_面试被问MySQL 主从复制,怎么破?

一、前言随着应用业务数据不断的增大&#xff0c;应用的响应速度不断下降&#xff0c;在检测过程中我们不难发现大多数的请求都是查询操作。此时&#xff0c;我们可以将数据库扩展成主从复制模式&#xff0c;将读操作和写操作分离开来&#xff0c;多台数据库分摊请求&#xff0…

“.Net 社区虚拟大会”(dotnetConf) 2016 Day 1 Keynote: Scott Hunter

“.Net 社区虚拟大会”(dotnetConf) 2016 今天凌晨在Channel9 上召开&#xff0c;在Scott Hunter的30分钟的 Keynote上没有特别的亮点&#xff0c;所讲内容都是 微软“.Net社区虚拟大会”dotnetConf2015&#xff1a;关键词&#xff1a;.NET 创新、开源、跨平台 的具体化&#x…

Java(enum)枚举用法详解

转载自 Java&#xff08;enum&#xff09;枚举用法详解本篇文章主要介绍了Java 枚举用法详解&#xff0c;枚举的好处&#xff1a;可以将常量组织起来&#xff0c;统一进行管理。有兴趣的可以一起来了解一下。概念 enum的全称为 enumeration&#xff0c; 是 JDK 1.5 中引入的新特…

python处理脑电信号_用ICA去除脑电信号中的眼球链接

你有没有注意到你的“组件”完全是原始信号的比例和颠倒&#xff1f;这是因为你不能得到比信号更多的成分。在您需要执行以下步骤&#xff1a;将所有EEG通道输入ICA手动移除包含眨眼或其他伪影的组件用反变换重构让我们详细了解第2步&#xff1a;为什么要手动删除组件&#xff…

ASP.NET Core 中文文档 第一章 入门

原文&#xff1a;Getting Started翻译&#xff1a;娄宇(Lyrics)校对&#xff1a;刘怡(AlexLEWIS) 1、安装 .NET Core 2、创建一个新的 .NET Core 项目&#xff1a; mkdir aspnetcoreappcd aspnetcoreapp dotnet new 3、编辑 project.json 文件&#xff0c;添加 Kestrel HTTP se…

Properties文件的XML格式

转载自 Properties文件的XML格式 想必大家都用过*.properties文件&#xff0c;作为配置文件。但是&#xff0c;如果该文件写入了中文&#xff0c;待编译后内容就会成为乱码&#xff0c;使用native命令也好、使用ant执行编码转换也好&#xff0c;多少有点麻烦&#xff0c;与其如…

python简单爬虫课题_VS2019python爬虫入门

VS2019新建python项目在vs2019中添加python编译环境创建python控制台应用程序项目配置python环境安装requests第三方库管理程序包&#xff0c;执行安装requests包命令pip install requests导入第三方包import requests简单爬虫编写import requestsif __name__ "__main__&…

“.Net 社区虚拟大会”(dotnetConf) 2016 Day 2 Keynote: Miguel de Icaza

美国时间 6月7日--9日&#xff0c;为期三天的微软.NET社区虚拟大会正式在 Channel9 上召开&#xff0c;美国时间6.8 是第二天&#xff0c; Miguel de Icaza 做Keynote&#xff0c;Miguel 在波士顿Xamarin的办公室&#xff0c;所以使用了Skype。 class"video_iframe" …

Java泛型总结

转载自 Java泛型总结 Java泛型是JDK5引入的一个新特性&#xff0c;允许在定义类和接口的时候使用类型参数&#xff08;type parameter&#xff09;。声明的类型参数在使用的时候使用具体的类型来替换。泛型最主要的应用是在JDK5中的新集合类框架中。对于泛型概念的引入&#xf…

用 Visual Studio Code 在 macOS 上创建首个 ASP.NET Core 应用程序

原文&#xff1a;Your First ASP.NET Core Application on a Mac Using Visual Studio Code作者&#xff1a;Daniel Roth、Steve Smith 以及 Rick Anderson翻译&#xff1a;赵志刚校对&#xff1a;何镇汐、刘怡(AlexLEWIS) 本节将展示如何在 macOS 平台上创建首个 ASP.NET Core…

linux安装mysql遇到的问题_Linux下安装MySQL5.7及遇到的问题解决方法

一、下载地址本文安装的版本&#xff1a;或者使用wget下载&#xff1a;[rootlocalhost opt]# wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.32-el7-x86_64.tar.gz[rootlocalhost opt]# tar -xvf mysql-5.7.32-el7-x86_64.tar.gz二、检查是否已经安装过mysql[…

Java 8 新特性:扩展注解(类型注解和重复注解)

转载自 Java 8 新特性&#xff1a;扩展注解&#xff08;类型注解和重复注解&#xff09;&#xff08;注&#xff1a;先回顾下JDK1.5版本出现的注解 &#xff0c;然后再解释JDK 8的注解 更新内容。&#xff09; 一.注解&#xff08;JDK1.5&#xff09; 1.注解&#xff08;&…

框架写mysql插入为空_学习springMVC框架配置遇到的问题-数据写入不进数据库时的处理办法...

Idea简单SpringMVC框架配置前边已经介绍过了Struts在Idea上的配置,相对于Struts来说,我觉得SpringMVC有更多的优势,首先Struts是需要对action进行配置,页面发送不同的请求,就需要配置不同的acti ...hibernate学习之一 框架配置hibernate 框架 1.hibernate框架应用在javaee三层结…

“.Net 社区虚拟大会”(dotnetConf) 2016 Day 3 Keynote: Scott Hanselman

美国时间 6月7日--9日&#xff0c;为期三天的微软.NET社区虚拟大会正式在 Channel9 上召开&#xff0c;美国时间6.9 是第三天&#xff0c; Scott Hanselman 做Keynote。今天主题围绕的是.NET OpenSource 展开&#xff0c;Hanselman通过PowerBI分析了.NET社区这两年的发展&#…

Java注释@interface的用法

转载自 Java注释interface的用法java用 interface Annotation{ } 定义一个注解 Annotation&#xff0c;一个注解是一个类 Override&#xff0c;Deprecated&#xff0c;SuppressWarnings为常见的3个注解。注解相当于一种标记&#xff0c;在程序中加上了注解就等于为程序加上了某…

RPC的发展历史

服务器通讯原理就是一台socket服务器A,另一台socket客户端B,现在如果要通讯的话直接以流方式写入或读出。 这样能实现通讯&#xff0c;但有个问题。如何知道更多信息&#xff1f;比如需要发送流大小&#xff0c;编码&#xff0c;Ip等。 这样就有了协议&#xff0c;协议就是规范…

类的继承python 简明_[简明python教程]学习笔记2014-05-04

今天学习的内容&#xff1a;1.面向对象编程的概念1)面向对象的三个基本特征&#xff1a;封装、继承、多态2)类和对象是面向对象编程的2个主要方面。类使用class关键字创建。类的域和方法被列在一个缩进块中。2.类[rootreed 0504]# cat simpleclass.py#!/usr/bin/pythonclass Pe…

子类可以继承到父类上的注解吗

转载自 子类可以继承到父类上的注解吗?不了解注解基础知识的请先看《JDK 5 Annotation\注解\注释\自定义注解》子类可以继承到父类上的注解吗&#xff1f; 我们知道在编写自定义注解时&#xff0c;可以通过指定Inherited注解&#xff0c;指明自定义注解是否可以被继承。但实现…

java aop 实例_Spring aop 简单示例

简单的记录一下spring aop的一个示例基于两种配置方式&#xff1a;基于xml配置基于注解配置这个例子是模拟对数据库的更改操作添加事物其实并没有添加&#xff0c;只是简单的输出了一下记录首先看下整个例子的目录图全部代码就不贴了&#xff0c;数目有点多&#xff0c;不过很简…

用 Visual Studio 和 ASP.NET Core MVC 创建首个 Web API

原文&#xff1a;Building Your First Web API with ASP.NET Core MVC and Visual Studio作者&#xff1a;Mike Wasson 和 Rick Anderson翻译&#xff1a;谢炀(kiler)校对&#xff1a;何镇汐、刘怡(AlexLEWIS) HTTP 协议不仅仅提供网页服务。它也是一个构建公开服务和数据 API …