lucene介绍与使用

一、什么是lucene

     Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。

   上面这段是百度百科的解释,简单的说就是一个实现全文检索引擎的工具包,里面提供了一些api接口供使用者调用。

  需要注意的是lucene并不是一个搜索引擎,Lucene仅仅是一个工具包,它不能独立运行,不能单独对外提供服务。搜索引擎可以独立运行对外提供搜索服务,如百度,搜狐等。

二、 什么是搜索引擎

搜索引擎是一种软件,能够为文本建立索引,能够根据索引搜索文本信息。如

     web搜索:百度,google

    桌面搜索:开始,运行里面的搜索。

    企业搜索:站内搜索,企业知识库搜索。

 1.两个基本的组件

  •     索引组件
  •     搜索组件

三、什么是全文检索

    全文检索是计算机程序通过扫描文章中的每一个词,对必要的词建立一个索引,指明该词在文章中出现的次数和位置。当用户查询时根据建立的索引查找,类似于通过字典的检索字表查字的过程。

    全文检索(Full-Text Retrieval)是指以文本作为检索对象,找出含有指定词汇的文本。全面、准确和快速是衡量全文检索系统的关键指标。

    关于全文检索,我们要知道:1,只处理文本。2,不处理语义。3,搜索时英文不区分大小写。4,结果列表有相关度排序。5,并且可以对结果具有过滤高亮的功能

   在信息检索工具中,全文检索是最具通用性和实用性的。

四、Lucene实现全文检索的流程

全文检索的流程:索引流程、搜索流程

索引流程:采集数据(读取数据库,读取文件数据)—》文档处理(存储到索引库中,索引库文件可以存在内存也可以存在磁盘)
搜索流程:输入查询条件—》通过lucene的查询器查询索引—》从索引库中取出结—》视图渲染

五、使用案例

上面已经讲解了lucene相关的一些基本知识,下面结合案例来讲解下lucene的使用,涉及到使用Lucene的API来实现对索引的增(创建索引)、删(删除索引)、改(修改索引)、查(搜索数据)。本案例是spring boot项目,从页面浏览器上输入要搜索的关键字,然后在控制里打印出搜索的数据信息,对于工程的创建和其他的一些无关代码就不粘贴了,主要粘贴一些主要的代码

1.pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.tp</groupId>
    <artifactId>lucene</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>lucene</name>
    <description>Demo project for lucene study</description>

    <properties>
        <java.version>1.8</java.version>
        <lunece.version>4.10.2</lunece.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!-- lucene核心库 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>${lunece.version}</version>
        </dependency>
        <!-- Lucene的查询解析器 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>${lunece.version}</version>
        </dependency>
        <!-- lucene的默认分词器库 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>${lunece.version}</version>
        </dependency>
        <!-- lucene的高亮显示 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>${lunece.version}</version>
        </dependency>

        <!-- 热部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.索引存储对象product

package com.tp.lucene;


import lombok.Data;

/**
 * @Package: com.tp.lucene
 * @ClassName: Product
 * @Author: tanp
 * @Description: 产品实体类
 * @Date: 2020/9/17 15:34
 */
@Data
public class Product {

    int id;
    String name;
    String category;
    float price;
    String place;
    String code;

    Product(int id,String name,String category,float price,String place,String code){
        this.id = id;
        this.name = name;
        this.category = category;
        this.place = place;
        this.code = code;
        this.price = price;
    }

    Product(){}


    @Override
    public String toString() {
        return "Product [id=" + id + ", name=" + name + ", category=" + category + ", price=" + price + ", place="
                + place + ", code=" + code + "]";
    }

}

3.测试接口类

package com.tp.lucene;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.highlight.Highlighter;
import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
import org.apache.lucene.search.highlight.QueryScorer;
import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 做 Lucene的思路。
 * 1. 首先搜集数据
 * 数据可以是文件系统,数据库,网络上,手工输入的,或者像本例直接写在内存上的
 * 2. 通过数据创建索引
 * 3. 用户输入关键字
 * 4. 通过关键字创建查询器
 * 5. 根据查询器到索引里获取数据
 * 6. 然后把查询结果展示在用户面前
 */

