文档章节

中文分词算法 之 基于词典的正向最大匹配算法

杨尚川
 杨尚川
发布于 2014/03/18 11:44
字数 3353
阅读 1334
收藏 46

基于词典的正向最大匹配算法最长词优先匹配,算法会根据词典文件自动调整最大长度,分词的好坏完全取决于词典。

 

算法流程图如下:

 

Java实现代码如下:

 

/**
 * 基于词典的正向最大匹配算法
 * @author 杨尚川
 */
public class WordSeg {
    private static final List<String> DIC = new ArrayList<>();
    private static final int MAX_LENGTH;
    static{
        try {
            System.out.println("开始初始化词典");
            int max=1;
            int count=0;
            List<String> lines = Files.readAllLines(Paths.get("D:/dic.txt"), Charset.forName("utf-8"));
            for(String line : lines){
                DIC.add(line);
                count++;
                if(line.length()>max){
                    max=line.length();
                }
            }
            MAX_LENGTH = max;
            System.out.println("完成初始化词典,词数目:"+count);
            System.out.println("最大分词长度:"+MAX_LENGTH);
        } catch (IOException ex) {
            System.err.println("词典装载失败:"+ex.getMessage());
        }
        
    }
    public static void main(String[] args){
        String text = "杨尚川是APDPlat应用级产品开发平台的作者";  
        System.out.println(seg(text));
    }
    public static List<String> seg(String text){        
        List<String> result = new ArrayList<>();
        while(text.length()>0){
            int len=MAX_LENGTH;
            if(text.length()<len){
                len=text.length();
            }
            //取指定的最大长度的文本去词典里面匹配
            String tryWord = text.substring(0, 0+len);
            while(!DIC.contains(tryWord)){
                //如果长度为一且在词典中未找到匹配,则按长度为一切分
                if(tryWord.length()==1){
                    break;
                }
                //如果匹配不到,则长度减一继续匹配
                tryWord=tryWord.substring(0, tryWord.length()-1);
            }
            result.add(tryWord);
            //从待分词文本中去除已经分词的文本
            text=text.substring(tryWord.length());
        }
        return result;
    }
}

 

词典文件下载地址dic.rar,简单吧,呵呵

 

实现功能是简单,不过这里的词典中词的数目为:427452,我们需要频繁执行DIC.contains(tryWord))来判断一个词是否在词典中,所以优化这行代码能够显著提升分词效率(不要过早优化、不要做不成熟的优化)。

 

上面的代码是利用了JDK的Collection接口的contains方法来判断一个词是否在词典中,而这个方法的不同实现,其性能差异极大,上面的初始版本是用了ArrayList:List<String> DIC = new ArrayList<>()。那么这个ArrayList的性能如何呢?还有更好性能的实现吗?

 

通常来说,对于查找算法,在有序列表中查找比在无序列表中查找更快,分区查找全局遍历要快。

 

通过查看ArrayList、LinkedList、HashSet的contains方法的源代码,发现ArrayList和LinkedList采用全局遍历的方式且未利用有序列表的优势,HashSet使用了分区查找,如果hash分布均匀冲突少,则需要遍历的列表就很少甚至不需要。理论归理论,还是写个代码来测测更直观放心,测试代码如下:

 

/**
 * 比较词典查询算法的性能
 * @author 杨尚川
 */
