概述

es是一个基于Lucene的搜索引擎。对于初学者来说,可以将其看作一款NoSQL。es一般可以用作项目中的搜索、检索模块,提供关键词检索、条件过滤、聚合等功能。

es在单独使用时,可以实现的功能有比如:淘宝、京东等的商品搜索,根据品牌、分类的商品过滤,根据价格等的排序。数据聚合则是可以统计如:某一价格段的商品的销量,等类似的数据。

es中的数据是以文档(docment)为单位保存的,可以看作是数据库中的一行数据。文档保存在类型(type)中,可以看作是数据库中的表。而存储以上这些的称为索引(index),可以看作是数据库中的某一个库。 但是需要注意的是,es7后就不推荐使用type了,默认每个索引只有一个type,名字为“_doc”,而在es8中则会删除type。

这里主要是对于es的入门使用的介绍,以帮助初学者。

对于一些基础概念,则推荐阅读官方文档:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
中文官方文档对应版本较低,但是底层的一些概念是没有改变的。

分片、副本:https://www.elastic.co/guide/cn/elasticsearch/guide/current/_add-an-index.html
聚合:https://www.elastic.co/guide/cn/elasticsearch/guide/current/aggregations.html

安装

需要提前安装:elasticsearch7.x、kibana,教程太多,这里不贴出。

kibana可以可视化的对es进行操作。

使用

增删改查

es提供了非常多的Rest风格的接口,可以直接通过http请求对其进行操作,而请求的数据格式则为json。

添加

put请求为添加。使用postman对一下地址进行请求,结果如下。
http://localhost:9200/customer/external/1

也可以使用post请求,不加id(标识)的话每次请求会随机生成一个唯一id,加了就和put一样

查询

  • 使用get请求即可

get:http://localhost:9200/customer/external/1

更新

更新有两种方法:1、就是使用添加的api进行请求,如果请求的id相同,则为添加。在此不进行介绍。

2、post请求:http://localhost:9200/customer/external/1/_update

而此种方法请求时,请求的json格式要如下:

{"doc":{"name":"1111"}
}

两种方法的区别在于,第二种方式,若数据相同,则不会执行更新,而第一种,无论数据与否,都会用新数据将旧数据覆盖,那么这个不覆盖如何体现呢,我们看一下es返回的数据。

{"_index": "customer","_type": "external","_id": "1","_version": 8,"result": "noop","_shards": {"total": 0,"successful": 0,"failed": 0},"_seq_no": 16,"_primary_term": 29
}

可以看到第6行,result为noop,代表没有执行更新。

另外,还有两个字段值得注意: _seq_no、 _primary_term。

  • _seq_no是数据每更新一次就会+1的字段

  • _primary_term是es服务端每重启一次会+1的字段

而第二种方法更新请求的若为已有的数据,则_seq_no不会进行+1操作。

而且我们可以根据这两个字段,来为更新请求加上乐观锁。

先将数据查出,再将查出的_seq_no和 _primary_term 放入请求中:

使用put请求: http://localhost:9200/customer/external/1?if_seq_no=14&if_primary_term=29,若_seq_no和 _primary_term对应不上,则会更新失败。

删除

发送DELETE请求即可

  • DELETE: http://localhost:9200/customer/external/1

也可以删除整个index,但是删除type只能是删除type下的所有doc

  • DELETE: http://localhost:9200/customer

批量

一个个的添加、删除非常耗时耗力,我们还可以进行批量的操作,使用bluk接口

此时需要使用kibana 的 devtools

语法格式:

两行为一个整体(删除操作因为不需要请求的json数据,所以一行)

{action:{metadata}}\n
{request body  }\n{action:{metadata}}\n
{request body  }\n

这里的批量操作,当发生某一条执行发生失败时,其他的数据仍然能够接着执行,也就是说彼此之间是独立的。

bulk api以此按顺序执行所有的action(动作)。如果一个单个的动作因任何原因失败,它将继续处理它后面剩余的动作。当bulk api返回时,它将提供每个动作的状态(与发送的顺序相同),所以您可以检查是否一个指定的动作是否失败了。

示例1:

可以在请求的时候,在path里指定index和type