/**
 * @Package: com.tp.lucene
 * @ClassName: TestLucene
 * @Author: tanp
 * @Description: ${description}
 * @Date: 2020/9/17 10:34
 */
@RestController
@RequestMapping(value = "/lucene")
public class TestLucene {

    @RequestMapping("lucene01")
    public void Luncene01(String keyWord) throws Exception {


        //1.创建分词器对象
        Analyzer analyzer = new StandardAnalyzer();

        //2.创建目录索引
        Directory directory = createIndex(analyzer);


        //3.创建查询器
        Query query = new QueryParser("name", analyzer).parse(keyWord);


        //4.获取搜索工具
        IndexSearcher searcher = getSearch(directory);

        //5.查询数据
        ScoreDoc[] hits = getResults(searcher, query);

        //6.展示数据
        showSearchResults(hits, searcher, query, analyzer);
    }

    /**
     * @Description 获取查询数据, 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
     * @Date 2020/9/17 16:50
     * @Author tanp
     */
    private ScoreDoc[] getResults(IndexSearcher searcher, Query query) throws IOException {
        //比如要查询第10页,每页10条数据。
        //Lucene 分页通常来讲有两种方式:
        //第一种是把100条数据查出来,然后取最后10条。 优点是快,缺点是对内存消耗大。
        //第二种是把第90条查询出来,然后基于这一条,通过searchAfter方法查询10条数据。 优点是内存消耗小,缺点是比第一种更慢


        int pageSize = 50;
        int pageNum = 1;
        //方式1
        //查询出数据
//        TopDocs topDocs = searcher.search(query, pageNum * pageSize);
//        System.out.println("查询到的总条数\t" + topDocs.totalHits);
//        //获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
//        ScoreDoc[] alllScores = topDocs.scoreDocs;
//        List<ScoreDoc> hitScores = new ArrayList<>();
//        int start = (pageNum - 1) * pageSize;
//        int end = pageSize * pageNum;
//        for (int i = start; i < end; i++) {
//            hitScores.add(alllScores[i]);
//        }
//        ScoreDoc[] hits = hitScores.toArray(new ScoreDoc[]{});
//        return hits;

        //方式2
        int start = (pageNum - 1) * pageSize;
        if(0==start){
            TopDocs topDocs = searcher.search(query, pageNum*pageSize);
            return topDocs.scoreDocs;
        }
        // 查询数据, 结束页面自前的数据都会查询到,但是只取本页的数据
        TopDocs topDocs = searcher.search(query, start);
        //获取到上一页最后一条
        ScoreDoc preScore= topDocs.scoreDocs[start-1];
        //查询最后一条后的数据的一页数据
        topDocs = searcher.searchAfter(preScore, query, pageSize);
        return topDocs.scoreDocs;
    }

    /**
     * @Description 展示查询到的数据
     * @Date 2020/9/17 14:34
     * @Author tanp
     */
    private void showSearchResults(ScoreDoc[] hits, IndexSearcher searcher, Query query, Analyzer analyzer) throws IOException, InvalidTokenOffsetsException {
        System.out.println("找到 " + hits.length + " 个命中.");
        System.out.println("序号\t匹配度得分\t结果");

        // 查询高亮,对于name字段中符合keyWord的高亮
        QueryScorer score = new QueryScorer(query);
        // 定制高亮标签
        SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<span style='color:red'>", "</span>");
        // 高亮分析器
        Highlighter highlighter = new Highlighter(simpleHTMLFormatter, score);


        for (int i = 0; i < hits.length; i++) {
            ScoreDoc scoreDoc = hits[i];
            // 取出文档编号
            int docId = scoreDoc.doc;
            // 根据编号去找文档
            Document document = searcher.doc(docId);
            System.out.print((i + 1));
            System.out.print("\t" + scoreDoc.score);
            List<IndexableField> fields = document.getFields();
            for (IndexableField f : fields) {
                if ("name".equals(f.name())) {
                    //name字段中,符合搜索keyWord的高亮展示
                    TokenStream tokenStream = analyzer.tokenStream(f.name(), new StringReader(document.get(f.name())));
                    // 获取高亮的片段
                    String fieldContent = highlighter.getBestFragment(tokenStream, document.get(f.name()));
                    System.out.print("\t" + fieldContent);
                } else {
                    System.out.print("\t" + document.get(f.name()));
                }
            }
            System.out.println();
        }
    }

