30多线程爬虫与ES新闻搜索引擎

项目地址

从零开始

  • GitHub - new repository
  • 建立新项目
    • mvn archetype:generate 快速生成项目骨架(不推荐)
    • IDEA - New Project (等价于上面)
    • 直接从别人那儿抄一个
  • .gitignore 忽略提交的
  • README.md 项目说明,显示在项目地址首页
  • 配置基本的代码质量检查插件
    • 越早代价越低

项目的演进:正确性

如何保证改动代码不会破坏原先的功能?

  • 改完代码开始测,人肉测试,关键的功能测试,测试完提交;有问题回滚,排查问题
  • 自动化测试
    • push代码的时候都会触发circleci测试
    • 根据业务

好的代码习惯

  • 不要妥协
  • 不要自己造轮子
  • 不要做一个只会从网上抄代码的人
  • 如何阅读和使用官方文档?

测试的原则

  • 测试包放在test目录下
  • 每一个测试是一个类,负责一个很小的功能模块,这个类中的每个方法用于测试一个关键的功能点。

circleci

  • git登录

  • Projects - 对想要CI的项目点Set Up Project

    • 如果CI配置文件.circleci/config.xml,这里需要多一步创建配置文件
  • start building

每当push新的代码都会触发CI

git创建new-feature分支

git checkout -b new-feature

确定算法

为什么互联网被称为网,爬虫被称为爬虫

  • 从一个节点出发,遍历所有的节点

算法:广度优先算法的一个变体

如何拓展?

  • 假如未来要换数据库/上Elasticsearch
  • 爬虫的通用化

SpotBugs插件引入

            <plugin>
                <groupId>com.github.spotbugs</groupId>
                <artifactId>spotbugs-maven-plugin</artifactId>
                <version>3.1.12.2</version>
                <dependencies>
                    <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
                    <dependency>
                        <groupId>com.github.spotbugs</groupId>
                        <artifactId>spotbugs</artifactId>
                        <version>4.0.0-beta3</version>
                    </dependency>
                </dependencies>
                <executions>
                    <execution>
                        <id>spotbugs</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>check</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

<phase>verify</phase>插件绑定到verify阶段

<goal>check</goal>相当于spotbugs:check

  • Maven的生命周期

    • 默认的生命周期,比如执行test,就会把test前面所有阶段从头都执行一遍
    • 先声明的先执行
  • Maven的插件与目标

    • 目标与生命周期阶段的绑定

数据的持久化

数据库的访问方法

如何告诉用户应该如何建表

  • 自动化数据库的创建?迁移

使用Flyway数据库自动化迁移工具

mvn flyway:clean && mvn flyway:migrate

ORM初步

对象关系映射(英语:Object Relational Mapping,简称ORM

现在使用的数据库是关系型数据库

怎么样实现Relational到Object这样的绑定呢?

  • 把NEWS表变成对象,news表里的列和对象的属性一一对应

  • News.java 并给上Getter and Setter方法

  • 时间戳用Instant从Java1.8开始

public class News {
    private Integer id;
    private String url;
    private String content;
    private String title;
    private Instant createdAt;
    private Instant modifiedAt;
}
  • NEWS表
create table NEWS(
id bigint primary key auto_increment,
title text,
content text,
url varchar(1000),
created_at timestamp default now(),
modified_at timestamp default now()
) DEFAULT CHARSET=utf8mb4;

把和数据库相关的操作剥离成DAO

数据访问对象data access objectDAO)是为某种类型的数据库或其他持久性机制提供一个抽象接口的对象。

引入MyBatis简化数据库操作

docker容器启数据库

  • 非持久化 重启就没有了

    docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql:5.7.29
  • 数据库持久化映射 -v参数,映射到磁盘

    docker run --name mysql -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -v  E:/git/sina-crawler/mysql-data:/var/lib/mysql -d mysql:5.7.29
  • 移除数据库

    docker rm -f mysql 