public class SearchTest {
    //为了生成随机查询的词列表
    private static final List<String> DIC_FOR_TEST = new ArrayList<>();
    //通过更改这里DIC的实现来比较不同实现之间的性能
    private static final List<String> DIC = new ArrayList<>();
    static{
        try {
            System.out.println("开始初始化词典");
            int count=0;
            List<String> lines = Files.readAllLines(Paths.get("D:/dic.txt"), Charset.forName("utf-8"));
            for(String line : lines){
                DIC.add(line);
                DIC_FOR_TEST.add(line);
                count++;
            }
            System.out.println("完成初始化词典,词数目:"+count);
        } catch (IOException ex) {
            System.err.println("词典装载失败:"+ex.getMessage());
        }        
    }
    public static void main(String[] args){
        //选取随机值
        List<String> words = new ArrayList<>();
        for(int i=0;i<100000;i++){
            words.add(DIC_FOR_TEST.get(new Random(System.nanoTime()+i).nextInt(427452)));
        }
        long start = System.currentTimeMillis();
        for(String word : words){
            DIC.contains(word);
        }
        long cost = System.currentTimeMillis()-start;
        System.out.println("cost time:"+cost+" ms");
    }
}

 

#分别运行10次测试,然后取平均值
LinkedList     10000次查询       cost time:48812 ms
ArrayList      10000次查询       cost time:40219 ms
HashSet        10000次查询       cost time:8 ms
HashSet        1000000次查询     cost time:258 ms
HashSet        100000000次查询   cost time:28575 ms

 

我们发现HashSet性能最好,比LinkedList和ArrayList快约3个数量级!这个测试结果跟前面的分析一致,LinkedList要比ArrayList慢一些,虽然他们都是全局遍历,但是LinkedList需要操作下一个数据的引用,所以会多一些操作,LinkedList因为需要保存前驱后继引用,占用的内存也要高一些。

 

虽然HashSet已经有不错的性能了,但是如果词典越来越大,内存占用越来越多怎么办?如果有一个数据结构,有接近HashSet性能的同时,又能对词典的数据进行压缩以减少内存占用,那就完美了。

 

前缀树(Trie)有可能可以实现“鱼与熊掌兼得”的好事,自己实现一个Trie的数据结构,代码如下:

 

/**
 * 前缀树的Java实现
 * 用于查找一个指定的字符串是否在词典中
 * @author 杨尚川
 */
public class Trie {
    private final TrieNode ROOT_NODE = new TrieNode('/');

    public boolean contains(String item){
        //去掉首尾空白字符
        item=item.trim();
        int len = item.length();
        if(len < 1){
            return false;
        }
        //从根节点开始查找
        TrieNode node = ROOT_NODE;
        for(int i=0;i<len;i++){
            char character = item.charAt(i);
            TrieNode child = node.getChild(character);
            if(child == null){
                //未找到匹配节点
                return false;
            }else{
                //找到节点,继续往下找
                node = child;
            }
        }
        if(node.isTerminal()){
            return true;
        }
        return false;
    }
    public void addAll(List<String> items){
        for(String item : items){
            add(item);
        }
    }
    public void add(String item){
        //去掉首尾空白字符
        item=item.trim();
        int len = item.length();
        if(len < 1){
            //长度小于1则忽略
            return;
        }
        //从根节点开始添加
        TrieNode node = ROOT_NODE;
        for(int i=0;i<len;i++){
            char character = item.charAt(i);
            TrieNode child = node.getChildIfNotExistThenCreate(character);
            //改变顶级节点
            node = child;
        }
        //设置终结字符,表示从根节点遍历到此是一个合法的词
        node.setTerminal(true);
    }
    private static class TrieNode{
        private char character;
        private boolean terminal;
        private final Map<Character,TrieNode> children = new ConcurrentHashMap<>();        
        public TrieNode(char character){
            this.character = character;
        }
        public boolean isTerminal() {
            return terminal;
        }
        public void setTerminal(boolean terminal) {
            this.terminal = terminal;
        }        
        public char getCharacter() {
            return character;
        }
        public void setCharacter(char character) {
            this.character = character;
        }
        public Collection<TrieNode> getChildren() {
            return this.children.values();
        }
        public TrieNode getChild(char character) {
            return this.children.get(character);
        }        
        public TrieNode getChildIfNotExistThenCreate(char character) {
            TrieNode child = getChild(character);
            if(child == null){
                child = new TrieNode(character);
                addChild(child);
            }
            return child;
        }
        public void addChild(TrieNode child) {
            this.children.put(child.getCharacter(), child);
        }
        public void removeChild(TrieNode child) {
            this.children.remove(child.getCharacter());
        }        
    }
    
