solr6.3.0升级与IK动态词库自动加载
摘要:对于中文的搜索来说,词库系统是一个很比较重要的模块,本篇以IK分词器为例子,介绍如何让分词器从缓存或文件系统中自动按照一定频次进行加载扩展词库
Lucene、Solr或ElasticStack如何从外部动态加载词库进入到内存作为分词使用,且这一系列动作不需要重启相应的搜索服务?当前市面上各种博客、论坛都是各种转载或者只是最简单的使用IK,根本无法达到线上使用的条件,而IK分词器默认是一次启动将主词库、停用词以及扩展词库全部加载完毕,后续如果再想要增加额外的扩展词就必须得修改对应的扩展词表并重新打包上传并重启服务方能生效,这种方式不适合应用与线上服务。那么到底如何实现这种无缝扩充词库呢?下面针对IK来做分析,其他的几种分词器,都是大同小异的原理。
(一)词库介绍
不论使用什么类型的分词器,一般都少不了使用词库,而词库里面,除了主词库之外,还有扩展词库,同义词库,禁用词库等,其中扩展词库,同义词库,禁用词库是比较基础的词库,一般类型的业务开发,使用这3种词库后,基本能满足需求,特殊情况需要另外考虑。
(二)词库需求
每一个网站都需要有一个特定行业的词库,来丰富词库系统,当然你可以不用建立词库,这样的效果可能检索的时候,用户体验可能会比较差,在系统运行过程中,词库是可以动态更新的,所以要求我们的分词器,能够动态更新所有的词库,比如禁用词,同义词,扩展词等,这样做动态性比较好,但已经建好索引的文本,与目前的词库可能会存在一些误差,这种差别会在下一次重建索引时得到改变,所谓词库的动态更新,也就是在后台单独起个线程定时在内存里重新Load词库。
(三)为什么需要从数据库或文件系统加载词库?
在实际的开发中,搜索作为一个重要的组件,很少单独部署作为一个应用,除非是那种比较小的数据量,或者对搜索要求不是非常严格,通常在互联网或者电子商务行业,特别是电商行业,因为访问量比较大,对系统并发、负载均衡、响应请求要求比较高,所以搜索作为一个关键的应用通常需要采用集群的方式来构建一个高可用,高扩展的检索系统,在集群中,一般采用主从架构的方式,这样一来1主N从,需要有很多份词库文件,如果词库经常变化那么这种牵一发而动全身的趋势,就会变的很明显,解决办法主要有2种:
(1)在配置主从同步架构时,把变化的词库放在Master上,然后同步的时候把词库的配置文件也同步过去。
(2)第二种就是今天主题所说,所有的词库文件都从某一个集中的地方管理,然后各个solr节点,定时从数据库或缓存里读取并更新(在IK源码的Dictionary里进行更新)。
第一种方式的弊端在于,仅仅在solr的主从架构时,采用这种会比较方便,如果是solrcloud的模式,这种方法就不适用了
第二种方式相对来说比较方便,整个集群只维持一份词库文件,改动较小,而且更好的办法我们可以结合本地词库+数据库的方式一起工作,这样一来当数据库出现宕机的时候,我们的词库仍能正常工作。
(四)使用流程简析
定义一个IKTokenizerFactory类继承TokenizerFactory并实现ResourceLoaderAware接口,并重写inform方法和create方法,在solr里配置使用。
GitHub源码地址:https://github.com/liang68/ik-analyzer-solr6
(五) 修改详情
1. 源码级别(需要JDK1.8)
代码下载、编译和修改,扩展自己用于动态更新词库的类IKTokenizerFactory以及固定频次扫描的UpdateKeeper类。具体看如下代码示例:
package:org.wltea.analyzer.lucene
可以直接从我的github上下载solr-6.3.0版本下的词库动态更新版。额外增加用于配置文件配置IK词库加载的工厂类:IKTokenizerFactory,具体源代码见下或者参看我的github源码。其中集成了TokenizerFactory,实现了ResourceLoaderAware和UpdateKeeper.UpdateJob接口,需要重写inform和create方法:
/**
* @author liangyongxing
* @editTime 2017-02-06
* 增加IK扩展词库动态更新类
*/
public class IKTokenizerFactory extends TokenizerFactory
implements ResourceLoaderAware, UpdateKeeper.UpdateJob {
private boolean useSmart = false;
private ResourceLoader loader;
private long lastUpdateTime = -1L;
private String conf = null; public IKTokenizerFactory(Map<String, String> args) {
super(args);
this.useSmart = getBoolean(args, "useSmart", false);
this.conf = get(args, "conf");
System.out.println(String.format(":::ik:construction:::::::::::::::::::::::::: %s", this.conf));
} @Override
public Tokenizer create(AttributeFactory attributeFactory) {
return new IKTokenizer(attributeFactory, useSmart());
} @Override
public void inform(ResourceLoader resourceLoader) throws IOException {
System.out.println(String.format(":::ik:::inform:::::::::::::::::::::::: %s", this.conf));
this.loader = resourceLoader;
update();
if ((this.conf != null) && (!this.conf.trim().isEmpty())) {
UpdateKeeper.getInstance().register(this);
}
} @Override
/**
* 执行更新词典操作
* @throws IOException
*/
public void update() throws IOException {
Properties p = canUpdate();
if (p != null) {
List<String> dicPaths = SplitFileNames(p.getProperty("files"));
List inputStreamList = new ArrayList();
for (String path : dicPaths) {
if ((path != null) && (!path.isEmpty())) {
InputStream is = this.loader.openResource(path); if (is != null) {
inputStreamList.add(is);
}
}
}
if (!inputStreamList.isEmpty())
Dictionary.reloadDic(inputStreamList);
}
} /**
* 检查是否要更新
* @return
*/
private Properties canUpdate() {
try {
if (this.conf == null)
return null;
Properties p = new Properties();
InputStream confStream = this.loader.openResource(this.conf);
p.load(confStream);
confStream.close();
String lastupdate = p.getProperty("lastupdate", "0");
//System.err.println(String.format("read %s file get lastupdate is %s.", this.conf, lastupdate));
Long t = new Long(lastupdate);
if (t.longValue() > this.lastUpdateTime) {
this.lastUpdateTime = t.longValue();
String paths = p.getProperty("files");
if ((paths == null) || (paths.trim().isEmpty()))
return null;
System.out.println("loading conf files success.");
return p;
}
this.lastUpdateTime = t.longValue();
return null;
}
catch (Exception e) {
//e.printStackTrace();
System.err.println("IK parsing conf NullPointerException~~~~~" + e.getStackTrace());
}
return null;
} public static List<String> SplitFileNames(String fileNames) {
if (fileNames == null) {
return Collections.emptyList();
}
List result = new ArrayList();
for (String file : fileNames.split("[,\\s]+")) {
result.add(file);
}
return result;
} private boolean useSmart() {
return this.useSmart;
}
}
UpdateKeeper这个类主要实现了Runnable接口,封装了每1分钟自动读取配置,查看当前配置是否发生改变(根据配置的lastupdate=1 整数与当前内存中的整数做对比从而判断是否需要重新加载扩展词库),若发生改变会自动读取扩展词库将其加入到内存中。
/**
* Created by liangyongxing on 2017/2/6.
* 1分钟自动判断更新
*/
public class UpdateKeeper implements Runnable {
static final long INTERVAL = 60000L;
private static UpdateKeeper singleton;
Vector<UpdateJob> filterFactorys;
Thread worker; private UpdateKeeper() {
this.filterFactorys = new Vector(); this.worker = new Thread(this);
this.worker.setDaemon(true);
this.worker.start();
} public static UpdateKeeper getInstance() {
if (singleton == null) {
synchronized (UpdateKeeper.class) {
if (singleton == null) {
singleton = new UpdateKeeper();
return singleton;
}
}
}
return singleton;
} public void register(UpdateJob filterFactory) {
this.filterFactorys.add(filterFactory);
} public void run() {
while (true) {
try {
Thread.sleep(INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
} if (!this.filterFactorys.isEmpty()) {
for (UpdateJob factory : this.filterFactorys) {
try {
factory.update();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
} public interface UpdateJob {
void update() throws IOException;
}
}
package:org.wltea.analyzer.dic
这个包下的Dictionary类是加载词典到内存的主要类,针对自动动态扩展词库功能需要在这个类中增加额外的方法来实现只针对自己配置的扩展词库进行加载,具体增加的代码片段详情如下(考虑到该类代码有点多,只贴出来自己新增加的方法):
/**
* 重新更新词典
* 由于停用词等不经常变也不建议常增加,故这里只修改动态扩展词库
* @param inputStreamList
* @author liangyongxing
* @createTime 2017年2月7日
* @return
*/
public static Dictionary reloadDic(List<InputStream> inputStreamList) {
if (singleton == null) {
Configuration cfg = DefaultConfig.getInstance();
initial(cfg);
}
for (InputStream is : inputStreamList) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"), 512);
String theWord = null;
HashMap<String, String> map = new HashMap();
do {
theWord = br.readLine();
if (theWord != null && !"".equals(theWord.trim())) {
singleton._MainDict.fillSegment(theWord.trim().toLowerCase().toCharArray());
}
} while (theWord != null);
} catch (IOException ioe) {
System.err.println("Other Dictionary loading exception.");
ioe.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
is = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
return singleton;
}
改好了之后需要修改兼容版本的pom.xml文件,对应的lucene版本如下:
<groupId>org.wltea.ik-analyzer</groupId>
<artifactId>ik-analyzer-solr</artifactId>
<version>6.3.0</version>
<packaging>jar</packaging>
<name>ik-analyzer-solr6.3</name>
<url>http://code.google.com/p/ik-analyzer/</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lucene.version>6.3.0</lucene.version>
<javac.src.version>1.8</javac.src.version>
<javac.target.version>1.8</javac.target.version>
<maven.compiler.plugin.version>3.3</maven.compiler.plugin.version>
</properties>
改完之后重新build之后对应的有个核心类会抛异常,对应的类IKQueryExpressionParser会报错,报错的原因是对应的方法以及下线了,需要修改为当前最新用法,通过官方API文档看到BooleanQuery的使用方法已经改变,将其改正。 具体修改如下所示:
修改为如下所示:
改正完成后重新打包即可,将对应的jar上传到服务器solr安装的对应dist位置下即可。剩下的就是需要配置好相对应集群的配置文件上传,具体不要心急继续往后看哦。
2. 配置级别
jar包上传完之后搜索引擎服务启动或者重启之后,需要将配置文件上传到zk进行统一管理,其中我们需要修改schema.xml和solrconfig.xml
schema.xml中需要将中文分词引入(6.x名称修改为managed-schema,该文件采用和ElasticStack类似的映射方式,但是不论solr还是ES还是建议使用人为指定配置方式,这样不会出一些其他意想不到的问题--具体问题请参看我的博客:http://www.cnblogs.com/liang1101/articles/6379393.html)
<fieldType name="text_zh" class="solr.TextField" >
<analyzer type="index" >
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false" conf="ik.conf"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" useSmart="false" conf="ik.conf"/>
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
</analyzer>
</fieldType>
其中userSmart的意思是是否启用智能分词,默认是最大细粒度的方式,一般建议index为默认的,query为智能分词,但是这也需要看自己的业务再做相应的决定;对应conf="ik.conf"是自己指定的,这块最终是在代码处进行处理,这里定义的conf则在代码中需要通过conf名称去获取到ik.conf配置文件,再通过ik.conf文件配置项进行判断是否需要动态更新词库,具体的可以参看源代码,这里就不做多余的解释了。
solrconfig.xml配置文件需要将你新打的ik-analyzer-solr-6.3.0.jar引入:
<lib dir="/home/solr/solr/dist/" regex="ik-analyzer-solr-\d.*\.jar" />
至此代码级别和配置级别都已经大功告成,剩下的就是具体的测试流程了。
特此申明:
有好多朋友在按照上述配置运行代码的时候发现有问题,报出空指针异常等问题,这里我再作进一步的解释,包括整个要上传的文件目录与内容,以及 ik.conf 有什么用
1. ik.conf 作用
ik.conf 主要用于标识 IK 词库是否有进行更新的,有一个实时扫描程序在扫描这个文件里的内容从而判定是否需要重新加载词库。ik.conf 内容为:
lastupdate=1
files=dict1.txt
其中:lastupdate 后面的数字来记录当前更新版本,如果有需要修改只需要修改这个值(我一般就是直接 + 1 处理,例如我增加词库后会将这里修改为 2)重新上传即可。而 files 后面的文件指向你自己的扩展词库,这样你想增加新词库的话,后面用逗号跟着新文件就可以接入新词库文件,而不需要修改 solr ik 底层核心配置文件 IKAnalyzer.cfg,如果修改这个文件必须得重启 solr,这个线上肯定是不允许的(你要知道一个文件大小限制是1MB,所以可能会随着词库的增加,文件数也是需要增加的)
2. 往 zk 上传一个集群所需要的配置信息列表
以下列表列出来的就是我在 solr-6.4.0 上做测试时的所有内容
如有其它朋友有什么问题,欢迎在博客下面提问,或者在我对应的 github 地址提问题,这两块我有对应的信息提示,会及时回复大家。
(六) 测试详情
1. 配置信息上传至ZooKeeper
通过zk上传命令将对应的配置文件夹上传,具体上传命令如下:
/data/solr-6.3.0/server8983/scripts/cloud-scripts/zkcli.sh -zkhost zk1 -cmd upconfig -confdir /data/article_common_newest -confname article_common_newest
一台服务器上起4个实例,分别为:8983/7574/6362/5251,对应服务也分别是server8983/server7574/server6362/server5251
zk1 是其中一台zk服务器也是solr服务,对应的服务器hostname为zk1
article_common_newest 为对应配置文件存放的文件夹名称,是放在了/data 目录下
-confname article_common_newest 对应创建集群时所依赖的底层配置名称
2. 搜索引擎集群创建
curl "http://ip:8983/solr/admin/collections?action=CREATE&name=article_test&router.field=fingerprint&numShards=2&replicationFactor=2&maxShardsPerNode=2&collection.configName=article_common_newest&createNodeSet=ip:8983_solr,ip:7574_solr"
到创建好的集群内通过界面的Analysis选项卡进行分析,查看IK是否生效:
如果遇到这种界面的话,恭喜你,说明至此自动动态词库更新已经配置成功了。
add by 2017/02/22 增加第二种远程读取方案
申明:当前提交在github上的代码,主版本还是以读取相对本地文件来进行更新词库的(虽然是通过ZooKeeper进行同步的),所以为了区分主逻辑,在需要改动的类上都加上后缀:Remote
1. 本地主词库和主扩展词库是不需要改动的,即源IKAnalyzer.cfg.xml中的配置是不需要改动的。
2. 由于新增加了远程读取词库,故需要在IKAnalyzer.cfg.xml中增加两项配置,指定远程路径地址,例如:
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">location</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<entry key="remote_ext_stopwords">location</entry>
其中 location 是指一个 url,比如 http://yoursite.com/getCustomDict,该请求只需满足以下两点即可完成分词热更新。该 http 请求需要返回两个头部(header),一个是 Last-Modified,一个是 ETag,这两者都是字符串类型,只要有一个发生变化,该插件就会去抓取新的分词进而更新词库。该 http 请求返回的内容格式是一行一个分词,换行符用 \n 即可。
满足上面两点要求就可以实现热更新分词了,不需要重启solr
可以将需自动更新的热词放在一个 UTF-8 编码的 .txt 文件里,放在 nginx 或其他简易 http server 下,当 .txt 文件修改时,http server 会在客户端请求该文件时自动返回相应的 Last-Modified 和 ETag。可以另外做一个工具来从业务系统提取相关词汇,并更新这个 .txt 文件。
3. 增加了两项配置,故需要在源码级别的配置文件中增加两个属性值,用于读取上面配置的路径,在org.wltea.analyzer.cfg.ConfigurationRemote中,即:
/**
* 获取远程扩展词典配置路径
* @return List<String> 相对类加载器的路径
*/
public abstract List<String> getRemoteExtDictionarys();
/**
* 获取远程停止词配置路径
* @return List<String> 相对类加载器的路径
*/
public abstract List<String> getRemoteExtStopWordDictionarys();
其次在org.wltea.analyzer.cfg.DefaultConfigRemote类中(这个类为对应获取相应地址值),增加对这两个属性值的读取:
//配置属性--远程扩展词典
private static final String REMOTE_EXT_DICT = "remote_ext_dict";
//配置属性--远程停止词典
private static final String REMOTE_EXT_STOP = "remote_ext_stopwords"; /**
* 读取远程扩展词典内容到内存
*/
public List<String> getRemoteExtDictionarys() {
List remoteExtDictFiles = new ArrayList(2);
String remoteExtDictCfg = this.props.getProperty("remote_ext_dict");
if (remoteExtDictCfg != null) {
String[] filePaths = remoteExtDictCfg.split(";");
if (filePaths != null) {
for (String filePath : filePaths) {
if ((filePath != null) && (!"".equals(filePath.trim()))) {
remoteExtDictFiles.add(filePath);
}
}
}
}
return remoteExtDictFiles;
} /**
* 读取远程停止词典内容到内存
*/
public List<String> getRemoteExtStopWordDictionarys() {
List remoteExtStopWordDictFiles = new ArrayList(2);
String remoteExtStopWordDictCfg = this.props.getProperty("remote_ext_stopwords");
if (remoteExtStopWordDictCfg != null) {
String[] filePaths = remoteExtStopWordDictCfg.split(";");
if (filePaths != null) {
for (String filePath : filePaths) {
if ((filePath != null) && (!"".equals(filePath.trim()))) {
remoteExtStopWordDictFiles.add(filePath);
}
}
}
}
return remoteExtStopWordDictFiles;
}
4. 修改词典加载主类,为了与之前的org.wltea.analyzer.dic.Dictionary类不相互影响,故新扩展了一个名为:org.wltea.analyzer.dic.DictionaryRemote类,内部基本不用变,只需要修改动态加载词库那部分代码,具体请看第5点。
5. 新增加一个名为:org.wltea.analyzer.dic.Monitor类,用于定时更新词库的事件类,这个类主要的功能就是每1分钟执行一次Monitor类查看指定的指标数据是否发生变化,如果发生变化则自动更新词库,否则不变,具体代码如下所示:
/**
* 远程调用文件并定时检查更新
* add by liangyongxing
* @createTime 2017-02-22
*/
public class Monitor implements Runnable {
private static CloseableHttpClient httpclient = HttpClients.createDefault();
private String last_modified;
private String eTags;
private String location; public Monitor(String location) {
this.location = location;
this.last_modified = null;
this.eTags = null;
} @Override
public void run() {
RequestConfig rc = RequestConfig.custom().setConnectionRequestTimeout(10000).setConnectTimeout(10000).setSocketTimeout(15000).build(); HttpHead head = new HttpHead(this.location);
head.setConfig(rc); if (this.last_modified != null) {
head.setHeader("If-Modified-Since", this.last_modified);
}
if (this.eTags != null) {
head.setHeader("If-None-Match", this.eTags);
}
CloseableHttpResponse response = null;
try {
response = httpclient.execute(head); if (response.getStatusLine().getStatusCode() == 200) {
if ((!response.getLastHeader("Last-Modified").getValue().equalsIgnoreCase(this.last_modified))
|| (!response.getLastHeader("ETag").getValue().equalsIgnoreCase(this.eTags))) {
DictionaryRemote.getSingleton().reLoadMainDict();
this.last_modified = (response.getLastHeader("Last-Modified") == null ? null : response.getLastHeader("Last-Modified").getValue());
this.eTags = (response.getLastHeader("ETag") == null ? null : response.getLastHeader("ETag").getValue());
}
} else if (response.getStatusLine().getStatusCode() != 304) {
System.err.println("remote_ext_dict " + this.location + " return bad code " + response.getStatusLine().getStatusCode() + "");
}
} catch (Exception e) {
System.err.println("remote_ext_dict error!" + e.getStackTrace());
} finally {
try {
if (response != null)
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
6. 在DictionaryRemote类即字典主类初始化时,需要将自动监控加载代码注册
7. 新增加IKTokenizerFactoryRemote类,如果想要使用这种方式自动加载词库,那么在solrconfig.xml文件中需要配置这个类即可
相应最新的代码都已经上传到我的github上,地址为:https://github.com/liang68/ik-analyzer-solr6
有什么问题欢迎留言,我会尽可能早的回复。
solr6.3.0升级与IK动态词库自动加载的更多相关文章
- PHP+Jquery+Ajax 实现动态生成GUID、加载GUID
GUID: 全局唯一标识符(GUID,Globally Unique Identifier)是一种由算法生成的二进制长度为128位的数字标识符.GUID主要用于在拥有多个节点.多台计算机的网络或系统中 ...
- 动态script标签同步加载 ps:无打包编译,静态实现静态资源入口动态配置,无编译打包静态资源添加版本号
/**功能:创建动态标签加载css ,js文件,重点是js文件,利用onloading加递归实现动态标签的同步加载用法:在html文件body底部script内部声明并调用下列函数,obj中写要加载的 ...
- c++ 动态库的加载
转载:https://blog.csdn.net/ztq_12345/article/details/99677769 使用ide是vs, 使用Windows.h下的3个函数对动态库进行加载第一个:H ...
- AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖
好了,现进入正题,在 AngularJs 实现动态(懒)加载主要是依赖于3个主JS文件和一段依赖的脚本. 实现的过程主要是引用3个主要的JS文件 <script src="angula ...
- 速战速决 (5) - PHP: 动态地创建属性和方法, 对象的复制, 对象的比较, 加载指定的文件, 自动加载类文件, 命名空间
[源码下载] 速战速决 (5) - PHP: 动态地创建属性和方法, 对象的复制, 对象的比较, 加载指定的文件, 自动加载类文件, 命名空间 作者:webabcd 介绍速战速决 之 PHP 动态地创 ...
- 页面滚动动态加载数据,页面下拉自动加载内容 jquery
<!DOCTYPE=html> <html> <head> < script src="js/jquery.js" type=" ...
- [JS前端开发] js/jquery控制页面动态加载数据 滑动滚动条自动加载事件
页面滚动动态加载数据,页面下拉自动加载内容 相信很多人都见过瀑布流图片布局,那些图片是动态加载出来的,效果很好,对服务器的压力相对来说也小了很多 有手机的相信都见过这样的效果:进入qq空间,向下拉动空 ...
- AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖-转
http://blog.csdn.net/zhangh8627/article/details/51752872 AngularJs 通过 ocLazyLoad 实现动态(懒)加载模块和依赖 标签: ...
- thinkphp5.0 自动加载
自动加载 概述 ThinkPHP5.0真正实现了按需加载,所有类库采用自动加载机制,并且支持类库映射和composer类库的自动加载. 自动加载的实现由think\Loader类库完成,自动加载规范符 ...
随机推荐
- Daily Scrum 11.7
明后两天周六日,按照TFS的日常安排应该是休息,所以让他们自由完成已经分配的任务. 姓名 今日任务 黄新越 提取爬取网页的关键字并输出到接口 刘垚鹏 程序总架构的修改与多线程的学习 王骜 多线程学习 ...
- window 窗口编辑
package com.chuangkohua; import java.awt.FileDialog; import java.awt.FlowLayout; import java.awt.Fra ...
- J2EE 13种技术规范
J2EE平台由一整套服务(种技术规范进行简单的描述(限于篇幅,这里只能进行简单的描述): 1.JDBC(Java Database Connectivity): JDBC API为访问不同的数据 ...
- C++内存布局(1)-让new出的两个变量在堆上的地址连续
大家都知道栈的地址按照从高到低的顺序增长的, 而堆的地址是按照从底到高的顺序增长的. ); ); cout<<"n1,n2所指的地址:" << n1 < ...
- grep文本处理工具
grep是一款文本过滤工具,基于正则表达式进行模式匹配sed:stream editor 流编辑器awk:linux上实现为gawk,文本报告生成器(格式化文本)文本搜索工具,根据用户指定的模式,对目 ...
- Hive如何加载和导入HBase的数据
当我们用HBase 存储实时数据的时候, 如果要做一些数据分析方面的操作, 就比较困难了, 要写MapReduce Job. Hive 主要是用来做数据分析的数据仓库,支持标准SQL 查询, 做数据分 ...
- ACM数论之旅13---容斥原理(一切都是命运石之门的选择(=゚ω゚)ノ)
容斥原理我初中就听老师说过了,不知道你们有没有听过(/≧▽≦)/ 百度百科说: 在计数时,必须注意没有重复,没有遗漏. 为了使重叠部分不被重复计算,人们研究出一种新的计数方法. 这种方法的基本思想是: ...
- java 数据结构与算法---树
一.树的概念 除根节点外,其余节点有且只有一个父节点. 1.度 节点的度:每个节点的子节点个数. 树的度:树内各个节点的度的最大值. 树的高度(深度):树中节点的最大层次称为树的深度. 节点路径:一 ...
- QQ分享-定制分享卡片
一般H5页面在进行分享的时候,都会生成一个分享卡片,但是这些卡片的生成是很多时候是我们是想要生成的卡片, 对于QQ,我们只需要在html页面里加如3个标签即可,如下: <meta itempro ...
- Jmeter—添加断言 判断接口响应数据是否符合预期
发出请求之后,通过添加断言可以判断响应数据是否是我们的预期结果. 1 在Jmeter中发送一个状态返回200的http请求(参数故意输入错误).结果肯定是不是返回200啦. 但结果树中http请求的图 ...