ZB-030-03确定算法和重构

确定算法

  • 为什么互联网称为“网”,爬虫被称为“爬虫”
    • 从一个节点出发,遍历所有节点
  • 算法:广度优先算法的一个变体
    • 非常建议自己手写相关算法
      • 广度优先/队列数据结构/JDK的队列实现
  • 如何扩展?
    • 慢慢的把烂代码,啰嗦的代码去掉
    • 假如我想要未来换数据库/上ES
    • 爬虫的通用化

开始实践

新开一个分支

1
git checkout -b basic-algorithm

先来一个死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Main {
public static void main(String[] args) throws IOException {
// 待处理的链接池
List<String> linkPool = new ArrayList<>();

// 已经处理的链接池
Set<String> processedLinks = new HashSet<>();
linkPool.add("https://sina.cn");

while(true){
if(linkPool.isEmpty()){
break;
}

String link = linkPool.get(0);
if(processedLinks.contains(link)){
continue;
}

// 过滤外链
if(!link.contains("sina.cn")){
// 这是我们不感兴趣的不管他
continue;
}else{
// 这是我们感兴趣的,我们只处理新浪站内的链接
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("https://sina.cn/");
try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getStatusLine());
HttpEntity entity1 = response1.getEntity();
System.out.println(EntityUtils.toString(entity1));
}
// 此时需要 jsoup
}

}

}
}

jsoup maven

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.12.1</version>
</dependency>

继续解析网页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.github.hcsp;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Main {
public static void main(String[] args) throws IOException {
// 待处理的链接池
List<String> linkPool = new ArrayList<>();

// 已经处理的链接池
Set<String> processedLinks = new HashSet<>();
linkPool.add("https://sina.cn");

while(true){
if(linkPool.isEmpty()){
break;
}

// String link = linkPool.get(linkPool.size() - 1);
// // ArrayList从底部删除更有效率
// linkPool.remove(linkPool.size() - 1);

// 简化为一行
String link = linkPool.remove(linkPool.size() - 1);

if(processedLinks.contains(link)){
continue;
}

// 过滤外链
if ( link.contains("news.sina.cn") || "https://sina.cn".equals(link) ) {
// 这是我们感兴趣的,我们只处理新浪站内的链接

System.out.println(link);
if(link.startsWith("//")){
link = "https:" + link;
System.out.println("处理后的link:"+link);
}

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(link);
httpGet.addHeader("User-Agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36");


try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getStatusLine());
HttpEntity entity1 = response1.getEntity();
String html = EntityUtils.toString(entity1);

Document doc = Jsoup.parse(html);

ArrayList<Element> links = doc.select("a");

for(Element aTag:links){
// 丢到链接池里
linkPool.add(aTag.attr("href"));
}

// 假如这是个新闻的详情页就存入数据库,否则,就什么都不做
/*
新闻详情有 标题/内容
*/
ArrayList<Element> articleTags = doc.select("article");
if(!articleTags.isEmpty()){
for (Element articleTag:articleTags){
String title = articleTags.get(0).child(0).text();
System.out.println(title);
}
}

processedLinks.add(link);
}
} else {
// 这是我们不感兴趣的不管他
continue;
}

}

}
}

重构

上面代码里有这样一个条件来筛选我们要的网址

1
2
3
4
5
6
// 过滤外链
if ( (link.contains("news.sina.cn") || "https://sina.cn".equals(link)) && !link.contains("passport.sina.cn") ) {
// 这是我们感兴趣的,我们只处理新浪站内的链接
...
}
`

通常情况下,这么多的条件下

  • 你会写注释,我们不反对写注释,
    • 注释可能会过时,有一天条件变了,但是注释你没改
    • 或者两年后代码不停的变更
    • 一个过时的注释,比不写注释还要糟糕,因为他给了你错误的信息
  • 最聪明的方法是 提取方法并给一个名字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static boolean isIntresetingLink(String link){
return (link.contains("news.sina.cn") || "https://sina.cn".equals(link)) && !link.contains("passport.sina.cn");
}

// 还可以继续简化
private static boolean isIntresetingLink(String link){
return (isNewsPage(link) || isIndexPage(link)) && isNotLoginPage(link);
}


private static boolean isNewsPage(String link){
return link.contains("news.sina.cn");
}

private static boolean isIndexPage(String link){
return "https://sina.cn".equals(link);
}
private static boolean isNotLoginPage(String link){
return !link.contains("passport.sina.cn");
}


// 此时的 if
if(isIntresetingLink(link)){
....
}

每当你要写注释的时候,请先尝试重构,让注释显得多余

继续优化,提取函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static Document httpGetAndParseHtml(String link){
if(link.startsWith("//")){
link = "https:" + link;
System.out.println("处理后的link:"+link);
}

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(link);
httpGet.addHeader("User-Agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36");


try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getStatusLine());
HttpEntity entity1 = response1.getEntity();
String html = EntityUtils.toString(entity1);
return Jsoup.parse(html);
}
}

IDEA重构技巧

  • 选中你的代码块
  • 右键 Refactor -> Extract -> Method -> 写个方法名

三个好处

  • 你是个人类,你大脑处理信息是有限的,因此100行的代码 和10行的小方法,10行的更容易被处理和理解
  • 短的方法容易被复用,越短小的方法越容易复用,而复用所有代码的书写原则,不要重复你自己
  • 对于Java来说你可以对复用方法进行覆盖,实现多态的功能

提交代码

  • 01我们在新分支 basic-algorithm
  • 02提交我们的代码
    • git add .
    • git commit 在vi里写你的信息
    • git push 此时报错了,因为远程仓库没有你的这个分支
    • git push --set-upstream origin basic-algorithm
  • 03打开你的 github 我的是 https://github.com/slTrust/xdml-crawler/

    • 页面会显示一个你最近的提交信息 点击 Compare & pull request 绿色按钮
    • 点击 Create pull request
    • 此时它会开始跑CI ,我们这个代码是会失败的 在 Details里
      • 有任何错误请看 日志的内容 红色的 ERROR
    • 改正我们的代码,再次执行 02 步操作
    • 此时页面刷新以下,多了你最新的 commit , 此时代码通过了检查,意味着我们可以合并了
    • Merge pull request 合并很有讲究的,它有三个选项

      • Create a merge commit
        等价于

        1
        2
        3
        git checkout master
        git merge basic-algorithm
        # 它会产生一个新的提交
      • Squash and merge
        等价于

        1
        2
        3
        git merge --squash
        好处是 比方你这个分支有两个或100个 提交
        他给你压缩成一个提交,这样你回滚的时候非常方便
      • Rebase and merge

        1
        2
        git rebase
        自行尝试
  • 04 此时我们尝试 Squash and merge ,然后点击 Squash and merge

    • 此时弹出一个文本框,意思是 你本来有多个commit 现在压缩成一个 commit
    • 你需要为压扁的这个 commit 定义 提交信息 , 默认是把之前的提交信息列出来
    • 我们稍微修改下信息, 然后点击 Confirm squash and merge
  • 05 此时去看你的仓库的 commit