    public void show(){
        show(ROOT_NODE,"");
    }
    private void show(TrieNode node, String indent){
        if(node.isTerminal()){
            System.out.println(indent+node.getCharacter()+"(T)");
        }else{
            System.out.println(indent+node.getCharacter());
        }
        for(TrieNode item : node.getChildren()){
            show(item,indent+"\t");
        }
    }
    public static void main(String[] args){
        Trie trie = new Trie();
        trie.add("APDPlat");
        trie.add("APP");
        trie.add("APD");
        trie.add("Nutch");
        trie.add("Lucene");
        trie.add("Hadoop");
        trie.add("Solr");
        trie.add("杨尚川");
        trie.add("杨尚昆");
        trie.add("杨尚喜");
        trie.add("中华人民共和国");
        trie.add("中华人民打太极");
        trie.add("中华");
        trie.add("中心思想");
        trie.add("杨家将");        
        trie.show();
    }
}

 

 

修改前面的测试代码,把List<String> DIC = new ArrayList<>()改为Trie DIC = new Trie(),使用Trie来做词典查找,最终的测试结果如下:

 

#分别运行10次测试,然后取平均值
LinkedList     10000次查询       cost time:48812 ms
ArrayList      10000次查询       cost time:40219 ms
HashSet        10000次查询       cost time:8 ms
HashSet        1000000次查询     cost time:258 ms
HashSet        100000000次查询   cost time:28575 ms
Trie           10000次查询       cost time:15 ms
Trie           1000000次查询     cost time:1024 ms
Trie           100000000次查询   cost time:104635 ms

 

可以发现Trie和HashSet的性能差异较小,在半个数量级以内,通过jvisualvm惊奇地发现Trie占用的内存比HashSet的大约2.6倍,如下图所示:

 

HashSet:

 

Trie:


 

词典中词的数目为427452,HashSet是基于HashMap实现的,所以我们看到占内存最多的是HashMap$Node、char[]和String,手动执行GC多次,这三种类型的实例数一直在变化,当然都始终大于词数427452。Trie是基于ConcurrentHashMap实现的,所以我们看到占内存最多的是ConcurrentHashMap、ConcurrentHashMap$Node[]、ConcurrentHashMap$Node、Trie$TrieNode和Character,手动执行GC多次,发现Trie$TrieNode的实例数一直保持不变,说明427452个词经过Trie处理后的节点数为603141。

 

很明显地可以看到,这里Trie的实现不够好,选用ConcurrentHashMap占用的内存相当大,那么我们如何来改进呢?把ConcurrentHashMap替换为HashMap可以吗?HashSet不是也基于HashMap吗?看看把ConcurrentHashMap替换为HashMap后的效果,如下图所示:



 

内存占用虽然少了10M左右,但仍然是HashSet的约2.4倍,本来是打算使用Trie来节省内存,没想反正更加占用内存了,既然使用HashMap来实现Trie占用内存极高,那么试试使用数组的方式,如下代码所示:

 

/**
 * 前缀树的Java实现
 * 用于查找一个指定的字符串是否在词典中
 * @author 杨尚川
 */
public class TrieV2 {
    private final TrieNode ROOT_NODE = new TrieNode('/');

