Java爬虫

爬虫

网络爬虫,又被称为网页蜘蛛,网络机器人,是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本,另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。

大部分爬虫都是按发送请求—>获得页面—>解析页面—>抽取并储存内容这样的流程来进行,这其实也是模拟了我们使用浏览器获取网页信息的过程。

从功能上来讲,爬虫一般分为数据采集、处理、存储三个部分。爬虫从一个或若干个初始 网页的URL开始。获取初始网页上的URL,在抓取网页的过程中,不断从当前页面上抽取新的URL放入队列,直到满足系统的一定停止条件。

简单来讲,爬虫就是一个探测机器,它的基本操作就是模拟人的行为去各个网站溜达,点点按钮,查查数据,或者把看到的信息背回来。就像一只虫子在一幢楼里不知疲倦地爬来爬去。

Java爬虫和Python爬虫的对比

  • Java实现网络爬虫的代码要比Python多很多,而且实现相对复杂一些。
  • Java对于爬虫的相关库也有,但是没有Python那么多。

Java实现爬虫的技术

  1. 底层实现 HttpClient + Jsoup

  2. 开源框架 Webmagic

爬虫可以做什么

  • 可以实现搜索引擎
  • 大数据时代,可以让我们获取更多的数据源。
  • 快速填充测试和运营数据
  • 为人工智能提供训练数据集

网络爬虫的流程

他的主要工作就是跟据指定的 url地址 去发送请求,获得响应,然后解析响应,一方面从响应中查找出想要查找的数据,另一方面从响应中解析出新的 URL路径,然后继续访问,继续解析;继续查找需要的数据和继续解析出新的 URL路径

Request和Response

  • 浏览器发送消息给该网络所在的服务器,这个过程叫做HTTP Request。

  • 服务器收到浏览器发送的消息后,能够根据浏览器发送消息的内容,做出相应处理,然后把消息传给浏览器,这个过程叫做HTTP Response。

  • 浏览器收到服务器的Response信息后,会对信息进行相应处理,然后展示。

Request中包含:

可以分为4部分内容:请求方法(Request Method)、请求的网址(Request URL)、请求头(Request Headers)、请求体(Request Body)

1.请求方法 Request Method

常见的请求方法有两种:GET 和 POST。在浏览器中直接输入URL并回车,这便发起了一个GET 请求,请求的参数会直接包含到URL里。

方法 描述
GET 请求页面,并返回页面内筒
HEAD 类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报头
POST 大多数用于提交表单或上传文件,数据包含在请求体中
PUT 从客户端向服务器传送的数据取代指定文档中的内容
DELETE 请求服务器删除指定的页面
CONNECT 把服务器当做跳板,让服务器代替客户端访问其他网页
OPTIONS 允许客户端查看服务器的性能
TRACE 回显服务器收到的请求,主要用于测试或诊断

2. 请求的网址

请求URL即统一资源定位符,它可以唯一确定我们想请求的资源。比如一个网页文档、一张图片、一个视频等都可以用URL来确定。

3.请求头

以键值对的形式,请求的一些配置信息告诉服务器,让服务器判断这些配置信息解析。

请求头 描述
Accept 请求报头域,用于指定客户端可接受哪些类型的信息
Accept - Language 指定客户端可接受的语言类型。
Accept-Encoding 指定客户端可接受的内容编码。
Host 用于指定请求支援的主机IP和端口号,其内容为请求URL的原始服务器或网关的位置
Cookie 是网站为了辨别用户进行会话跟踪而存储在用户本地的数据。它的主要功能是维持当前访会话
Content-Type 也叫互联网媒体类型(Internet Media Type)或者MIME类型,在HTTP协议消息头中,它用来表示具体请求中的媒体类型信息
Content-Type 提交数据的方式
application/x-www-form-urlencoded 表单数据
multipart/from-data 表单文件上传
application/json 序列化JSON数据
text/xml XML数据

Response中包含:

可以分为三部分:响应状态码(Response Status Code)、响应头(Response Headers)、响应体(Response Body)

1.响应状态码

响应状态码表示服务器的响应状态,如200代表服务器正常响应,404代表页面未找到,500代表服务器内部发生错误。在爬虫中,我们可以根据状态码来判断服务器响应状态,如状态码为200,则证明成功返回数据,在进行进一步的处理,否则直接忽略。

状态码分类 分类描述
100~199 信息,服务器收到请求,需要请求者继续执行操作
200~299 成功,操作被成功接收并处理
300~399 重定向,需要进一步的操作以完成请求
400~499 客户端错误,请求包含语法错误或无法完成请求
500~599 服务器错误,服务器在处理请求的过程中发生错误

2.响应头

响应头包含了服务器对请求的应答信息,如Content-Type、Server、Set-Cookie等。

响应头 描述
Date 标识响应产生的时间
Last-Modified 指定资源的最后修改时间。
Content-Encoding 指定响应内容的编码
Server 包含服务器的信息,比如名称、版本号等。
Content-Type 文档类型,指定返回的数据类型是什么,如text/html代表返回HTML文档,application/x-javascript则代表返回JavaScript文件,image/jpeg则代表返回图片。
Set-Cookie 设置Cookies。响应头中的Set-Cookie 告诉浏览器需要将此内容放在Cookies中,下次请求携带Cookies请求。
Expires 指定响应的过期时间,可以使代理服务器或浏览器将加载的内容更新到缓存中。如果再次访问时,就可以直接从缓存中加载,降低服务器负载,缩短加载时间。