db/mybatis/config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/news?characterEncoding=UTF-8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="db/mybatis/MyMapper.xml"/>
        <mapper resource="db/mybatis/MockMapper.xml"/>
    </mappers>
</configuration>

mapUnderscoreToCamelCase

  • 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。

session.commit(); 提交 如果成功则提交到数据库

session.rollback(); 回滚 回滚到没做操作之前的状态

如果成功则提交到数据库有异常则回滚到没做操作之前的状态

MySQL

MySQL 插入的数据乱码

  • 把news数据库编程UTF-8
ALTER DATABASE news CHARACTER SET =utf8mb4 COLLATE = utf8mb4_unicode_ci;
  • JDBC的链接也改成UTF-8
<configuration>
    <url>jdbc:mysql://localhost:3306/news?characterEncoding=UTF-8</url>
</configuration>
  • 创建NEWS表的时候设置默认字符集
create table NEWS(
id bigint primary key auto_increment,
title text,
content text,
url varchar(1000),
created_at timestamp default now(),
modified_at timestamp default now()
) DEFAULT CHARSET=utf8mb4;

MySQL索引

B+Tree索引

  • 是大多数 MySQL 存储引擎的默认索引类型。

  • 默认情况下,在数据之外MySQL会维护一个B+树,一般来说是id,id是主键,主键是主索引

  • 假如再建了一个name索引,会维护一个新的B+树,每一个记录都指向它对应的id,根据一个name查找的话,首先执行name索引,找到id之后,回去查主索引拿到真正的

MySQL建索引(建议看官方文档)

mysql官方创建所有官方文档

建立索引

CREATE INDEX created_at_index
    ON NEWS (created_at)

查看索引

show index from NEWS

Explain优化查询检测

解释当前语句以什么样的方式执行

explain select * from NEWS where created_at = '2019-02-18'

分析Explain

联合索引创建

例子1:

where a = xx and b = xx

创建(a, b)联合索引,它会建两个索引(a)(a, b)

同理(a, b, c),它会建三个索引(a)(a, b)(a, b, c)

例子2:

创建(a, b, c, d)联合索引

selete x from xx where a=1 and b=2 and c>3 and d=4

根据这个sql,查询优化器发现 and c>3 这里有个范围查找,意味着索引用处不大,找到这个索引把它后面的数据捞出来

查询优化器每当看到一个范围索引的时候,它就到此为止,然后把前面的a=1 and b=2 试图用联合索引去查,但是后面还有个d=4 是用不到索引

索引优化的过程

a=1 and b=2 and d=4 and c>3 如果我们有(a,b,d)就可以用联合索引了,不幸的是建的是(a,b,c,d)的索引,这个过程就是sql索引优化的过程

根据业务中写出的具体的sql,来决定具体建什么索引

(a,b,d,c) 这三个and的顺序都是可以换的,(b,d,a,c)(d,b,a,c) 查询优化器会自动的匹配到最优的索引上去

为时间戳建立索引

mysql remove timestamp from date 去掉时间戳的时分秒

update NEWS set created_at=date(created_at), modified_at=date(modified_at)

修改原有的所有,不要新加索引

已经有了一个(created_at),应该修改(created_at)为联合索引,因为联合索引会创建(created_at)和(created_at, modified_at)

CREATE INDEX created_at_modified_at_index
    ON NEWS (created_at, modified_at)

最左前缀匹配

created_at = '2019-02-18' 可以使用联合索引 type=ref

explain select * from NEWS where created_at = '2019-02-18' and modified_at < '2019-01-18'

created_at > '2019-02-18'没有可以用的索引 type=ALL

explain select * from NEWS where created_at > '2019-02-18' and modified_at = '2019-01-18'

Elasticsearch原理与数据索引实战

MySQL 长处在非文本数据的索引,检索本文中的一两个字符串,MySQL就显得力不从心

select * from NEWS where content like '%床前明月光%'

用这个语句查询慢

为什么传统的关系型数据库按照id这个列搜索很快?

  • id一般是主键,建立了一个主索引,内部建了一个B+树,是排好序的,可以非常快的查找到