    public boolean contains(String item){
        //去掉首尾空白字符
        item=item.trim();
        int len = item.length();
        if(len < 1){
            return false;
        }
        //从根节点开始查找
        TrieNode node = ROOT_NODE;
        for(int i=0;i<len;i++){
            char character = item.charAt(i);
            TrieNode child = node.getChild(character);
            if(child == null){
                //未找到匹配节点
                return false;
            }else{
                //找到节点,继续往下找
                node = child;
            }
        }
        if(node.isTerminal()){
            return true;
        }
        return false;
    }
    public void addAll(List<String> items){
        for(String item : items){
            add(item);
        }
    }
    public void add(String item){
        //去掉首尾空白字符
        item=item.trim();
        int len = item.length();
        if(len < 1){
            //长度小于1则忽略
            return;
        }
        //从根节点开始添加
        TrieNode node = ROOT_NODE;
        for(int i=0;i<len;i++){
            char character = item.charAt(i);
            TrieNode child = node.getChildIfNotExistThenCreate(character);
            //改变顶级节点
            node = child;
        }
        //设置终结字符,表示从根节点遍历到此是一个合法的词
        node.setTerminal(true);
    }
    private static class TrieNode{
        private char character;
        private boolean terminal;
        private TrieNode[] children = new TrieNode[0];
        public TrieNode(char character){
            this.character = character;
        }
        public boolean isTerminal() {
            return terminal;
        }
        public void setTerminal(boolean terminal) {
            this.terminal = terminal;
        }        
        public char getCharacter() {
            return character;
        }
        public void setCharacter(char character) {
            this.character = character;
        }
        public Collection<TrieNode> getChildren() {
            return Arrays.asList(children);            
        }
        public TrieNode getChild(char character) {
            for(TrieNode child : children){
                if(child.getCharacter() == character){
                    return child;
                }
            }
            return null;
        }        
        public TrieNode getChildIfNotExistThenCreate(char character) {
            TrieNode child = getChild(character);
            if(child == null){
                child = new TrieNode(character);
                addChild(child);
            }
            return child;
        }
        public void addChild(TrieNode child) {
            children = Arrays.copyOf(children, children.length+1);
            this.children[children.length-1]=child;
        }
    }
    
    public void show(){
        show(ROOT_NODE,"");
    }
    private void show(TrieNode node, String indent){
        if(node.isTerminal()){
            System.out.println(indent+node.getCharacter()+"(T)");
        }else{
            System.out.println(indent+node.getCharacter());
        }        
        for(TrieNode item : node.getChildren()){
            show(item,indent+"\t");
        }
    }
    public static void main(String[] args){
        TrieV2 trie = new TrieV2();
        trie.add("APDPlat");
        trie.add("APP");
        trie.add("APD");
        trie.add("杨尚川");
        trie.add("杨尚昆");
        trie.add("杨尚喜");
        trie.add("中华人民共和国");
        trie.add("中华人民打太极");
        trie.add("中华");
        trie.add("中心思想");
        trie.add("杨家将");        
        trie.show();
    }
}

 

 

内存占用情况如下图所示:

 

 

现在内存占用只有HashSet方式的80%了,内存问题总算是解决了,进一步分析,如果词典够大,词典中有共同前缀的词足够多,节省的内存空间一定非常客观。那么性能呢?看如下重新测试的数据:

#分别运行10次测试,然后取平均值
LinkedList     10000次查询       cost time:48812 ms
ArrayList      10000次查询       cost time:40219 ms
HashSet        10000次查询       cost time:8 ms
HashSet        1000000次查询     cost time:258 ms
HashSet        100000000次查询   cost time:28575 ms
Trie           10000次查询       cost time:15 ms
Trie           1000000次查询     cost time:1024 ms
Trie           100000000次查询   cost time:104635 
TrieV1         10000次查询       cost time:16 ms
TrieV1         1000000次查询     cost time:780 ms
TrieV1         100000000次查询   cost time:90949 ms
TrieV2         10000次查询       cost time:50 ms
TrieV2         1000000次查询     cost time:4361 ms
TrieV2         100000000次查询   cost time:483398

 

 

总结一下,ArrayList和LinkedList方式实在太慢,跟最快的HashSet比将近慢约3个数量级,果断抛弃。Trie比HashSet慢约半个数量级,内存占用多约2.6倍,改进的TrieV1比Trie稍微节省一点内存约10%,速度差不多。进一步改进的TrieV2比Trie大大节省内存,只有HashSet的80%,不过速度比HashSet慢约1.5个数量级。

 