    /**
     * @Description 获取索引搜索工具
     * @Date 2020/9/17 11:43
     * @Author tanp
     */
    private IndexSearcher getSearch(Directory directory) throws Exception {
        if (directory == null) {
            //获取内存目录索引
            directory = new RAMDirectory();
            //磁盘目录索引,要么使用内存目录索引,要么使用自磁盘目录索引,二选一
            //directory = FSDirectory.open(new File("d:\\indexDir"));
        }
        //创建索引读取工具
        IndexReader indexReader = DirectoryReader.open(directory);
        //创建索引搜索工具
        IndexSearcher searcher = new IndexSearcher(indexReader);
        return searcher;
    }

    /**
     * @Description 创建索引
     * @Date 2020/9/17 10:43
     * @Author tanp
     */
    private Directory createIndex(Analyzer analyzer) throws Exception {
        //1.创建内存目录索引
        Directory directory = new RAMDirectory();
        //磁盘目录索引,要么使用内存目录索引,要么使用自磁盘目录索引,二选一
        //Directory directory = FSDirectory.open(new File("d:\\indexDir"));
        //2.根据分词器创建写出工具配置对象
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
       //设置打开方式,OpenMode.CREATE会先清空原来的索引,再添加新的索引,如果不设置,多运行几次则会发现同样的id的数据在索引中存储了好几份
        config.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
        //3.创建索引的写出工具类。参数:索引的目录和配置信息
        IndexWriter indexWriter = new IndexWriter(directory, config);
        //4.将数据写入索引
        addDoc(indexWriter);

        //额外功能:删除id=51173的数据
        //indexWriter.deleteDocuments(new Term("id", "51173"));
        //删除全部数据
        //indexWriter.deleteAll();

        //额外功能:更新
        Document doc = new Document();
        doc.add(new TextField("id", "141769", Field.Store.YES));
        doc.add(new TextField("name", "神鞭,鞭没了,神还在", Field.Store.YES));
        doc.add(new TextField("category", "道具", Field.Store.YES));
        doc.add(new TextField("price", "998", Field.Store.YES));
        doc.add(new TextField("place", "南海群岛", Field.Store.YES));
        doc.add(new TextField("code", "888888", Field.Store.YES));
        indexWriter.updateDocument(new Term("id", "141769"), doc );

        indexWriter.commit();
        indexWriter.close();
        return directory;
    }

    /**
     * @Description 将数据写入索引
     * @Date 2020/9/17 10:53
     * @Author tanp
     */
    private void addDoc(IndexWriter indexWriter) throws Exception {
        //1.获取数据,本次直接模拟10条数据,还可从文件中获取,数据库中获取
        //List<Product> products = getProducts();
        //从文本中获取数据
        List<Product> products = getProducts1();
        for (Product p : products) {
            //每条数据创建一个Document,并把这个Document放进索引里。
            Document doc = new Document();
            // 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
            // 这里我们name字段需要用TextField,即创建索引又会被分词。StringField会创建索引,但是不会被分词
            doc.add(new TextField("id", String.valueOf(p.getId()), Field.Store.YES));
            doc.add(new TextField("name", p.getName(), Field.Store.YES));
            doc.add(new TextField("category", p.getCategory(), Field.Store.YES));
            doc.add(new TextField("price", String.valueOf(p.getPrice()), Field.Store.YES));
            doc.add(new TextField("place", p.getPlace(), Field.Store.YES));
            doc.add(new TextField("code", p.getCode(), Field.Store.YES));
            indexWriter.addDocument(doc);
        }
    }