3.响应体

最重要的当属响应体的内容了。响应的正文数据都在响应体中,比如请求网页时,它的响应体就是网页的HTML代码;请求一张图片时,它的响应体就是图片的二进制数据。我们走爬虫请求网页后,要解析的内容就是响应体。

HttpClient抓取数据

pom.xml

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>

Get方式

public class Test {
    public static void main(String[] args) throws IOException {
        // 打开浏览器,创建 HttpClient 对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        
        // 输入网址
        HttpGet httpGet = new HttpGet("https://www.baidu.com/");
        
        /* 带参数的方式
        	URIBuilder uriBuilder = new URIBuilder("https://www.baidu.com/");
        	uriBuilder.setParameter("key","value");
        	HttpGet httpGet = new HttpGet(uriBuilder.build());
        */
        
        // 配置请求信息
        RequestConfig build = RequestConfig.custom()
                .setConnectTimeout(1000)            // 创建连接的最长时间,单位毫秒
                .setConnectionRequestTimeout(500)   // 获取连接的最长时间,单位毫秒
                .setSocketTimeout(10000)            // 数据传输的最长时间,单位毫秒
                .build();
        // 给请求设置请求信息
        httpGet.setConfig(build);
        
        // 使用 HttpClient 对象发起请求
        CloseableHttpResponse response = httpClient.execute(httpGet);
        // 解析响应,获取数据
        if(response.getStatusLine().getStatusCode()==200){      // 判断状态码是否是200
            HttpEntity httpEntity = response.getEntity();       // 响应体
            String string = EntityUtils.toString(httpEntity, "utf8");	// 响应内容
            System.out.println(string);
        }
        // 关闭资源
        response.close();
        httpClient.close();
    }
}

Post方式

public class Test {
    public static void main(String[] args) throws IOException, URISyntaxException {
        // 打开浏览器,创建 HttpClient 对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 输入网址
        HttpPost httpPost = new HttpPost("https://www.baidu.com/");

        /* 带参数的方式
        	// 声明 List 集合,封装表单中的参数
       	 	List<NameValuePair> params = new ArrayList<NameValuePair>();
        	params.add(new BasicNameValuePair("key","value"));
        	// 创建表单的 Entity 对象
        	UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(params,"utf8");
        	// 设置表单的 Entity 对象到 Post请求中
        	httpPost.setEntity(formEntity);
        */

        // 使用 HttpClient 对象发起请求
        CloseableHttpResponse response = httpClient.execute(httpPost);
        // 解析响应,获取数据
        if(response.getStatusLine().getStatusCode()==200){      // 判断状态码是否是200
            HttpEntity httpEntity = response.getEntity();       // 响应体
            String string = EntityUtils.toString(httpEntity, "utf8");
            System.out.println(string);
        }
        response.close();
        httpClient.close();
    }
}

连接池

public class Test {
    public static void main(String[] args) throws IOException {
        // 创建连接池管理器
        PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager();

        // 设置最大连接数
        manager.setMaxTotal(50);
        // 设置每个主机的最大连接数
        manager.setDefaultMaxPerRoute(5);

        // 使用连接池管理器发送请求(不需要每次创建新的HttpClient,而是从连接池中获取HttpClient对象)
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(manager).build();
        HttpGet httpGet = new HttpGet("https://www.baidu.com/");

        // 使用 HttpClient 对象发起请求
        CloseableHttpResponse response = httpClient.execute(httpGet);
        // 解析响应,获取数据
        if(response.getStatusLine().getStatusCode()==200){      // 判断状态码是否是200
            HttpEntity httpEntity = response.getEntity();       // 响应体
            String string = EntityUtils.toString(httpEntity, "utf8");
            System.out.println(string);
        }
        // 这里不能关闭 HttpClient,应该是由 连接池管理 HttpClient 的
        response.close();
    }
}

Jsoup解析数据

Jsoup 是一款 Java 的 HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS以及类似于 jQuery 的操作方法来取出和操作数据。这种方式的局限性在于,通过 Jsoup 爬虫只适合爬静态网页,所以只能爬当前页面的信息。

虽然使用 Jsoup 可以代替 HttpClient 直接发起请求解析数据,但是往往不会这样用,因为实际开发过程中,需要使用到多线程,连接池,代理等方式,而 Jsoup 对这些的支持并不是很好,所以我们一般把 Jsoup 仅仅作为 Html 解析工具使用。

pom.xml

<dependency>
	<groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.7.3</version>
</dependency>

基础代码

public class Test {
    // 准备抓取的目标地址
    private static final String url = "https://www.cnblogs.com";