TrieV2实现了节省内存的目标,节省了约70%,但是速度也慢了,慢了约10倍,可以对TrieV2做进一步优化,TrieNode的数组children采用有序数组,采用二分查找来加速。

 

下面看看TrieV3的实现:



 

使用了一个新的方法insert来加入数组元素,从无到有构建有序数组,把新的元素插入到已有的有序数组中,insert的代码如下:

 

        /**
         * 将一个字符追加到有序数组
         * @param array 有序数组
         * @param element 字符
         * @return 新的有序数字
         */
        private TrieNode[] insert(TrieNode[] array, TrieNode element){
            int length = array.length;
            if(length == 0){
                array = new TrieNode[1];
                array[0] = element;
                return array;
            }
            TrieNode[] newArray = new TrieNode[length+1];
            boolean insert=false;
            for(int i=0; i<length; i++){
                if(element.getCharacter() <= array[i].getCharacter()){
                    //新元素找到合适的插入位置
                    newArray[i]=element;
                    //将array中剩下的元素依次加入newArray即可退出比较操作
                    System.arraycopy(array, i, newArray, i+1, length-i);
                    insert=true;
                    break;
                }else{
                    newArray[i]=array[i];
                }
            }
            if(!insert){
                //将新元素追加到尾部
                newArray[length]=element;
            }
            return newArray;
        }

 

 

有了有序数组,在搜索的时候就可以利用有序数组的优势,重构搜索方法getChild:



  

数组中的元素是TrieNode,所以需要自定义TrieNode的比较方法:



 

好了,一个基于有序数组的二分搜索的性能提升重构就完成了,良好的单元测试是重构的安全防护网,没有单元测试的重构就犹如高空走钢索却没有防护垫一样危险,同时,不过早优化不做不成熟的优化是我们应该谨记的原则,要根据应用的具体场景在算法的时空中做权衡。

 

OK,看看TrieV3的性能表现,当然了,内存使用没有变化,和TrieV2一样:

 

TrieV2         10000次查询       cost time:50 ms
TrieV2         1000000次查询     cost time:4361 ms
TrieV2         100000000次查询   cost time:483398 ms
TrieV3         10000次查询       cost time:21 ms
TrieV3         1000000次查询     cost time:1264 ms
TrieV3         100000000次查询   cost time:121740 ms

 

 

提升效果很明显,约4倍。性能还有提升的空间吗?呵呵......

 

代码托管于GITHUB

 

 

参考资料:

1、中文分词十年回顾 

2、中文信息处理中的分词问题

3、汉语自动分词词典机制的实验研究

4、由字构词_中文分词新方法

5、汉语自动分词研究评述

 

NUTCH/HADOOP视频教程

 

© 著作权归作者所有

杨尚川

杨尚川

粉丝 1102
博文 220
码字总数 1624053
作品 12
东城
架构师
私信 提问
加载中

评论(20)

lichaoxi
lichaoxi
我感觉用三叉搜索树保存词典(比如说中子树保存词,左子树保存比中子树小的词,右子树保存比中子树大的词),效率和内存方面都会有提升。
whaozl
whaozl

引用来自“whaozl”的评论

您好,川哥,这个可以用布隆过滤器来实现查找吗?不知道具体效果怎么样?

引用来自“杨尚川”的评论

绝对不可以,因为当你向“布隆过滤器”发问“一个字符串”是不是词的时候,“布隆过滤器”如果告诉你说没有这个词,那肯定是没有,但是如果“布隆过滤器”告诉你说有这个词,那么“布隆过滤器”有可能是在骗你,这样导致的结果是,你分出来的词中有很多的词是不存在的词!
理解了,布隆过滤器不会漏判,只会错判,这里还是要的准确哈。谢谢川哥。
杨尚川
杨尚川 博主

