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的使用方式

优化

  1. 上面完成之后,有个地方看着就不太舒服,doFetchPage 方法中的抓去网页,有不少的硬编码,而且那么一大串看着也不太利索, 所以考虑加一个配置项,用于记录HTTP相关的参数

  2. 可以用更成熟的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 动手写爬虫: 一、实现一个最简单爬虫相关推荐

  1. 一起写个Dubbo——1. 一个最简单的实现

    本文原载于我的博客,地址:https://blog.guoziyang.top/archives/61/,项目地址:https://github.com/CN-GuoZiyang/My-RPC-Fra ...

  2. python 爬虫 教程_一个入门级python爬虫教程详解

    前言 本文目的:根据本人的习惯与理解,用最简洁的表述,介绍爬虫的定义.组成部分.爬取流程,并讲解示例代码. 基础 爬虫的定义:定向抓取互联网内容(大部分为网页).并进行自动化数据处理的程序.主要用于对 ...

  3. 用java汽车美容店管理系统_洗车店 一个较为简单的洗车店管理系统 联合开发网 - pudn.com...

    201716010311%2B王杰%2B数据库课设, 0 , 2020-01-01 201716010311%2B王杰%2B数据库课设\201716010311%2B王杰%2B源代码, 0 , 201 ...

  4. python简易爬虫课程设计_python实现简单爬虫功能的示例

    在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...

  5. python 爬虫程序示例,python实现简单爬虫功能的示例

    在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...

  6. python实现简单爬虫百度首页_python实现简单爬虫功能的示例

    在我们日常上网浏览网页的时候,经常会看到一些好看的图片,我们就希望把这些图片保存下载,或者用户用来做桌面壁纸,或者用来做设计的素材. 我们最常规的做法就是通过鼠标右键,选择另存为.但有些图片鼠标右键的 ...

  7. python简单爬虫代码-一则python3的简单爬虫代码

    不得不说python的上手非常简单.在网上找了一下,大都是python2的帖子,于是随手写了个python3的.代码非常简单就不解释了,直接贴代码. 代码如下: #test rdp import ur ...

  8. 《跟我一起学爬虫系列》3-一个简单爬虫示例

    本文抓取页面为http://news.baidu.com/,首先介绍下无关紧要但有必要了解的,然后代码说明 robots.txt Robots协议全称是网络爬虫排除标准,网站通过Robots协议定义网 ...

  9. pythonurllib登录微博账号_简单爬虫实现登录新浪微博(python2.7)

    因为图论作业,所以要写一个爬虫,就开始学python.接触python开始,就觉得这个语言非常舒服,不需要定义变量,不需要分号,非常简洁. 下面就聊聊,我写爬虫的经历.上网搜了一下爬虫的代码,发现简单 ...

  10. 【python爬虫学习篇】初识网络爬虫以及了解Web前端

    目录 1,初识爬虫 1.1,网络爬虫概述 1.2,爬虫的分类 1.3,网络爬虫的基本原理 1.4,搭建开发环境 2,了解web前端 2.1,HTTP基本原理 2.1.1HTTP协议 2.1.2,Web ...

最新文章

  1. 用矩阵内积的办法构造迭代次数受控的神经网络1:0.6:0.1=4:3:2
  2. Django基础一之web框架的本质
  3. python gps模块_一步一步使用uPyCraft学习MicroPython之GPS记录器
  4. 小女也爱c#(3)--俄罗斯方块练习数组
  5. mysql函数使用场景_mysql的函数和存储过程的比较,以及在实际场景中的使用案例...
  6. 字符串逆序(信息学奥赛一本通-T1162)
  7. 单体预聚合的目的是什么_第七章 配位聚合
  8. SPARK学习之 --- eclipse / sbt / scala 配置
  9. 虚幻开放日2017ppt
  10. 信号与系统公式笔记(8)——拉普拉斯变换
  11. python异常处理时所使用的保留字_【2020年12月计算机二级Python语言考试冲刺题(二)】- 环球网校...
  12. 第一篇文献:谈大数据时代的云控制摄影测量 ——张祖勋院士
  13. AJP:有和没有内化性精神障碍的受虐女孩情绪回路延迟成熟的差异性
  14. tiny linux u盘_多系统U盘启动盘制作工具(YUMI)下载-多系统U盘启动盘制作工具(YUMI)PC版下载v2.0.7.6...
  15. 多搜 - 多个网站一起搜 (舆情监控版)
  16. Tensorflow中的多层感知器学习
  17. Python+Flask
  18. keepalived脑裂现象
  19. 几种常见的软件团队模式优缺点总结
  20. Bin Packing Problem

热门文章

  1. 分析Android长按电源键事件并定制长按电源dialog
  2. ubuntu 下的 dnw 下载安装和使用
  3. 深度强化学习中的对抗攻击和防御
  4. 从木桶理论看自身职业生涯规划
  5. 硅谷中那些潜在的颠覆世界的力量
  6. 【WiFi 6E】WiFi 6E信道分布
  7. 饥荒服务器文档,建立饥荒服务器
  8. centos安装图形化界面及vnc-server连接
  9. R语言使用qlnorm函数生成对数正态分布分位数数据、使用plot函数可视化对数正态分布分位数数据(logarithmic normal distribution)
  10. windows黑客编程系列(六):进程遍历之查询系统是否运行杀软