传统的B+树的结构不适于文本检索

  • 传统的B+树索引本质上是比较相等操作,在比较id=xx的时候可以快速的找到
  • 文本本质上是一个contains操作,对文本是不是包含这个关键字

倒排索引

什么是倒排索引

作者:武培轩
链接:https://www.zhihu.com/question/23202010/answer/1054033556
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

倒排索引(Inverted Index) 也常被称为反向索引,是搜索引擎中非常重要的数据结构,为什么说它重要呢,我们首先拿一本书《重构 改善既有代码的设计》举个例子:

如果一本书没有目录的话,理论上也是可以读的,只是合上书下次再次阅读的时候,就有些耗费时间了。

通过给一本书加目录页,可以快速了解这本书的大致内容分布以及每个章节的页码数,这样在查询内容的时候效率就会非常高了,所以书的目录就是书本内容的简单索引。

想象一下你要搜索 case语句 这个关键词在这本书的页码,你应该怎么办呢?有些技术类的书籍会在最后提供索引页,这本书的索引页如下:

只需要从索引页中查找 case语句,就可以查找到关键词在书本中的页码位置了。

看完这个例子,让我们来把图书和搜索引擎做个简单的类比:

图书当中的目录页就相当正向索引(Forward Index)索引页就相当于倒排索引的简单实现,在搜索引擎中,正向索引指的是文档 ID 到文档内容和单词的关联,倒排索引就是单词到文档 ID 的关系

docker 启动Elasticsearch

拉取镜像

docker pull elasticsearch:7.6.0

创建esdata目录

mkdir esdata

持久化 win用绝对路径

docker run -d -v E:/git/sina-crawler/esdata:/usr/share/elasticsearch/data --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.6.0

-d 后台运行
--name 容器的名字
-p docker容器的端口和本地的端口映射

访问elasticsearch 浏览器http://localhost:9200/

官方文档:elasticsearch the definitive guide

https://www.elastic.co/guide/en/elasticsearch/guide/master/running-elasticsearch.html

从MySQL从读取数据再模拟数据写入elasticsearch

引入elasticsearch-rest-high-level-client

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.6.0</version>
</dependency>

代码