POST: customer/external/_bulk

{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"John Doe"}

以上的请求数据,两行为一个整体,第一行为操作:比如index为添加、delete为删除。第二行为操作的数据。

{“index”:{"_id":“1”}}
{“name”:“John Doe”}

即表示,添加{"name":"John Doe"}数据 ,id为1。

示例2:

也可以在action的metadata中指定index、type

index和create是同一个操作,delete不需要请求体,所以为一行

POST: /_bulk

{"delete":{"_index":"website","_type":"blog","_id":"123"}}
{"create":{"_index":"website","_type":"blog","_id":"123"}}
{"title":"my first blog post"}
{"index":{"_index":"website","_type":"blog"}}
{"title":"my second blog post"}
{"update":{"_index":"website","_type":"blog","_id":"123"}}
{"doc":{"title":"my updated blog post"}}

Query DSL

以上的简单查询,是无法帮助我们实现检索等功能的,所以就引出了DSL查询语句。

首先,先导入es官方的测试数据:https://github.com/elastic/elasticsearch/blob/6d42e197b82aa1c98a1796d547e373f7029ee27a/docs/src/test/resources/accounts.json

POST请求bank/account/_bulk 地址,向bank索引中批量添加数据

DSL语句的基本结构为:

 {QUERY_NAME:{FIELD_NAME:{ARGUMENT:VALUE,ARGUMENT:VALUE,...}   }
}

查询

查询后,默认会根据匹配程度生成一个评分_score,也会根据此评分进行排序

match_all 查询全部

GET /bank/_search
{"query": {"match_all": {}}
}

match 关键词查询

  • 查询非字符串(keyword、long)的会查询相等的
GET bank/_search
{"query": {"match": {"account_number": 20}}
}

match返回account_number=20的数据。

  • 查询字符串(text)的会查询包含的
GET bank/_search
{"query": {"match": {"address": "kings"}}
}

全文检索,最终会按照评分进行排序,会对检索条件进行分词匹配。

  • 查询text时以keyword的特性查询,即匹配完全相等的
GET bank/_search
{ "query": { "match": { "address.keyword": "440 King Street"}}
}

字段.keyword:则会匹配这个字段相等的,而不是包含

match_phrase 短句匹配

将需要匹配的值当成一整个单词(不分词)进行检索

match的区别在于:如下两个查询,match_phrase会查询包括Kings Place这两个连续单词的,而match会查询地址包括KingsPlace的,而包括Kings Place这两个连续单词的评分会更高

GET bank/_search
{"query": {"match_phrase": {"address": "Kings Place"}}
}
GET bank/_search
{"query": {"match": {"address": "Kings Place"}}
}

multi_math 多字段匹配

GET bank/_search
{"query": {"multi_match": {"query": "mill","fields": ["state","address"]}}
}

state或者address中包含mill,并且在查询过程中,会对于查询条件进行分词。

term 精确值查找

只对非text字段生效,必须完全匹配才会被查找到。

GET bank/_search
{"query": {"term": {"age" : 20}}
}

bool 复杂查询

一个 bool 过滤器由这几部分组成:

{"bool" : {    "must" :      [],"should" :     [],"must_not" :   [],"filter":      [],}
}

must

所有的语句都 必须(must) 匹配

must_not

所有的语句都 不能(must not) 匹配

should