引用来自“whaozl”的评论

您好,川哥,这个可以用布隆过滤器来实现查找吗?不知道具体效果怎么样?
绝对不可以,因为当你向“布隆过滤器”发问“一个字符串”是不是词的时候,“布隆过滤器”如果告诉你说没有这个词,那肯定是没有,但是如果“布隆过滤器”告诉你说有这个词,那么“布隆过滤器”有可能是在骗你,这样导致的结果是,你分出来的词中有很多的词是不存在的词!
whaozl
whaozl
您好,川哥,这个可以用布隆过滤器来实现查找吗?不知道具体效果怎么样?
悠悠然然
悠悠然然

引用来自“杨尚川”的评论

引用来自“悠悠然然”的评论

呵呵,光说有改进区间,给不出具体意见,会被人鄙视:
String tryWord = text.substring(0, 0+len);
while(!DIC.contains(tryWord)){
  if(tryWord.length()==1){
   break;
  }
  tryWord=tryWord.substring(0, tryWord.length()-1);
}
问题1:text.substring(0, 0+len);会导致产生大量的新的字符串的产生,消耗CPU的同时还会促发垃圾回收频繁发生导致性能下降。
问题2:另外,随着最大长度的变大,性能会导致严重下降,因为做的无用功太多了。
问题3:前一次检索的计算量,在下一次检索时完全浪费了。

基于此,至少还可以有1个左右数量级可以提升。

substring的改进可以提升2.5倍,可以看我另一篇博文《中文分词算法 之 基于词典的逆向最大匹配算法》中的测试数据,分词速度:1007.0 字符/毫秒。

任重而道远,持续改进必能取得真经。
杨尚川
杨尚川 博主

引用来自“Skidoo”的评论

就像一楼说的 ,你使用trie的思路有问题。在字符串有较多较长共同前缀时用trie一般可以节省内存并且提升查询效率。但是查找效率再高也很难超过hash啊。所以你可以看到你用trie的时候查询效率赶不上直接用hash,因为直接用hash的话一次遍历一次计算既可以得到hash值,但是你trie内部借助hash实现,每查找虽然也是一次遍历,但是每次遍历时针对每个字符都要动用一次hash计算,所以效率就下来了。至于用trie后你的内存占用反而增大,一个原因是你的词典中共同前缀应该不多,二个还是因为你每个节点都借助hash实现。hash内存占用的浪费期望就是50%,因为hash是空间换时间的算法。所以你的trie实现既浪费空间又浪费时间。

Trie的使用没有问题,看我另一篇博文《中文分词算法 之 基于词典的逆向最大匹配算法》中的测试数据可得知:即使经过优化后TrieV3仍然比HashSet慢4倍,也不影响它在分词算法中的作用,从上面的数据可以看到,TrieV3的整体分词性能领先HashSet十五个百分点(15%),而且内存占用只有HashSet的80%。
杨尚川
杨尚川 博主

引用来自“悠悠然然”的评论

呵呵,光说有改进区间,给不出具体意见,会被人鄙视:
String tryWord = text.substring(0, 0+len);
while(!DIC.contains(tryWord)){
  if(tryWord.length()==1){
   break;
  }
  tryWord=tryWord.substring(0, tryWord.length()-1);
}
问题1:text.substring(0, 0+len);会导致产生大量的新的字符串的产生,消耗CPU的同时还会促发垃圾回收频繁发生导致性能下降。
问题2:另外,随着最大长度的变大,性能会导致严重下降,因为做的无用功太多了。
问题3:前一次检索的计算量,在下一次检索时完全浪费了。

基于此,至少还可以有1个左右数量级可以提升。

substring的改进可以提升2.5倍,可以看我另一篇博文《中文分词算法 之 基于词典的逆向最大匹配算法》中的测试数据,分词速度:1007.0 字符/毫秒。
k
kidding
jdk1.8 优化了hashmap在hash冲突下的链表改为treemap
h
hari

