Java 动手写爬虫: 一、实现一个最简单爬虫
2019独角兽企业重金招聘Python工程师标准>>>
第一篇
准备写个爬虫, 可以怎么搞?
使用场景
先定义一个最简单的使用场景,给你一个url,把这个url中指定的内容爬下来,然后停止
- 一个待爬去的网址(有个地方指定爬的网址)
- 如何获取指定的内容(可以配置规则来获取指定的内容)
设计 & 实现
1. 基本数据结构
CrawlMeta.java
一个配置项,包含塞入的 url 和 获取规则
/*** Created by yihui on 2017/6/27.*/
@ToString
public class CrawlMeta {/*** 待爬去的网址*/@Getter@Setterprivate String url;/*** 获取指定内容的规则, 因为一个网页中,你可能获取多个不同的内容, 所以放在集合中*/@Setterprivate Set<String> selectorRules;// 这么做的目的就是为了防止NPE, 也就是说支持不指定选择规则public Set<String> getSelectorRules() {return selectorRules != null ? selectorRules : new HashSet<>();}}
CrawlResult
抓取的结果,除了根据匹配的规则获取的结果之外,把整个html的数据也保存下来,这样实际使用者就可以更灵活的重新定义获取规则
import org.jsoup.nodes.Document;@Getter
@Setter
@ToString
public class CrawlResult {/*** 爬取的网址*/private String url;/*** 爬取的网址对应的 DOC 结构*/private Document htmlDoc;/*** 选择的结果,key为选择规则,value为根据规则匹配的结果*/private Map<String, List<String>> result;}
说明:这里采用jsoup来解析html
2. 爬取任务
爬取网页的具体逻辑就放在这里了
一个爬取的任务
CrawlJob
,爬虫嘛,正常来讲都会塞到一个线程中去执行,虽然我们是第一篇,也不至于low到直接放到主线程去做面向接口编程,所以我们定义了一个
IJob
的接口
IJob.java
这里定义了两个方法,在job执行之前和之后的回调,加上主要某些逻辑可以放在这里来做(如打日志,耗时统计等),将辅助的代码从爬取的代码中抽取,使代码结构更整洁
public interface IJob extends Runnable {/*** 在job执行之前回调的方法*/void beforeRun();/*** 在job执行完毕之后回调的方法*/void afterRun();
}
AbstractJob
因为IJob
多了两个方法,所以就衍生了这个抽象类,不然每个具体的实现都得去实现这两个方法,有点蛋疼
然后就是借用了一丝模板设计模式的思路,把run方法也实现了,单独拎了一个doFetchPage
方法给子类来实现,具体的抓取网页的逻辑
public abstract class AbstractJob implements IJob {public void beforeRun() {}public void afterRun() {}@Overridepublic void run() {this.beforeRun();try {this.doFetchPage();} catch (Exception e) {e.printStackTrace();}this.afterRun();}/*** 具体的抓去网页的方法, 需要子类来补全实现逻辑** @throws Exception*/public abstract void doFetchPage() throws Exception;
}
SimpleCrawlJob
一个最简单的实现类,直接利用了JDK的URL方法来抓去网页,然后利用jsoup进行html结构解析,这个实现中有较多的硬编码,先看着,下面就着手第一步优化
/*** 最简单的一个爬虫任务* <p>* Created by yihui on 2017/6/27.*/
@Getter
@Setter
public class SimpleCrawlJob extends AbstractJob {/*** 配置项信息*/private CrawlMeta crawlMeta;/*** 存储爬取的结果*/private CrawlResult crawlResult;/*** 执行抓取网页*/public void doFetchPage() throws Exception {URL url = new URL(crawlMeta.getUrl());HttpURLConnection connection = (HttpURLConnection) url.openConnection();BufferedReader in = null;StringBuilder result = new StringBuilder();try {// 设置通用的请求属性connection.setRequestProperty("accept", "*/*");connection.setRequestProperty("connection", "Keep-Alive");connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");// 建立实际的连接connection.connect();Map<String, List<String>> map = connection.getHeaderFields();//遍历所有的响应头字段for (String key : map.keySet()) {System.out.println(key + "--->" + map.get(key));}// 定义 BufferedReader输入流来读取URL的响应in = new BufferedReader(new InputStreamReader(connection.getInputStream()));String line;while ((line = in.readLine()) != null) {result.append(line);}} finally { // 使用finally块来关闭输入流try {if (in != null) {in.close();}} catch (Exception e2) {e2.printStackTrace();}}doParse(result.toString());}private void doParse(String html) {Document doc = Jsoup.parse(html);Map<String, List<String>> map = new HashMap<>(crawlMeta.getSelectorRules().size());for (String rule: crawlMeta.getSelectorRules()) {List<String> list = new ArrayList<>();for (Element element: doc.select(rule)) {list.add(element.text());}map.put(rule, list);}this.crawlResult = new CrawlResult();this.crawlResult.setHtmlDoc(doc);this.crawlResult.setUrl(crawlMeta.getUrl());this.crawlResult.setResult(map);}
}
4. 测试
上面一个最简单的爬虫就完成了,就需要拉出来看看,是否可以正常的工作了
就拿自己的博客作为测试网址,目标是获取 title + content,所以测试代码如下
/*** 测试我们写的最简单的一个爬虫,** 目标是爬取一篇博客*/
@Test
public void testFetch() throws InterruptedException {String url = "https://my.oschina.net/u/566591/blog/1031575";Set<String> selectRule = new HashSet<>();selectRule.add("div[class=title]"); // 博客标题selectRule.add("div[class=blog-body]"); // 博客正文CrawlMeta crawlMeta = new CrawlMeta();crawlMeta.setUrl(url); // 设置爬取的网址crawlMeta.setSelectorRules(selectRule); // 设置抓去的内容SimpleCrawlJob job = new SimpleCrawlJob();job.setCrawlMeta(crawlMeta);Thread thread = new Thread(job, "crawler-test");thread.start();thread.join(); // 确保线程执行完毕CrawlResult result = job.getCrawlResult();System.out.println(result);
}
代码演示示意图如下
从返回的结果可以看出,抓取到的title中包含了博客标题 + 作着,主要的解析是使用的 jsoup,所以这些抓去的规则可以参考jsoup的使用方式
优化
上面完成之后,有个地方看着就不太舒服,
doFetchPage
方法中的抓去网页,有不少的硬编码,而且那么一大串看着也不太利索, 所以考虑加一个配置项,用于记录HTTP相关的参数可以用更成熟的http框架来取代jdk的访问方式,维护和使用更加简单
仅针对这个最简单的爬虫,我们开始着手上面的两个优化点
1. 改用 HttpClient 来执行网络请求
使用httpClient,重新改上面的获取网页代码(暂不考虑配置项的情况), 对比之后发现代码会简洁很多
/*** 执行抓取网页*/
public void doFetchPage() throws Exception {HttpClient httpClient = HttpClients.createDefault();HttpGet httpGet = new HttpGet(crawlMeta.getUrl());HttpResponse response = httpClient.execute(httpGet);String res = EntityUtils.toString(response.getEntity());if (response.getStatusLine().getStatusCode() == 200) { // 请求成功doParse(res);} else {this.crawlResult = new CrawlResult();this.crawlResult.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());this.crawlResult.setUrl(crawlMeta.getUrl());}
}
这里加了一个对返回的code进行判断,兼容了一把访问不到数据的情况,对应的返回结果中,新加了一个表示状态的对象
CrawlResult
private Status status;public void setStatus(int code, String msg) {this.status = new Status(code, msg);
}@Getter
@Setter
@ToString
@AllArgsConstructor
static class Status {private int code;private String msg;
}
然后再进行测试,结果发现返回状态为 403, 主要是没有设置一些必要的请求参数,被拦截了,手动塞几个参数再试则ok
HttpGet httpGet = new HttpGet(crawlMeta.getUrl());
httpGet.addHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
httpGet.addHeader("connection", "Keep-Alive");
httpGet.addHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");
2. http配置项
显然每次都这么手动塞入参数是不可选的,我们有必要透出一个接口,由用户自己来指定一些请求和返回参数
首先我们可以确认下都有些什么样的配置项
- 请求方法: GET, POST, OPTIONS, DELET ...
- RequestHeader:
Accept
Cookie
Host
Referer
User-Agent
Accept-Encoding
Accept-Language
... (直接打开一个网页,看请求的hedaers即可) - 请求参数
- ResponseHeader: 这个我们没法设置,但是我们可以设置网页的编码(这个来fix中文乱码比较使用)
- 是否走https(这个暂时可以不考虑,后面讨论)
新增一个配置文件,配置参数主要为
- 请求方法
- 请求参数
- 请求头
@ToString
public class CrawlHttpConf {private static Map<String, String> DEFAULT_HEADERS;static {DEFAULT_HEADERS = new HashMap<>();DEFAULT_HEADERS.put("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");DEFAULT_HEADERS.put("connection", "Keep-Alive");DEFAULT_HEADERS.put("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");}public enum HttpMethod {GET,POST,OPTIONS,PUT;}@Getter@Setterprivate HttpMethod method = HttpMethod.GET;/*** 请求头*/@Setterprivate Map<String, String> requestHeaders;/*** 请求参数*/@Setterprivate Map<String, Object> requestParams;public Map<String, String> getRequestHeaders() {return requestHeaders == null ? DEFAULT_HEADERS : requestHeaders;}public Map<String, Object> getRequestParams() {return requestParams == null ? Collections.emptyMap() : requestParams;}
}
新建一个 HttpUtils
工具类,来具体的执行Http请求, 下面我们暂先实现Get/Post两个请求方式,后续可以再这里进行扩展和优化
public class HttpUtils {public static HttpResponse request(CrawlMeta crawlMeta, CrawlHttpConf httpConf) throws Exception {switch (httpConf.getMethod()) {case GET:return doGet(crawlMeta, httpConf);case POST:return doPost(crawlMeta, httpConf);default:return null;}}private static HttpResponse doGet(CrawlMeta crawlMeta, CrawlHttpConf httpConf) throws Exception {
// HttpClient httpClient = HttpClients.createDefault();SSLContextBuilder builder = new SSLContextBuilder();
// 全部信任 不做身份鉴定builder.loadTrustMaterial(null, (x509Certificates, s) -> true);HttpClient httpClient = HttpClientBuilder.create().setSslcontext(builder.build()).build();// 设置请求参数StringBuilder param = new StringBuilder(crawlMeta.getUrl()).append("?");for (Map.Entry<String, Object> entry : httpConf.getRequestParams().entrySet()) {param.append(entry.getKey()).append("=").append(entry.getValue()).append("&");}HttpGet httpGet = new HttpGet(param.substring(0, param.length() - 1)); // 过滤掉最后一个无效字符// 设置请求头for (Map.Entry<String, String> head : httpConf.getRequestHeaders().entrySet()) {httpGet.addHeader(head.getKey(), head.getValue());}// 执行网络请求return httpClient.execute(httpGet);}private static HttpResponse doPost(CrawlMeta crawlMeta, CrawlHttpConf httpConf) throws Exception {
// HttpClient httpClient = HttpClients.createDefault();SSLContextBuilder builder = new SSLContextBuilder();
// 全部信任 不做身份鉴定builder.loadTrustMaterial(null, (x509Certificates, s) -> true);HttpClient httpClient = HttpClientBuilder.create().setSslcontext(builder.build()).build();HttpPost httpPost = new HttpPost(crawlMeta.getUrl());// 建立一个NameValuePair数组,用于存储欲传送的参数List<NameValuePair> params = new ArrayList<>();for (Map.Entry<String, Object> param : httpConf.getRequestParams().entrySet()) {params.add(new BasicNameValuePair(param.getKey(), param.getValue().toString()));}httpPost.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));// 设置请求头for (Map.Entry<String, String> head : httpConf.getRequestHeaders().entrySet()) {httpPost.addHeader(head.getKey(), head.getValue());}return httpClient.execute(httpPost);}
}
然后我们的 doFetchPage 方法将简洁很多
/**
* 执行抓取网页
*/
public void doFetchPage() throws Exception {HttpResponse response = HttpUtils.request(crawlMeta, httpConf);String res = EntityUtils.toString(response.getEntity());if (response.getStatusLine().getStatusCode() == 200) { // 请求成功doParse(res);} else {this.crawlResult = new CrawlResult();this.crawlResult.setStatus(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());this.crawlResult.setUrl(crawlMeta.getUrl());}
}
下一步
上面我们实现的是一个最简陋,最基础的东西了,但是这个基本上又算是满足了核心的功能点,但距离一个真正的爬虫框架还差那些呢 ?
另一个核心的就是:
爬了一个网址之后,解析这个网址中的链接,继续爬!!!
下一篇则将在本此的基础上,考虑如何实现上面这个功能点;写这个博客的思路,将是先做一个实现需求场景的东西出来,,可能在开始实现时,很多东西都比较挫,兼容性扩展性易用性啥的都不怎么样,计划是在完成基本的需求点之后,然后再着手去优化看不顺眼的地方
坚持,希望可以持之以恒,完善这个东西
源码
项目地址: https://github.com/liuyueyi/quick-crawler
上面的分了两步,均可以在对应的tag中找到响应的代码,主要代码都在core模块下
第一步对应的tag为:v0.001
优化后对应的tag为:v0.002
转载于:https://my.oschina.net/u/566591/blog/1047233
Java 动手写爬虫: 一、实现一个最简单爬虫相关推荐
- 一起写个Dubbo——1. 一个最简单的实现
本文原载于我的博客,地址:https://blog.guoziyang.top/archives/61/,项目地址:https://github.com/CN-GuoZiyang/My-RPC-Fra ...
- python 爬虫 教程_一个入门级python爬虫教程详解
前言 本文目的:根据本人的习惯与理解,用最简洁的表述,介绍爬虫的定义.组成部分.爬取流程,并讲解示例代码. 基础 爬虫的定义:定向抓取互联网内容(大部分为网页).并进行自动化数据处理的程序.主要用于对 ...
- 用java汽车美容店管理系统_洗车店 一个较为简单的洗车店管理系统 联合开发网 - pudn.com...
201716010311%2B王杰%2B数据库课设, 0 , 2020-01-01 201716010311%2B王杰%2B数据库课设\201716010311%2B王杰%2B源代码, 0 , 201 ...
- python简易爬虫课程设计_python实现简单爬虫功能的示例
在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...
- python 爬虫程序示例,python实现简单爬虫功能的示例
在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...
- python实现简单爬虫百度首页_python实现简单爬虫功能的示例
在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...
- python简单爬虫代码-一则python3的简单爬虫代码
不得不说python的上手非常简单.在网上找了一下,大都是python2的帖子,于是随手写了个python3的.代码非常简单就不解释了,直接贴代码. 代码如下: #test rdp import ur ...
- 《跟我一起学爬虫系列》3-一个简单爬虫示例
本文抓取页面为http://news.baidu.com/,首先介绍下无关紧要但有必要了解的,然后代码说明 robots.txt Robots协议全称是网络爬虫排除标准,网站通过Robots协议定义网 ...
- pythonurllib登录微博账号_简单爬虫实现登录新浪微博(python2.7)
因为图论作业,所以要写一个爬虫,就开始学python.接触python开始,就觉得这个语言非常舒服,不需要定义变量,不需要分号,非常简洁. 下面就聊聊,我写爬虫的经历.上网搜了一下爬虫的代码,发现简单 ...
- 【python爬虫学习篇】初识网络爬虫以及了解Web前端
目录 1,初识爬虫 1.1,网络爬虫概述 1.2,爬虫的分类 1.3,网络爬虫的基本原理 1.4,搭建开发环境 2,了解web前端 2.1,HTTP基本原理 2.1.1HTTP协议 2.1.2,Web ...
最新文章
- 用矩阵内积的办法构造迭代次数受控的神经网络1:0.6:0.1=4:3:2
- Django基础一之web框架的本质
- python gps模块_一步一步使用uPyCraft学习MicroPython之GPS记录器
- 小女也爱c#(3)--俄罗斯方块练习数组
- mysql函数使用场景_mysql的函数和存储过程的比较,以及在实际场景中的使用案例...
- 字符串逆序(信息学奥赛一本通-T1162)
- 单体预聚合的目的是什么_第七章 配位聚合
- SPARK学习之 --- eclipse / sbt / scala 配置
- 虚幻开放日2017ppt
- 信号与系统公式笔记(8)——拉普拉斯变换
- python异常处理时所使用的保留字_【2020年12月计算机二级Python语言考试冲刺题(二)】- 环球网校...
- 第一篇文献:谈大数据时代的云控制摄影测量 ——张祖勋院士
- AJP:有和没有内化性精神障碍的受虐女孩情绪回路延迟成熟的差异性
- tiny linux u盘_多系统U盘启动盘制作工具(YUMI)下载-多系统U盘启动盘制作工具(YUMI)PC版下载v2.0.7.6...
- 多搜 - 多个网站一起搜 (舆情监控版)
- Tensorflow中的多层感知器学习
- Python+Flask
- keepalived脑裂现象
- 几种常见的软件团队模式优缺点总结
- Bin Packing Problem