    public static void main(String[] args) {
        try {
            // 链接到目标地址,先获得的是整个页面的 html 标签页面,该方法只支持http或https协议
            Connection connect = Jsoup.connect(url);
            // 设置超时时间,并且以 Get 请求方式请求服务器。可以获取到整个 <html> 中的内容
            Document doc = connect.timeout(6000).get();

            // 通过元素的id获取html中的特定元素
            Element elementById = doc.getElementById("user_info");
            System.out.println(elementById.text());     // 获取文本数据
            System.out.println(elementById.html());     // 获取html数据

            //可以通过元素的标签获取html中的特定元素
            Elements title = doc.select("title");
            System.out.println(title.text());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

解析一个HTML字符串

public class Test {
    public static void main(String[] args) {
        String html = "<html><div>将军不下马,各自奔前程</div></html>";
        // 解析一个HTML字符串
        Document parse = Jsoup.parse(html);
        System.out.println(parse);

        /* 解析文件 
        	Document doc = Jsoup.parse(new File("D:\\index.html"),"utf8");
        */
        
        String html1 = "<p>技不外传,海不露底</p>";
        // 会创建一个空壳的文档,并将插入解析过的HTML到body元素中
        Document doc = Jsoup.parseBodyFragment(html1);
        Element body = doc.body();      // 与 doc.getElementsByTag("body") 相同
        System.out.println(body);
    }
}

获取页面中所有的图片

public class Test {
    // 准备抓取的目标地址
    private static final String url = "https://cn.bing.com/images/trending?		form=SBIRDI#enterInsights";

    public static void main(String[] args) throws Exception {
        // 链接到目标地址
        Connection connect = Jsoup.connect(url);
        // 设置超时时间,并以get请求方式请求服务器
        Document document = connect.timeout(6000).ignoreContentType(true).get();

        // 获取所有图片链接
        Elements imgtag = document.getElementsByTag("img");
        for (int i = 0; i < imgtag.size(); i++) {
        	System.out.println(imgtag.get(i).attr("src"));
        }
    }
}

Jsoup类常用方法

parse()					将输入的HTML解析为一个新的文档
parseBodyFragment()		创建一个空壳的文档,并插入解析过的HTML到body元素中
connect()				从一个URL加载一个Document

Connection接口常用方法

get()				以Get方式执行请求,并解析结果
post()				以Post方式执行请求,并解析结果
data()				key-value,添加请求参数,一个请求可能有多个相同名称的值
userAgent()			设置请求用户代理标题
cookie()			设置要在请求中发送的cookie
timeout()			设置请求超时

Document类常用方法

title()						获取文档标题元素的字符串内容
id()						获取id属性的值
className()					获取class属性的值
classNames()				获取class属性的值,返回Set集合
body()						获取body元素的所有子元素
attr()						获取/设置指定的属性
text()						获取/设置文本内容
html()						获取/设置html元素内容
val()						获取/设置value属性的内容
first()						获取多个里面的第一个的内容
outerHtml()					获取元素外html内容
attributes()				获取元素中所有的属性,返回 Attitudes
select()					通过选择器查找元素
getElementById()			根据id选择器来查找元素
getElementsByTag()			根据标签名来查找元素
getElementsByClass()		根据class选择器来查找元素
getElementsAttribute()		根据属性获取元素
getElementsByAttribute()	根据属性名和属性值来获取元素

Select选择器的用法

1.通过标签查找元素,例如:a
2.通过标签的命名空间查找元素,例如:使用 fb|name 语法来查找 <fb:name> 元素
3.通过id查找元素,例如:#logo
4.通过class查找元素,例如:.mast
5.通过属性查找元素,例如:[href]

WebMagic

WebMagic是一个开源的 Java爬虫框架,目标是简化爬虫的开发流程,让开发者专注于逻辑功能的开发。WebMagic的核心非常简单,但是覆盖爬虫的整个流程,也是很好的学习爬虫开发的材料。

WebMagic 的结构分为 Downloader、PageProcessor、Scheduler、Pipeline 四大组件,并由 Spider 将它们彼此组织起来。这四个组件对应爬虫生命周期中的下载、处理、管理和持久化等功能。WebMagic 的设计参考了 Scapy,但是实现方式更 Java 化一些。

而 Spider 则将这几个组件组织起来,让它们可以互相交互,流程化的执行,可以认为 Spider 是一个大的容器,它也是 WebMagic 逻辑的核心。

四大组件

  • Downloader

    Downloader 负责从互联网上下载页面,以便后续处理,WebMagic默认使用了 AppcheHttpClient 作为下载工具。

  • PageProcessor

    PageProcessor 负责解析页面,抽取有用信息,以及发现新的链接。WebMagic 使用 Jsoup 作为 HTML 解析工具,并基于开发了解析 XPath 的工具 Xsoup。PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。

  • Scheduler

    Scheduler 负责管理待抓取的 URL,以及一些去重的工作,WebMagic 默认提供了 JDK 的内存队列来管理 URL,并用集合来进行去重。也支持使用 Redis 进行分布式管理。

  • Pipeline

    Pipeline 负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic 默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。

用于数据流转的对象

  • Request

    Request 是对 URL 地址的一层封装,一个 Request 对应一个 URL 地址。它是 PageProcessor 与 Downloader 交互的载体,也是 PageProcessor 控制 Downloader 唯一方式。

    除了 URL 本身外,它还包含一个 Key-Value 结构的字段 extra。你可以在 extra 中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。例如附加上一个页面的一些信息等。

  • Page

    Page 代表了从 Downloader 下载到的一个页面,可能是 HTML,也可能是 JSON 或者其他文本格式的内容。

    Page 是 WebMagic 抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。

  • ResultItems

    ResultItems 相当于一个 Map,它保存 PageProcessor 处理的结果,供 Pipeline 使用。它的 API 与 Map 很类似,值得注意的是它有一个字段 skip,若设置为 true,则不应被 Pipeline 处理。

pom.xml

<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>webmagic-core</artifactId>
    <version>0.7.4</version>
</dependency>
<!-- 注意:如果是0.7.3版本对 SSL 的并不完全,WebMagic 默认的 HttpClient 只会用 TLSv1 去请求,如果是直接从 Maven 中央仓库下载依赖,在爬取只支持 SSL v1.2 的网站会有 SSL 的异常抛出,javax.net.ssl.SSLException的错误 -->
<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>webmagic-extension</artifactId>
    <version>0.7.4</version>
</dependency>

入门案例

// 实现解析的组件
public class JobProcessor implements PageProcessor {
    // 解析页面
    public void process(Page page) {
        // 解析返回的数据page,并将解析的结果放到 ResultItems 中。key随意取名,通过key获取解析的数据
        page.putField("key",page.getHtml().css("div.mt h2").all());
        
        // 使用XPath进行解析
        page.putField("key1",page.getHtml().xpath("//div[@id=news_div]/ul/li/div/a"));
        
        // 使用正则表达式进行解析
		page.putField("key2",page.getHtml().css("div#news_div a").regex(".*江苏.*").all());
        
        // 获取链接
  		page.addTargetRequests(page.getHtml().css("div#news_div").links().
        regex(".*9$").all());
        page.putField("url",page.getHtml().css("div.mt h1").all());
    }

    private Site site = Site.me()
            .setCharset("utf8")         // 设置编码
            .setTimeOut(10000)          // 设置超时时间,单位毫秒
            .setRetrySleepTime(3000)	// 设置重试的间隔时间
            .setSleepTime(3);           // 设置重试次数
    
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {
        Spider.create(new JobProcessor())
                .addUrl("https://www.jd.com/moreSubject.aspx")	// 要解析的url地址
                .addPipeline(new FilePipeline("D:\\result"))    // 指定解析的内容存放的位置
                .thread(5)      // 表示 5个线程同时执行
             	// 设置布隆去重过滤器,指定最多对 1000万数据进行去重操作,需要加 guava依赖
                .setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(10000000)))
                .run();         // 执行爬虫
    }
}

实现 PageProcessor 解析页面