应该包含(如果到达会增加相关文档的评分,并不会改变查询的结果。如果query中只有should且只有一种匹配规则,那么should的条件就会被作为默认匹配条件二区改变查询结果。

filter

过滤条件,使用上和查询一样,但是更匹配的不计入分数。

举例:

// 查询 address中有Street,年龄不为18,最好lastname有Wallace的(有的加分,没有的不影响),最后产生的结果再过滤(不记分数)gender为M的(此处使用的是term,term只对非text生效  .keyword是提前设置的,后面会说到,这里的作用是此次检索当作keyword来使用,所以才可以用term)
GET bank/_search
{"query": {"bool": {"must": [{"match": {"address": "Street"}}],"must_not": [{"match": {"age": "18"}}],"should": [{"match": {"lastname": "Wallace"}}],"filter": [{"term": {"gender.keyword": "M"}}]}}
}

filter 过滤

并不是所有的查询都需要产生分数,特别是哪些仅用于filtering过滤的文档。为了不计算分数,elasticsearch会自动检查场景并且优化查询的执行。

{"query": {"bool": {"must": [{"range": {"balance": {"gte": 10000,"lte": 20000}}},{"match": {"address": "Street"}}]}}
}
GET bank/_search
{"query": {"bool": {"must": [{"match": {"address": "Street"}}],"filter": [{"range": {"balance": {"gte": 10000,"lte": 20000}}}]}}
}

以上两种查询方式,一个是使用filter来匹配工资10000-20000的 最终查询出的分数为:

{"_index" : "bank","_type" : "account","_id" : "51","_score" : 0.95395315, // 《------"_source" : {"account_number" : 51,"balance" : 14097,"firstname" : "Burton","lastname" : "Meyers","age" : 31,"gender" : "F","address" : "334 River Street","employer" : "Bezal","email" : "burtonmeyers@bezal.com","city" : "Jacksonburg","state" : "MO"}
}

一个是通过must匹配工资,最终查询出的分数:

{"_index" : "bank","_type" : "account","_id" : "51","_score" : 1.9539531, // 《------"_source" : {"account_number" : 51,"balance" : 14097,"firstname" : "Burton","lastname" : "Meyers","age" : 31,"gender" : "F","address" : "334 River Street","employer" : "Bezal","email" : "burtonmeyers@bezal.com","city" : "Jacksonburg","state" : "MO"}
}

分页

在与query平行的一层,使用from 和 size 即可

GET bank/_search
{"query": {"match_all": {}},"from": 0,"size": 5,"_source": ["balance","firstname"]
}
// 表示从0开始,展示5个,即分页的第一页、页面大小为5
// _source的意思是,只展示firstname和balance两个字段

排序

GET bank/_search
{"query": {"match_all": {}},"from": 0,"size": 5,"sort": [{"account_number": {"order": "desc"}}],"_source": ["balance","firstname"]
}

mapping 映射

指定映射

说白了就是字段的类型,主要有:keyword、text、long、integer、date、object几种

上面的数据,是直接添加数据,es根据数据自动生成的映射

我们还可以在创建索引时指定映射

## 创建index时指定
PUT /my_index
{"mappings": {"properties": {"age": {"type": "integer"},"email": {"type": "keyword"},"name": {"type": "text","fields" : {// 指定 name.keyword时,字段以下面配置进行检索,此处的keyword无意义,可以任意更换其他名字"keyword" : {// 指定还可以作为keyword类型进行检索"type" : "keyword",// 表示作为keyword类型时,长度最多为256"ignore_above" : 256}}}}}
}
// ------------------------------------------------------------
{"mappings" : {"properties" : {"createTime" : {// type为date时需要指定日期的格式// 具体见文档:https://www.elastic.co/guide/reference/mapping/date-format/"type" : "date","format" : "yyyy-MM-dd HH:mm:ss"},"id" : {"type" : "keyword"},// 这里的memeber就是object类型,注意其下一级有“properties”"member" : {"properties" : {"id" : {"type" : "keyword"},"name" : {"type" : "text","analyzer" : "standard"}}},"title" : {"type" : "text",// 此处为指定分词器,一般需要安装ik分词器,后面会提到"analyzer" : "ik_smart"}}}
}

类型介绍

keyword

使用比较多的一个,主要可以用来储存不分词的字段,比如电话、邮箱这些只可以被整个检索到的

"id" : {"type" : "keyword"
}

text

一般的字符串类型,主要用来储存分词检索的字段,比如商品标题、博客简介等。可以指定分词器,比如搜索“小米手机”,就可能被分词为“小米”、“手机”两个词语,同事拥有“小米手机”的标题会评分较高,排位靠前,当然也可能你指定的分词器不会将“小米”和“手机分开”。

"title" : {"type" : "text",// 此处为指定分词器,一般需要安装ik分词器,后面会提到"analyzer" : "ik_smart"
}

date

日期时间类型,搭配format字段,统一日期格式,一般可以用于排序

"createTime" : {// type为date时需要指定日期的格式// 具体见文档:https://www.elastic.co/guide/reference/mapping/date-format/"type" : "date","format" : "yyyy-MM-dd HH:mm:ss"
}

arrays

数组类型

  • an array of strings: ["one","two"]
  • an array of integers: [1,2]
  • an array of arrays: [1, [2,3]] which is the equivalent of [1,2,3]
  • an array of objects: [{ "name": "Mary", "age": 12 },{ "name": "John", "age": 10 }]
PUT my-index-000001/_doc/1
{"message": "some arrays in this document...","tags":  [ "elasticsearch", "wow" ], "lists": [ {"name": "prog_list","description": "programming list"},{"name": "cool_list","description": "cool stuff list"}]
}PUT my-index-000001/_doc/2
{"message": "no arrays in this document...","tags":  "elasticsearch","lists": {"name": "prog_list","description": "programming list"}
}GET my-index-000001/_search
{"query": {"match": {"tags": "elasticsearch" }}
}

以上第三个请求,会检索tags中有elasticsearch的,数组内的数据也可以被检索到,所以最终两个doc都会被检索出来

object

对象类型

// 这里的memeber就是object类型,注意其下一级有“properties”
"member" : {"properties" : {"id" : {"type" : "keyword"},"name" : {"type" : "text","analyzer" : "standard"}}
}

检索的时候通过 对象名.字段名进行检索。如 "member.age":20来检索 年龄==20的

boolean

布尔类型 储存true false

其他类型

建议查看官方文档。

https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html

Aggregation聚合

官方中文文档写的非常好https://www.elastic.co/guide/cn/elasticsearch/guide/current/aggregations.html

概述

先说聚合是干嘛的

聚合可以用来查询诸如:2021-06-01这一天有多少注册人数,这些注册人数的男女性别分别为多少,他们的平均工资,以及男女的平均工资。

这类需要统计、计算的问题。(当然这是比较简单的使用,还有更加复杂的聚合

再说几个概念

桶聚合(Buckets):满足特定条件的文档的集合,如根据男女、月份、工资范围进行分桶。

指标(度量)聚合(Metrics):对桶内的文档进行统计计算,如计算count、平均、sum等。

桶在概念上类似于 SQL 的分组(GROUP BY),而指标(度量)则类似于 COUNT()SUM()MAX() 等统计方法。

桶聚合

概述

简单来说就是满足特定条件的文档的集合:

  • 一个员工属于 男性 桶或者 女性
  • 故宫属于 北京 桶,也可以属于中国
  • 日期2020-10-28属于 十月 桶,当然也可以属于21世纪

当聚合时,通过一条数据中的某个字段的值来确定该条数据属于哪个桶。

比如,我们创建一个条件为分桶字段为gender的,若gender字段只保存了两个值 男or女 ,则该桶则会分为两个,男和女

桶也可以进行嵌套,比如上面的两个男女桶,经过嵌套可以再根据年龄划分,得出不同年龄的桶。当然桶内也可以嵌套度量聚合,比如求不同年龄的平均工资。

terms

terms聚合的条件,就是根据这个字段有多少种不同的值,则生成多少个桶,如:性别的男女,生成两个桶,若保存的数据种还有“人妖”,则还会生成人妖桶

## 查询全部的人,根据年龄进行桶聚合
GET bank/_search
{"query": {"match_all": {}},"aggs": {"ageAgg": {"terms": {"field": "age"}}},"size": 0
}
## 返回结果:
"aggregations" : {"ageAgg" : {"doc_count_error_upper_bound" : 0,"sum_other_doc_count" : 463,"buckets" : [{    # 31岁桶"key" : 31,# 数量61"doc_count" : 61},{"key" : 39,"doc_count" : 60}# 此处省略一些其他年龄]}
}

以上的查询聚合结果,可以看到 “buckets” 为一个数组,其中有多个对象。而对象的key字段的意思即为分桶的依据,如第一个意思是31岁的数据有61条

range/date range

根据范围来分桶

GET /_search
{"aggs": {"price_ranges": {"range": {"field": "price","ranges": [{ "to": 100.0 },{ "from": 100.0, "to": 200.0 },{ "from": 200.0 }]}}}
}

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html

根据时间范围来分桶

POST /blog/_search?size=0
{"aggs": {"range": {"date_range": {"field": "createTime","format": "yyyy-MM-dd","ranges": [{ "from": "2020-01-10" } ,{ "to": "2021-06-10" }]}}}
}

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-daterange-aggregation.html

histogram

直方图聚合

请求:

interval 字段就是每间隔多少生成一个桶

min_doc_count 字段就是最小的数量,如果低于这个数量就不显示

POST /sales/_search?size=0
{"aggs": {"prices": {"histogram": {"field": "price","interval": 50,"min_doc_count": 1}}}
}

最后看返回结果:

可以根据key作为x轴,doc_count作为y轴,生成一张直方图

{..."aggregations": {"prices": {"buckets": [{"key": 0.0,"doc_count": 1},{"key": 50.0,"doc_count": 1},{"key": 150.0,"doc_count": 2},{"key": 200.0,"doc_count": 3}]}}
}

https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html

度量聚合

顾名思义,即求和、求平均之类的聚合

可以和桶聚合一起使用

avg

POST /exams/_search?size=0
{"aggs": {"avg_agg": { "avg": { "field": "grade" } }}
}

sum

POST /sales/_search?size=0
{"query": {"constant_score": {"filter": {"match": { "type": "hat" }}}},"aggs": {"hat_prices": { "sum": { "field": "price" } }}
}

嵌套

## 查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资
GET bank/_search
{"query": {"match_all": {}},"aggs": {"ageAgg": {"terms": {"field": "age","size": 100},"aggs": {"genderAgg": {"terms": {"field": "gender.keyword","size": 2},"aggs": {"blanceAvg": {"avg": {"field": "balance"}}}},"blanceAvg":{"avg": {"field": "balance"}}}}},"size": 0
}

分词器

安装分词器

安装常用的ik分词器

1、下载

https://github.com/medcl/elasticsearch-analysis-ik/releases

注意要下载与你的es对应版本的分词器。

2、解压

解压到es目录下的plugins文件夹即可,如图所示

使用

安装完成后重启es

GET my_index/_analyze
{"analyzer": "ik_smart", "text":"我是中国人"
}
## 分出 我、是、中国人 ,三个词
GET my_index/_analyze
{"analyzer": "ik_max_word", "text":"我是中国人"
}
## 分出 我、是、中国人、中国、国人 ,五个词

大部分时候创建索引时指定mapping为text需要分词的字段的分词所用的分词器。

自定义词库

有的比较新的网络用语没办法分词,需要自定义词库

1、进入plugins–>ik–>config目录,新建一个分词的文件,如:fenci.txt并在这一个文件中写入词汇,每个词以 回车 分割,可以参考 main.dic文件(自定义分词的文件后缀也可以是dic)

2、在config目录下的IKAnalyzer.cfg.xml文件中配置好自定义的词典

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties><comment>IK Analyzer 扩展配置</comment><!--用户可以在这里配置自己的扩展字典 --><entry key="ext_dict">fenci.txt</entry><!--用户可以在这里配置自己的扩展停止词字典--><entry key="ext_stopwords"></entry><!--用户可以在这里配置远程扩展字典 --><!-- <entry key="remote_ext_dict">http://127.0.0.1/es/fenci.txt</entry>--><!--用户可以在这里配置远程扩展停止词字典--><!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

【ElasticSearch 01】elasticsearch入门详解相关推荐

  1. Elasticsearch实战——function_score 查询详解

    Elasticsearch实战--function_score 查询详解 文章目录 Elasticsearch实战--function_score 查询详解 1. function_score简介 2 ...

  2. ElasticSearch预警服务-Watcher详解-Schedule配置

    介绍 Watcher服务详解-定时器的设定 关于Schedule配置选择,Watcher提供了丰富的时间语法支持,采用UTC时间,来我们一起看下如何设置: 支持的设置方式: hourly:按小时周期设 ...

  3. Spring和Elasticsearch全文搜索整合详解

    Spring和Elasticsearch全文搜索整合详解 一.概述 ElasticSearch是一个基于Lucene的搜索服务器.它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web ...

  4. [论文阅读] (01) 拿什么来拯救我的拖延症?初学者如何提升编程兴趣及LATEX入门详解

    又是在凌晨三点赶作业,又是在Deadline前去熬夜,一次次无眠,一次次抱怨.为什么三年前.两年前.一年前,甚至是昨天,我都下定决心"从现在开始读顶会论文",却又悄悄选择逃避:为什 ...

  5. FFmpeg入门详解之121:颜色空间转换RGB和YUV的原理与实战

    5.颜色空间转换RGB和YUV的原理与实战 三种颜色空间模型:RGB.YUV.HSV 一.概述 颜色通常用三个独立的属性来描述,三个独立变量综合作用,自然就构成一个空间坐标,这就是颜色空间. 但被描述 ...

  6. Jetpack Compose入门详解(实时更新)

    Jetpack Compose入门详解 前排提醒 前言(Compose是什么) 1.实战准备 一.优势与缺点 二.前四课 三.标准布局组件 1.Column 2.Row 3.Box 四.xml和com ...

  7. 生成对抗网络入门详解及TensorFlow源码实现--深度学习笔记

    生成对抗网络入门详解及TensorFlow源码实现–深度学习笔记 一.生成对抗网络(GANs) 生成对抗网络是一种生成模型(Generative Model),其背后最基本的思想就是从训练库里获取很多 ...

  8. Spring入门详解

    typora-copy-images-to: upload Spring入门详解 Spring框架是Java开发中最常用的框架,功能非常强大 源码下载:Spring Framework jar包.文档 ...

  9. linux 日志按大小切割_nginx入门详解(六)- 日志切割

    上一章讲解了nginx的目录加密功能,本章重点介绍nginx的日志切割. 笨办法学linux:nginx入门详解(五)- 目录加密​zhuanlan.zhihu.com 在第二章,我们探讨了nginx ...

  10. python怎么安装myqr模块-python二维码操作:对QRCode和MyQR入门详解

    python是所有编程语言中模块最丰富的 生活中常见的二维码功能在使用python第三方库来生成十分容易 三个大矩形是定位图案,用于标记二维码的大小.这三个定位图案有白边,通过这三个矩形就可以标识一个 ...

最新文章

  1. 中国电子学会图形化四级编程题:食堂取餐
  2. [转]Effective C# 原则5:始终提供ToString()
  3. python获取图片像素矩阵_用python处理图片实现图像中的像素访问
  4. linux gzip 命令简介
  5. 分析 linux 日志文件,linux精讲|操作系统常见日志文件分析
  6. yml配置文件中存在@无法识别,报错:found character ‘@‘ that cannot start any token. (Do not use @ for indentation)
  7. java图片识别查看器模拟_[转载]windows照片查看器无法显示图片内存不足
  8. python的if语句后面怎么加布尔运算符号是_python if 语句,布尔运算
  9. FreeRTOS+STM32F103串口通信错误解决方法
  10. SpringMVC源码解读 - HandlerMapping - SimpleUrlHandlerMapping初始化
  11. Java使用FTP上传文件被损坏的问题
  12. 再读《CSS权威指南》
  13. 宗成庆《自然语言理解》第二章作业
  14. android虚拟应用沙箱,Android的SandBox(沙箱)
  15. 地址栏中的#是什么意思
  16. JavaScript中常用的的字符串方法总结+详解
  17. Key was created with errors:报错
  18. Travelling Salesman Problem(旅行商问题)
  19. B站李永乐讲解傅里叶变换--笔记
  20. 一线互联网大厂中高级Android面试真题收录!大厂直通车!

热门文章

  1. oracle 计算入职年份,mssql sqlserver 获取入职日期到今天日期所经过的年份及月份信息呢?...
  2. uniapp pages.json 简单应用
  3. 推荐几款比较好用的AI画图软件
  4. “评价” 多款,多系统引导启动盘制作软件的优缺点
  5. yuv视频转png图片
  6. HTML球星简介,盘点足坛历史50大球星,第46-第50位
  7. android zxing 集成过程,android 集成Zxing教程
  8. Zxing Activity
  9. 微信号正则校验,qq正则,邮箱正则,英文名正则
  10. 服装店,直播带货有哪些话术技巧?