    /**
     * @Description 从文件中获取模拟产品数据
     * @Date 2020/9/17 16:25
     * @Author tanp
     */
    private List<Product> getProducts1() throws Exception {
        String fileName = "140k_products.txt";
        File file = new File(fileName);
        FileInputStream fileInputStream = new FileInputStream(file);
        InputStreamReader isr = new InputStreamReader(fileInputStream, "UTF-8");
        BufferedReader br = new BufferedReader(isr);
        String line;
        List<Product> products = new ArrayList<>();
        while ((line = br.readLine()) != null) {
            Product p = new Product();
            String[] fields = line.split(",");
            p.setId(Integer.parseInt(fields[0]));
            p.setName(fields[1]);
            p.setCategory(fields[2]);
            p.setPrice(Float.parseFloat(fields[3]));
            p.setPlace(fields[4]);
            p.setCode(fields[5]);
            products.add(p);
        }
        br.close();
        isr.close();
        fileInputStream.close();
        return products;
    }

    /**
     * @Description 手动输入产品信息模拟数据
     * @Date 2020/9/17 15:21
     * @Author tanp
     */
    private List<Product> getProducts() {
        List<Product> products = new ArrayList<>();
        products.add(new Product(0, "飞利浦led灯泡e27螺口暖白球泡灯家用照明超亮节能灯泡转色温灯泡", "照明用品", 23.01F, "湖南长沙", "000"));
        products.add(new Product(1, "飞利浦led灯泡e14螺口蜡烛灯泡3W尖泡拉尾节能灯泡暖黄光源Lamp", "照明用品", 23.01F, "湖南长沙", "001"));
        products.add(new Product(2, "雷士照明 LED灯泡 e27大螺口节能灯3W球泡灯 Lamp led节能灯泡", "照明用品", 23.01F, "湖南长沙", "002"));
        products.add(new Product(3, "飞利浦 led灯泡 e27螺口家用3w暖白球泡灯节能灯5W灯泡LED单灯7w", "照明用品", 23.01F, "湖南长沙", "003"));
        products.add(new Product(4, "飞利浦led小球泡e14螺口4.5w透明款led节能灯泡照明光源lamp单灯", "照明用品", 23.01F, "湖南长沙", "004"));
        products.add(new Product(5, "飞利浦蒲公英护眼台灯工作学习阅读节能灯具30508带光源", "照明用品", 23.01F, "湖南长沙", "005"));
        return products;
    }

}

六.代码讲解

首先我们可以看到在Luncene01方法中一共分了6步,我们也根据这六大步来讲解一下知识点

1.Analyzer分词器

提供分词算法,可以把文档中的数据按照算法分词,如我们搜索护眼光源的关键字,我们首先看下假设查询数据库数据,我们会写下如下sql

select * from tableA where nameA like %护眼光源% (tableA为表名,nameA为字段名),无论如何写sql,数据库里存储的数据一定是护眼光源几个字连在一起的,而 Analyze分词器则可以把护眼光源几个字数据分开来

2.创建目录索引

a)索引目录对象

Directory是目录类描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常用的包括FSDirectory(在文件系统存储索引)、RAMDirectory(在内存存储索引)。

FSDirectoryRAMDirectory是内存目录,会把索引库保存在内存,特点是速度快,但不是很安全。
RAMDirectory是磁盘文件系统目录,会把索引库指向本地磁盘,特点是速度稍慢,但比较安全。

b)IndexWriterConfig

      索引写出器配置类,可以设置最大缓存文档数,对documents建立索引的线程池,索引段的合并策略等。这个类里面最重要的还是它里面的三个枚举变量CREATE,APPEND,CREATE_OR_APPEND,如代码案例中设置的就是create模式

      CREATE模式:这个模式下,每次新建的索引都会先清空上次索引的目录,然后在新建当前的索引,注意可以不用事先创建索引目录,这个模式一般是测试时候用的。
     APPEND模式:这个模式下,每次新添加的索引,会被追加到原来的索引里,有一点需要注意的是,如果这个索引路径不存在的话,这个操作,将会导致报出一个异常,所以,使用此模式前,务必确定你有一个已经创建好的索引。
     CREATE_OR_APPEND模式:这个模式就是我们默认 的模式,也是比较安全或者比较通用的模式,如果这个索引不存在,那么在此模式下就会新建一个索引目录,如果已存在,那么在添加文档的时候,直接会以Append的方式追加到索引里,所以此模式下,并不会出现一些意外的情况,所以大多数时候,建议使用此方式,进行构建索引。