  • 抽取元素 Selectable

    WebMagic 里面主要使用了三种抽取技术:XPath、正则表达式、CSS选择器。

  • 抽取元素 API

    Selectable 相关的抽取元素链式 API 是 WebMagic 的一个核心功能。使用 Selectable 接口,可以直接完成页面元素的链式抽取,也无需去关系抽取的细节。

    方法 说明 示例
    xpath(String xpath) 使用 XPath 选择 html.xpath("//div[@class='title']")
    $(String selector) 使用 Css 选择器选择 html.$("div.title")
    $(String selector, String attr) 使用 CSS 选择器选择 html.$("div.title", "text")
    css(String selector) 功能同$(),使用 Css 选择器选择 html.css("div.title")
    links() 选择所有链接 html.links()
    regex(String regex) 使用正则表达式抽取 html.regex("(.*?)")
    regex(String regex, int group) 使用正则表达式抽取,并指定捕获组 html.regex("\
    \(.\*?)\", 1)
    replace(String regex, String replacement) 替换内容 html.replace("\", "")
  • 获取结果 API

    我们知道,一条抽取规则,无论是 XPath、CSS、正则。总有可能抽取到多条元素。WebMagic对这些进行了统一,可以通过不同的 API 获取到一个或者多个元素。

    方法 说明 示例
    get() 返回一条 Sting 类型的结果 String link = html.links().get();
    toString() 同 get(),返回一条 String 类型的结果 String link = html.linke().toString();
    all() 返回所有抽取结果 Sting links = html.linke().all()
    match() 是否有匹配结果 if(html.links().match)

使用 Pipeline 保存结果

WebMagic 用于保存结果的组件叫做 Pipeline。如果不使用的话,默认是通过控制台输出结果的,这件事也是通过一个内置的 Pipeline 完成的,它叫做 ConsolePipeline。

说明 备注
ConsolePipeline 输出结果到控制台 抽取结果需要实现 toString 方法
FilePipeline 保存结果到文件 抽取结果需要实现 toString 方法
JsonFilePipeline JSON 格式保存结果到文件
ConsolePageModelPipeline (注解模式) 输出结果到控制台
FilePageModelPipeline (注解模式) 保存结果到文件
JsonFilePageModelPipeline (注解模式) JSON格式保存结果到文件 想持久化的字段需要有 getter 方法

爬虫的配置、启动、终止

Spider

Spider 是爬虫启动的入口。在启动爬虫之前,我们需要使用一个 PageProcessor 创建一个 Spider 对象,然后使用 run() 进行启动。

同时 Spider 的其他组件(Downloader、Scheduler、Pipeline)都可以通过 set 方法来进行设置。

方法 说明 示例
create(PageProcessor) 创建 Spider Spider.create(new GithubRepoProcess())
addUrl(String...) 添加初始的 URL spider.addUrl("http://webmagic.io/docs/")
thread(n) 开启 n 个线程 spider.thread(5)
run() 启动,会阻塞当前线程执行 spider.run()
start()/runAsync() 异步启动,当前线程继续执行 spider.start()
stop() 停止爬虫 spider.stop()
addPipeline(Pipeline) 添加一个 Pipeline,一个 Spider 可以有多个 Pipeline spider.addPipeline(new ConsolePipeline())
setScheduler(Scheduler) 设置 Scheduler,一个 Spider 只能有一个 Scheduler spider.setScheduler(new RedisScheduler())
setDownloader(Downloader) 设置 Downloader,一个 Spider 只能有一个 Downloader spider.setDownloader(new SeleniumDownloader())
get(String) 同步调用,并直接取得结果 ResultItems result = spider.get("http://webmagic.io/docs/")
getAll(String...) 同步调用,并直接取得一堆结果 List results = spider.getAll("http//webmagic.io/docs/", "http://webmagic.io/xxx")

Site

Site.me() 可以对爬虫进行一些配置,包括编码、抓取间隔、超时时间、重试次数等。

方法 说明 示例
setCharset(String) 设置编码 site.setCharset("utf-8")
setUserAgent(String) 设置 UserAgent site.setUserAgent("Spider")
setTimeOut(int) 设置超时时间,单位毫秒 site.setTimeOut(3000)
setRetryTimes(int) 设置重试次数 site.setRetryTimes(3)
setCycleRetryTimes(int) 设置循环重试次数 site.setCycleRetryTimes(3)
addCookie(String, String) 添加一条 cookie site.addCookie("dotcomt_user","code4craft")
setDomain(String) 设置域名,序设置域名后,addCookid 才可生效 site.setDomain("github.com")
addHeader(String, String) 添加一条 addHeader site.addHeader("Referer", “https://github.com”)
setHttpProxy(HttpHost) 设置 Http 代理 site.setHttpProxy(new HttpHost("127.0.0.1", 8080))

Scheduler 组件

Scheduler 是 WebMagic 中进行 URL 管理的组件。一般来说,Scheduler 包含两个作用:

  • 对待抓取的 URL 队列进行关联
  • 对已抓取的 URL 进行去重
说明 备注
DuplicateRemovedScheduler 抽象基类,提供一些模板方法 继承它可以实现自己的功能
QueueScheduler 使用内存队列保存待抓取 URL
PriorityScheduler 使用带有优先级的内存队列保存待抓取 URL 耗费内存较 QueueScheduler 更大,但是当设置了 request.Priority 之后,只能使用 PriorityScheduler 才可以使优先级生效
FileCacheQueueScheduler 使用文件保存抓取 URL,可以在关闭程序并下次启动时,从之前抓取到的URL 继续抓取 需指定路径,会建立 .urls.txt 和 .cursor.txt 两个文件
RedisScheduler 使用 Redis 保存抓取队列,可进行多台机器同时合作抓取 需要安装并启动 redis

去重部分被单独抽象成了一个接口:DuplicateRemover,从而可以为同一个 Scheduler 选择不同的去重方式,以适应不同的需要。可以使用 spider.getScheduler() 来查看使用的是那种去重方式。RedisScheduler 是使用 Redis 的 set 进行去重,其他的 Scheduler 默认都使用 HashSetDuplicateRemover 来进行去重。

说明
HashSetDuplicateRemover 使用 HashSet 来进行去重,占用内存较大
BloomFilterDuplicateRemover 使用 BloomFilter 来进行去重,占用内存较小,但是肯能漏抓页面

如果要使用 BloomFilter,必须要加入以下依赖:

<dependency>
	<groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>16.0.1</version>
</dependency>

三种去重方式

  • HashSet

    使用 Java 中的 HashSet 不能重复的特点去重。优点是容易理解。使用方便,缺点在于占用内存大,性能较低。

  • Redis 去重

    使用 Redis 的 set 进行去重。优点是速度快(Redis 本身速度就很快),而且去重不会占用爬虫服务器的资源,可以处理更大数据量的数据爬去。缺点是需要准备 Redis 服务器,增加开发和使用成本。

  • 布隆过滤器

    使用布隆过滤器也可以实现去重,优点是占用的内存要比使用 HashSet 要小的多,也适合大量数据的去重操作。缺点是有误判的可能,没有重复可能会判定重复,但是重复数据一定会判定重复。

保存到数据库

// 必须实现 Pipeline 接口
public class SpringDataPipeline implements Pipeline {
    public void process(ResultItems resultItems, Task task) {
    	实体类 key = resultItems.get("key"); // 通过 key 获取到数据
		// 调用业务方法,将解析的数据保存到数据库中
    }
}
public static void main(String[] args) {
    Spider.create(new JobProcessor())
            .addUrl("https://www.jd.com/moreSubject.aspx")     // 要解析的url地址  
            .thread(5)      // 表示 5个线程同时执行
            .addPipeline(new SpringDataPipeline())// 指定解析的内容存放的位置(保存到数据库)
            .run();         // 执行爬虫
}
/* 页面经常会有分页,如果只是简单的爬虫的话,只能获取到第一页的。我们可以获取第二页、第三页的 url地址,把    他加到任务队列中就可以了。如果是先是一个首页,然后里面有详情页,也可以通过首页获取到详情页的 url地址,	然后将这个 url地址加入任务队列即可。
*/

// 把获取到的 url地址放到任务队列中
page.addTargetRequest("url路径");

网页去重

我们可以通过 Scheduler 对已经爬取的 url地址进行去重,避免同样的 url下载多次。其实不光 url需要去重,我们对下载的内容页需要去重。

去重方式

  • 指纹码对比

    对常见的去重方案是生成文档的指纹门,例如对一篇文章进行 DM5 加密生成一个字符串,我们可以认为这是这个文章的指纹码,再和其他的文章指纹码对比,一致则说明文章重复。但是这种方式是完全一致则是重复的,如果文章只是多了几个标点符号,那么便会认为是不一致的,这种方式并不合理。

  • BloomFilter

    这种方式是对文章进行计算得到一个数,再进行对比,缺点是和方式一一样,如果只有一点点不一样,也会认为不重复,这种方式不合理。

  • KMP 算法

    KMP 算法是一种改进的字符串匹配算法。KMP 算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。能够找到两个文章有哪些是一样的,哪些是不一样的。

    这种方式能够解决前面两个方式的问题,但是它的时间复杂度太高了,不适合大数据量的重复对比。

  • SimHash算法

    Google 的 simhash 算法产生的签名,可以满足我们的条件,这个算法并不算深奥,有容易理解,这种算法也是目前 Google 搜索引擎目前所使用的网页去重算法。

  • 其他

    最长公共子串、后缀数组、字典树、DFA,但是这些方式的时间复杂度并不适合数据量较大的工业应用场景。

SimHash

SimHash 是有 Charikar 在 2002 年提出来的,为了便于理解尽量不使用数据公式,分为以下几步:

  • 分词,把需要判断文本分词形成这个文章的特征单词。
  • hash,通过 hash 算法把每个词变成 hash值。
  • 加权,通过产生的 hash值,需要按照单词的权重形成加权数字串。
  • 合并,把上面各个单词算出来的序列值累加,变成只有一个序列串。
  • 降维,把算出来的序列串变成 01串,形成最终的 simhash 签名。

例如:有一篇文章,会先对这篇文章进行分词,然后计算出每一个分词的 hash值,例如:100101,101011,...,然后对这些 hash值进行加权,然后把这些加权的分词再合并到一起,变成一个字符串,然后再进行降维,形成最终的 simhash 签名。

我们把库里面的 文本都转换为 simhash 签名,并转换为 long 类型存储,空间大大减少,然后还需要计算 simhash的相似度,可以通过 海明距离(Hamming distance)来计算出两个 simhash 到底相似不相似,两个 simhash对应二进制(01串)取值不同的数量称为这两个 simhash 的海明距离。

环境

这个是不需要我们手写的,在 GitHub 上已经有人写好了,网址:https://github.com/CreekLou/simhash.git

下载后解压,使用 idea 打开,然后再使用 install 命令将他添加到我们本地的 maven 仓库即可。

使用 install 命令后报错:
Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project simhasher: There are test failures.

Please refer to D:\Program Files\IDEA\idea\simhash-master\target\surefire-reports for the individual test results.

解决办法,在 pom.xml 中添加:
<build>
   <plugins>
      <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
         <version>2.4.2</version>
         <configuration>
            <skipTests>true</skipTests>
         </configuration>
      </plugin>
   </plugins>
</build>


这块还没有结束,我后面加入这个依赖测试的时候,还有坑在里面,恶心的版本问题!
在使用时报错:
Exception in thread "main" java.lang.AbstractMethodError: org.apache.lucene.analysis.Analyzer.tokenStream(Ljava/lang/String;Ljava/io/Reader;)Lorg/apache/lucene/analysis/TokenStream;

修改 pom.xml 中的依赖版本
<dependency>
	<groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
	<!-- <version>3.6.1</version> -->
    <version>4.10.3</version>
</dependency>


还需要修改 WordsSegment 类中的 getCutWords() 方法,:
TokenStream ts = analyzer.tokenStream("searchValue", r);
ts.reset();			// 添加的内容

否则会报错:
Exception in thread "main" java.lang.IllegalStateException: TokenStream contract violation: reset()/close() call missing, reset() called multiple times, or subclass does not call super.reset(). Please see Javadocs of TokenStream class for more information about the correct consuming workflow.

使用

在项目的 pom.xml 中添加 simhash 依赖

<dependency>
    <groupId>com.lou</groupId>
    <artifactId>simhasher</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
public class Test {
    public static void main(String[] args) throws IOException {
        // 分词
        List<String> list = WordsSegment.getCutWords("我可爱的宝贝");
        System.out.println(list.toString());

        SimHasher simHasher = new SimHasher("我爱我的祖国");
        SimHasher simHasher1 = new SimHasher("我爱我的妈妈");
        System.out.println(simHasher.getSignature());       // 获取签名
        System.out.println(simHasher.getHash());            // 获取 hash值

		// 比较两者的 hash距离
        System.out.println(simHasher.getHashDistance(simHasher1.getHash()));    
        // 比较两者的海明距离
        System.out.println(simHasher.getHammingDistance(simHasher1.getSignature()));   
    }
}

代理

有些网站不允许爬虫进行数据爬取,因为会加大服务器的压力,其中一种最有效的方式是通过 ip+时间进行鉴别,因为正常人不可能短时间开启太多的页面,发起太多的请求。

我们使用的 WebMagic 可以很方便的设置爬取数据的时间,但是这样会大大降低我们爬取数据的效率,如果不小心 ip 被禁了,会让我们无法爬取数据,那么我们就有必要使用代理服务器来爬取数据。

代理服务器

也称网络代理,是一种特殊的网络服务,允许一个网络终端(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。

提供代理服务的电脑系统或其他类型的网络终端称之为代理服务器。一个完整的代理请求过程为:客户端首先与代理服务器创建连接,接着根据代理服务器所使用的代理协议,请求对目标服务器创建连接、或者获得目标服务器的指定资源。

https://proxy.mimvp.com/free.php 米扑代理

http://www.xicidaili.com/ 西刺免费代理IP

使用代理

WebMagic 使用的代理 APIProxyProvider。因为相对于 Site 的设置,ProxyProvider 定位更多是一个 组件,所以代理不再从 Site 设置,而是有 HttpClientDownloader 设置。

API 说明
HttpClientDownloader.setProxyProvider(ProxyProvider proxyProvider) 设置代理

ProxyProvider 有一个默认实现:SimpleProxyProvider。它是一个基于简单 Round-Robbin 的,没有失败检查的 ProxyProvider。可以配置任意个候选代理,每次会按顺序挑选一个代理使用。它适合用在自己搭建的比较稳定的代理的场景。

public class ProxyTest implements PageProcessor {
    public static void main(String[] args) {
        // 创建下载器 Downloader
        HttpClientDownloader httpClientDownloader = new HttpClientDownloader();

        // 给下载器设置代理服务器信息,使用 https://proxy.mimvp.com/freeopen 的这个代理服务器
        httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(new Proxy("106.15.25036",8080)));

        Spider.create(new ProxyTest())
                .addUrl("https://qifu.baidu.com/?activeKey=SEARCH_IP")
                .setDownloader(httpClientDownloader)        // 设置下载器
                .run();
    }

    public void process(Page page) {
        System.out.println(page.getHtml().toString());
    }

    public Site getSite() {
        return Site.me();
    }
}

WebDriver

Selenium-WebDriver 是一个支持浏览器自动化的工具。WebDriver 用驱动(driver)去控制不同的浏览器。目前驱动支持的浏览器包括 Firefox,Chrome,Safari 和 IE。微软正在为 Mcirosoft Edge 研发新的 driver。Firefox 的 driver 是内置的,所以 Firefox 是多数项目进行自动化测试的首选。

我们的代码运行起来是一个进程,里面调用 Selenium WebDriver 的库 和 各个浏览器的驱动进程 进行交互,传递 Selenium 命令给它们,并且获取命令执行的结果,返回给我们的代码进行处理。

Selenium WebDriver 目前包括两个版本 Selenium 2Selenium 3。这两个版本从开发代码调用接口上来看,几乎没什么区别。区别在于库的实现和 web driver的实现。Selenium2Selenium组织帮各种浏览器写web driver的,而Selenium 3里面的web driver是由各个浏览器厂商(Apple,Google,Microsoft,Mozilla)自己提供的。所以Selenium 3的自动化效率更高,成功率也更高。

WebDriver 能做什么?
1.WebDriver可是使浏览器自动化。WebDriver可以为我们打开URL与渲染出的页面进行交互:
	- 新建一个浏览器实例
	- 在浏览器中打开一个URL
	- 点击页面上的链接
	- 在字段中输入信息
	- 在页面中执行JavaScript
2.WebDriver不是一个测试REST APIs、SOAP APIs或数据库的工具。
3.它是一个自动化浏览器的工具。
4.因为 WebDriver本质上是一个库,我们可以将其与其他库一起使用,调用其他API或者对数据库进行操作。
5.通常WebDriver与其他库一起使用,访问数据库检查结果,使用REST库进行API调用,然后在WebDriver页面上检查结果。

下载网址:https://www.selenium.dev/downloads/

Chrom 的 Web Driver 驱动:http://npm.taobao.org/mirrors/chromedriver/

Firefox 的 Web Driver 驱动:https://github.com/mozilla/geckodriver/releases

Edge 的 Web Driver 驱动:https://developer.microsoft.com/en-us/micrsosft-edage/tools/webdriver

Safari 的 Web Driver 驱动:https://webkit.org/blog/6900/webdriver-support-in-safari-10/

版本问题

使用

将 selenium-java-3.141.59.zip 解压,然后将根目录下的 client-combine.jar、client-combined-sources.jar 以及 libs 目录下的 jar 导入到项目中。

// 运行后,程序会启动 Firefox浏览器,并自动进入百度页面
public class Test {
    public static void main(String[] args) {
        // 浏览器安装路径
        String path = "D:\\Program Files (x86)\\Tencent\\火狐\\firefox.exe";
        // 如果是火狐,使用:webdriver.firefox.bin。如果是谷歌,使用:webdriver.chrome.bin
        System.setProperty("webdriver.firefox.bin", path);

        // geckodriver.exe 位置
        String driverPath = "E:\\李某人\\软件\\其他软件\\Selenium WebDriver\\Firefox\\0.29.1\\geckodriver.exe";
        // 如果是火狐,使用:webdriver.gecko.driver。如果是谷歌,使用:webdriver.chrome.driver
        System.setProperty("webdriver.gecko.driver", driverPath);

        // 测试连接
        String url = "https://www.baidu.com/";

        // 启动 WebDriver,如果是谷歌的话,使用:new ChromeDriver();
        WebDriver firefoxDriver = new FirefoxDriver();

        // 浏览器跳转测试链接
        firefoxDriver.get(url);
        
        /* 显示等待
           // 设置等待时长,最长10s,如果在指定时间内没找到,那么抛出 Exception
           WebDriverWait webDriverWait = new WebDriverWait(firefoxDriver, 10);
           webDriverWait.until(ExpectedConditions.presenceOfElementLocated(By.id("id")));
        */

        // 获取整个页面内容
        System.out.println(firefoxDriver.getPageSource());
        
        // 通过 id选择器(id="kw")获取输入框中输入的字符
        firefoxDriver.findElement(By.id("kw")).sendKeys("菜鸟教程");

        // 通过 id选择器(id="su")的 click点击事件,会点击页面的这个按钮
        firefoxDriver.findElement(By.id("su")).click();
    }
}

下拉框

Select select = new Select(firefoxDriver.findElement(By.id("id")));
// 获取所有的下拉框内容
List<WebElement> options = select.getOptions();
for(WebElement o:options){
    System.out.println(o.getText());
}

// 获取选中的下拉框的内容
System.out.println(select.getFirstSelectedOption().getText());

// 获取选中的多个下拉框的内容
List<WebElement> allSelectedOptions = select.getAllSelectedOptions();
for(WebElement o:allSelectedOptions){
    System.out.println(o.getText());
}

By类的常用方法

方法 说明
By.id() id选择器
By.name() name选择器
By.tagName() 标签选择器
By.className() class选择器
By.lintText() 根据超链接的文字来定位元素
By.partialLinkText() 根据超链接的文字模糊匹配定位元素
By.xpath() 使用 XPath 来定位元素
By.cssSelector() 使用 cssSelector 来定位元素

WebElement接口的常用方法

方法 说明
sendKeys() 向输入框中传值
click() 单击事件
clear() 清除文本
submit() 提交表单
getSize() 获取输入框的尺寸,返回 Dimension
getText() 获取文本中的内容
isSelected() 是否是选中的
isDisplayed() 指定元素是否存在在页面上
isEnabled() 如果是可编辑状态,返回 true
getAttribute() 通过指定的属性名获取属性值
getCssValue() 通过指定的属性名获取对应的css值
getTagName() 获取标签名,例如:input
getRect() 获取元素的位置已经宽高
getLocation() 获取元素的位置,返回 Point

WebDriver 接口的常用方法

方法 说明
quit() 关闭整个浏览器的窗口
close() 关闭浏览器标签页
navigate().refresh() 刷新浏览器
navigate().forward() 前进
navigate().back() 后退
manage().timeouts().implicitlyWait(15, TimeUnit.SECONDS) 隐式等待,设置脚本在查找元素时的最大等待时间,如果超过了设置的时间,则抛出异常
manage().window().maximize() 发大浏览器窗口
switchTo().alert().accept() 弹窗,选择确认
switchTo().alert().dismiss() 弹窗,选择取消
getPageSource() 获取整个页面内容
findElement() 寻找元素
getCurrentUrl() 获取当前页的 URL路径
getTitle() 获取当前页的标题
getWindowHandle() 获取当前页的句柄
getWindowHandles() 获取所有也的句柄

XPath

XPath 即为 XML 路径语言,它是一种用来确定 XML (标准通用标记语言的子集) 文档中某部分位置的语言。XPath基于 XML 的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点,提供在数据结构树中找寻节点的能力。

XPath 说明
//input[@name] 获取所有 input 标签且属性名为 name的。
//input[@name='a'] 获取所有 input 标签且属性名为 name,name的值为 a的。
//title[@*] 获取所有 title 标签的,只要有属性的。
//* 获取文档中的所有元素
/div/* 获取 div 元素的所有子元素
posted @ 2021-06-26 23:43  显示昵称已被使用々  阅读(233)  评论(0)    收藏  举报