引用来自“Skidoo”的评论

不过一楼建议你用自动机实现kmp来进行匹配我倒是不太赞成,根据我的印象,kmp是进行模式匹配的。因为你这是在词典里查找,不是在文章里查找。还有就是即使用kmp,时间复杂度为o((n*m)*m),如果你的length设置比较长,还是不太合算,用hash的话时间复杂度为o(m^2),更快一些。

写错了,前面那个是O((n+m)*m)
h
hari
你后面的trie实现使用了数组虽然提升了内存使用,但是每次插入值的时候可能都复制一遍。而且虽然用了二分查找,但是又有哪个的查找效率能轻易干过hash呢。除非字符串超长,计算hash值的时间慢过一次二分查找。
中文分词算法 之 基于词典的正向最小匹配算法

在之前的博文中介绍了基于词典的正向最大匹配算法,比如我们切分句子: 中华人民共和国万岁万岁万万岁,使用正向最大匹配算法的切分结果为:[中华人民共和国, 万岁, 万岁, 万万岁],可以看到,...

杨尚川
2014/04/04
1K
0
Java中文分词组件 - word分词

Java分布式中文分词组件 - word分词 word分词是一个Java实现的分布式的中文分词组件,提供了多种基于词典的分词算法,并利用ngram模型来消除歧义。能准确识别英文、数字,以及日期、时间等数...

杨尚川
2014/04/29
25.1K
56
中文分词算法 之 基于词典的逆向最大匹配算法

在之前的博文中介绍了基于词典的正向最大匹配算法,用了不到50行代码就实现了,然后分析了词典查找算法的时空复杂性,最后使用前缀树来实现词典查找算法,并做了3次优化。 下面我们看看基于词...

杨尚川
2014/03/21
760
1
随思:关于中文分词方法

疑问:为什么会涉及到分词方法学呢? 为什么需要确定哪些是词语,哪些不是词语呢? 为什么需要进行分词,如果不分词会是什么情况呢? 分词的根本目的是为了搜索服务的,更确切的是为快速搜索而...

wangtaotao
2014/04/06
0
0
中文分词常用方法简述

中文分词 就是将一句话分解成一个词一个词,英文中可以用空格来做,而中文需要用一些技术来处理。 三类分词算法: 1. 基于字符串匹配: 将汉字串与词典中的词进行匹配,如果在词典中找到某个...

不会停的蜗牛
2017/10/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

CSS--列表

一、列表标识项 list-style-type none:去掉标识项 disc:默认实心圆 circle:空心圆 squire:矩形 二、列表项图片 list-style-img: 取值:url(路径) 三、列表项位置 list-style-position:...

wytao1995
今天
4
0
linux 命令-文本比较comm、diff、patch

本文原创首发于公众号:编程三分钟 今天学了三个文本比较的命令分享给大家。 comm comm 命令比较相同的文本 $ cat charabc$ cat chardiffadc 比如,我有两个文件char和chardiff如上,...

编程三分钟
今天
7
0
QML教程

https://blog.csdn.net/qq_40194498/article/category/7580030 https://blog.csdn.net/LaineGates/article/details/50887765...

shzwork
今天
5
0
HA Cluster之5

对于使用heartbeat v2版的CRM配置的集群信息都是保存在一个名为cib.xml的配置文件中,存放在/var/lib/heartbeat/crm/下。CIB:Cluster Information Base,由于xml文件配置不是那么方便,所以...

lhdzw
今天
6
0
玩转Redis-Redis基础数据结构及核心命令

  《玩转Redis》系列文章主要讲述Redis的基础及中高级应用,文章基于Redis5.0.4+。本文主要讲述Redis的数据结构String,《玩转Redis-Redis基础数据结构及核心命令》相关操作命令为方便对比...

zxiaofan666
今天
11
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部