public class ElasticsearchDataGenerator {
    public static void main(String[] args) throws IOException {
        SqlSessionFactory sqlSessionFactory;

        try {
            String resource = "db/mybatis/config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        List<News> newsFromMySQL = getNewsFromMySQL(sqlSessionFactory);

        for (int i = 0; i < 10; i++) {
            new Thread(() -> writeSingleThread(newsFromMySQL)).start();
        }
    }

    private static void writeSingleThread(List<News> newsFromMySQL) {
        try (RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")))) {
            // 单线程写入2000*1000 = 200_0000数据
            for (int i = 0; i < 1000; i++) {
                BulkRequest bulkRequest = new BulkRequest();
                for (News news : newsFromMySQL) {
                    IndexRequest request = new IndexRequest("news");

                    Map<String, Object> data = new HashMap<>();
                    data.put("contnet", news.getContent().length() > 10 ? news.getContent().substring(0, 10) : news.getContent());
                    data.put("url", news.getUrl());
                    data.put("title", news.getTitle());
                    data.put("createdAt", news.getCreatedAt());
                    data.put("modifiedAt", news.getModifiedAt());

                    request.source(data, XContentType.JSON);

                    bulkRequest.add(request);
                }

                BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);

                System.out.println("Current thread : " + Thread.currentThread().getName() + " finishes " + i + ":" + bulkResponse.status().getStatus());

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static List<News> getNewsFromMySQL(SqlSessionFactory sqlSessionFactory) {
        try (SqlSession session = sqlSessionFactory.openSession()) {
            return session.selectList("com.github.hcsp.MockMapper.selectNews");
        }
    }
}

RestClient操作海量数据,用完记得关闭,把RestClient放到try-with-resources里

搜索使用

http://localhost:9200/news/_search

http://localhost:9200/news/_search?q=title:床前明月光

  • 新闻搜索引擎不是精确匹配,搜索的关键字会被分词 ...

  • 根据分词进行倒排索引,预期得到的结果是相关的数据能被搜出来,不是非常匹配的放在后面

集群简介

官方文档:

https://www.elastic.co/guide/en/elasticsearch/guide/master/distributed-cluster.html

集群健康信息

http://localhost:9200/_cluster/health

{
   "cluster_name":          "elasticsearch",
   "status":                "green", 
   "timed_out":             false,
   "number_of_nodes":       1,
   "number_of_data_nodes":  1,
   "active_primary_shards": 0,
   "active_shards":         0,
   "relocating_shards":     0,
   "initializing_shards":   0,
   "unassigned_shards":     0
}

status 字段是我们最感兴趣的

  • green

    • 所有的主分片和备用分片都是可用的
  • yellow

    • 所有的主分片可用,但是没有备份分片存在
    • 这是一种警告状态,警告我们现在的集群很危险,万一有个节点挂了,有一部分数据变得不可用
  • red

    • 并非所有的主分片都是可用状态

为什么使用集群

  • 数据的备份

    • 避免单点故障
    • 避免一个节点挂掉后整个集群数据就不可用了
  • 数据的水平扩展

    • 假设成千上万人访问节点,节点的压力很大,假如说能把这些数据能平均的分配到更多的节点上,把并发访问请求的压力分散到了多个机器上

多线程 先让结果正确然后再优化跑得更快

public synchronized String getNextLinkThenDelete() throws SQLException {
    try (SqlSession session = sqlSessionFactory.openSession(true)) {
        String url = session.selectOne("com.github.hcsp.MyMapper.selectNextAvailabaleLink");
        if (url != null) {
            session.delete("com.github.hcsp.MyMapper.deleteLink", url);
        }
        return url;
    }
}

多线程中经典的先取值再根据判断结果做下一件事情,在多线程环境中是非常危险的,两个线程可能同时在执行这段代码

Github上的merge

  • Create a merge commit
    创建一个合并提交
    将所有提交合并到基本分支

  • Squash and merge
    压缩和合并
    把所有的提交压缩成一个提交

  • Rebase and merge
    重新建立基础并合并
    会把分支的Commits合并平移到Code Commits

Windows下git bash的一些坑

  • 解决 .bashrc 文件每次打开终端都需要 source 的问题
    ~/.bash_profile文件中输入source ~/.bashrc
    保存退出后,重新打开终端

  • 解决git bash乱码问题

  1. 打开GitBash(git-bash.exe)后,对窗口右键->Options->Text->Locale改为zh_CN,Character set改为UTF-8;

  2. 直接输入

    vi ~/.bashrc
    PATH=$PATH:$HOME/bin
    export PATH
    unset USERNAME
    export LANG=zh_CN.UTF-8
    export LESSCHARSET=utf-8

git 命令行不小心提交

  • 不小心提交了不想提交的代码
    • git reset Head~1 把当前分支代码向后回滚1个提交
    • idea的9:Version Control 鼠标右键到想回滚提交 Reset Current Branch to Here然后Reset
  • 不小心commit还push了
    • 如果是在自己的分支上,那就用不小心提交了不想提交的代码的方式操作,并且force push git push -f
    • 如果在主干上,把多提交的文件删掉。
  • git commit --amend
    • 提交修正
    • 合并到上一个commit,不会产生新的commit
  • 撤回提交的过程,别人能看到吗?
    • 只要没有push,都是只有自己能看到的

how to write a commit message

  • 将标题行限制为50个字符,写重要的,第一个字母大写,标题不以标点符号结束
  • 正文的每一行不超过72个字符,详细描述提交的变更在做什么
  • 用空行将主体与主体分开
  • 指定提交的类型。建议使用一组一致的词语来描述您的更改,这可能会更有益。示例:Bugfix、Update、Refactor、Bump 等
点赞

发表评论

电子邮件地址不会被公开。必填项已用 * 标注