C)IndexWriter索引写对象

IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。IndexWriter需要通过Directory对索引进行存储操作。

d) Document文档类

一个Document文档对象,是一条原始的数据,就相当于数据库中一条数据

e)Field 字段

一个Field就类似于数据库中的每个字段。一个Document中可以有很多个不同的字段,每一个字段都是一个Field类的对象。

一个Document中的字段其类型是不确定的,因此Field类就提供了各种不同的子类,来对应这些不同类型的字段。下边列出了开发中常用 的Filed类型,注意Field的属性,根据需求选择:

Field类数据类型Analyzed是否分词Indexed是否索引Stored是否存储说明
StringField(FieldName, FieldValue,Store.YES))字符串NYY或N这个Field用来构建一个字符串Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等)是否存储在文档中用Store.YES或Store.NO决定
LongField(FieldName, FieldValue,Store.YES)Long型YNY或N这个Field用来构建一个Long数字型Field,进行分词和索引,比如(价格)是否存储在文档中用Store.YES或Store.NO决定
StoredField(FieldName, FieldValue)重载方法,支持多种类型NNY这个Field用来构建不同类型Field不分析,不索引,但要Field存储在文档中
TextField(FieldName, FieldValue, Store.NO)或TextField(FieldName, reader)字符串或流YYY或N如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略.

      DoubleField、FloatField、IntField、LongField、StringField、TextField这些子类一定会被创建索引,但是不会被分词,而且不一定会被存储到文档列表。要通过构造函数中的参数Store来指定:如果Store.YES代表存储,Store.NO代表不存储

     TextField即创建索引,又会被分词。StringField会创建索引,但是不会被分词。如果不分词,会造成整个字段作为一个词条,除非用户完全匹配,否则搜索不到:

判断一个字段是否需要存储的前提是,该字段是否要显示到最终的结果中,如果要那么一定要存储,否则就不存储

判断一个字段是否需要创建索引的前提是,是否要要根据这个字段进行搜索

3.创建查询器

a) Query查询对象

包含要查询的关键词信息,可通过QueryParser来创建查询对象(常用的方法),也可通过Query子类来创建查询对象,Query子类常用的如下:
TermQuery:精确的词项查询,如 Query query = new TermQuery(new Term("name", "小王"));
NumericRangeQuery:数值范围查询,如 Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
BooleanQuery:组合查询 ,身没有查询条件,可以把其它查询通过逻辑运算进行组合

        //  交集:Occur.MUST + Occur.MUST
        //  并集:Occur.SHOULD + Occur.SHOULD
        //  非:Occur.MUST_NOT        
        Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
        Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
        // 创建布尔查询的对象
        BooleanQuery query = new BooleanQuery();
        // 组合其它查询
        query.add(query1, BooleanClause.Occur.MUST_NOT);
        query.add(query2, BooleanClause.Occur.SHOULD);

b) QueryParser(查询解析器)

QueryParser(单一字段的查询解析器),MultiFieldQueryParser(多字段的查询解析器)

4.获得搜索工具

a)IndexSearcher

索引搜索对象,执行搜索功能,实现快速搜索、排序、打分等功能。需要依赖IndexReader类

b) IndexReader

索引读取对象

5.查询数据

a)ScoreDoc

scoreDoc是得分文档对象,包含两部分数据。文档的唯一编号和文档的得分信息

b)TopDocs

查询结果对象,包含两部分数据,查询到的总条数信息、所有符合条件的得分文档数组

c)分页查询的两种方式

6.展示数据

高亮展示如代码中所展示

七、结果展示

如我在浏览器输入灯光

控制台打印结果如下

8、解析文件格式

本博客中涉及到解析文件数据,格式要求如下

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值