文本相似度分析实现

内容纲要

文本相似度分析实现

项目结构

image-20230509191839396

文本相似度

文本相似度指的是计算两篇文本的相似度,基于 MinHash算法思想。

调用举例:

text1 = file_util.read_whole_file('data/test001.txt')
text2 = file_util.read_whole_file('data/test002.txt')
similarity = text_similarity.calc_similarity(text1, text2)

输出:

data/test001.txt and data/test002.txt similarity: 0.94

abstract包

Auto_abstract

import os
from heapq import nlargest
from itertools import count, product

from nlp.utils import nlp_util

class AutoAbstract(object):
    def __init__(self, use_stopword=True,
                 stop_words_file='etc/stopwords.txt',
                 dict_path=None,
                 max_iter=100,
                 tol=0.0001):
        if dict_path:
            raise RuntimeError("True")
        self.__use_stopword = use_stopword
        self.__dict_path = dict_path
        self.__max_iter = max_iter
        self.__tol = tol

        self.__stop_words = set()
        if stop_words_file:
            self.__stop_words_file = stop_words_file
        if use_stopword:
            for word in open(self.__stop_words_file, 'r', encoding='utf-8'):
                self.__stop_words.add(word.strip())

    def generate_abstract(self, text_content, sentence_num):
        """
        提取文本的主题思想(摘要)
        :param text_content: 文本内容
        :param sentence_num: 取概率最高的前几句话作为摘要返回
        :return: 摘要
        """
        text_content = nlp_util.as_text(text_content)
        tokens = nlp_util.cut_sentences(text_content)
        sentences, sents = nlp_util.cut_filter_words(tokens, self.__stop_words, self.__use_stopword)

        graph = self.create_graph(sents)
        scores = nlp_util.weight_map_rank(graph, self.__max_iter, self.__tol)
        sent_selected = nlargest(sentence_num, zip(scores, count()))
        if sentence_num > len(sent_selected):
            sentence_num = len(sent_selected)
        sent_index = []
        for i in range(sentence_num):
            sent_index.append(sent_selected[i][1])

        result_sent = [sentences[i] for i in sent_index]
        return os.linesep.join(result_sent).replace(' ', '')

    @staticmethod
    def create_graph(word_sent):
        num = len(word_sent)
        board = [[0.0 for _ in range(num)] for _ in range(num)]

        for i, j in product(range(num), repeat=2):
            if i != j:
                board[i][j] = nlp_util.sentences_similarity(word_sent[i], word_sent[j])
        return board

nroute

import re
from math import log

from nlp.abstract.perceptron import Perceptron

re_eng = re.compile('[a-zA-Z0-9]', re.U)
re_han = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._%\-]+)", re.U)
re_skip = re.compile("(\r\n|\s)", re.U)

class Segment:
    def __init__(self):
        self.vocab = {}
        self.max_word_len = 0
        self.max_freq = 0
        self.total_freq = 0
        self.initialized = False
        self.model = None

    def init(self, vocab_path='data/abstract/dict/jiagu.dict', user_vocab='data/abstract/dict/user.dict',
             model_path='data/abstract/model/cws.model'):
        self.load_vocab(vocab_path)
        self.load_vocab(user_vocab)
        self.model = Perceptron(model_path)
        self.initialized = True

    def load_vocab(self, vocab_path):
        fin = open(vocab_path, 'r', encoding='utf8')
        for index, line in enumerate(fin):
            line = line.strip()
            if line == '':
                continue
            word_freq_tag = line.split('\t')
            if len(word_freq_tag) == 1:
                word = word_freq_tag[0]
                self.add_vocab(word)
            elif len(word_freq_tag) == 2:
                word = word_freq_tag[0]
                freq = int(word_freq_tag[1])
                self.add_vocab(word, freq)
        fin.close()

    def add_vocab(self, word=None, freq=None, tag=None):
        if freq == None:
            freq = self.max_freq

        if word not in self.vocab:
            self.vocab[word] = 0

        self.vocab[word] += freq
        self.total_freq += freq

        if freq > self.max_freq:
            self.max_freq = freq

        if len(word) > self.max_word_len:
            self.max_word_len = len(word)

    def del_vocab(self, word=None, freq=None, tag=None):
        if word not in self.vocab:
            return None

        vocab_freq = self.vocab[word]
        if freq == None or vocab_freq <= freq:
            del self.vocab[word]
            self.total_freq -= vocab_freq
        else:
            self.vocab[word] -= freq

    # self.max_freq and self.max_word_len ?

    def load_userdict(self, userdict):
        if self.initialized == False:
            self.init()

        if isinstance(userdict, str):
            self.load_vocab(userdict)

        for item in userdict:
            if isinstance(item, list):
                if len(item) == 1:
                    word = item[0]
                    self.add_vocab(word)
                elif len(item) == 2:
                    word = item[0]
                    freq = item[1]
                    self.add_vocab(word, freq)
            elif isinstance(item, str):
                self.add_vocab(word=item)

    def del_userdict(self, userdict):
        if self.initialized == False:
            self.init()

        for item in userdict:
            if isinstance(item, list):
                if len(item) == 1:
                    word = item[0]
                    self.del_vocab(word)
                elif len(item) == 2:
                    word = item[0]
                    freq = item[1]
                    self.del_vocab(word, freq)
            elif isinstance(item, str):
                self.del_vocab(word=item)

    def calc_route(self, sentence, DAG, route):
        vocab = self.vocab
        N = len(sentence)
        route[N] = (0, 0)
        logtotal = log(self.total_freq)
        for idx in range(N - 1, -1, -1):
            route[idx] = max(
                (log(vocab.get(sentence[idx:x + 1]) or 1) - logtotal + route[x + 1][0], x) for x in DAG[idx])

    def create_DAG(self, sentence):
        vocab = self.vocab
        max_word_len = self.max_word_len
        DAG = {}
        N = len(sentence)
        for idx in range(N):
            cand_idx = [idx]
            for i in range(idx + 1, idx + min(max_word_len, N - idx), 1):
                cand = sentence[idx: i + 1]
                if cand in vocab:
                    cand_idx.append(i)
            DAG[idx] = cand_idx
        return DAG

    def cut_search(self, sentence):
        DAG = self.create_DAG(sentence)
        old_j = -1
        for k, L in DAG.items():
            if len(L) == 1 and k > old_j:
                yield sentence[k:L[0] + 1]
                old_j = L[0]
            else:
                for j in L:
                    if j > k:
                        yield sentence[k:j + 1]
                        old_j = j

    def cut_vocab(self, sentence):
        DAG = self.create_DAG(sentence)
        route = {}
        self.calc_route(sentence, DAG, route)

        x = 0
        N = len(sentence)
        buf = ''
        while x < N:
            y = route[x][1] + 1
            l_word = sentence[x:y]
            if buf:
                yield buf
                buf = ''
            yield l_word
            x = y
        if buf:
            yield buf
            buf = ''

    def cut_words(self, sentence):
        DAG = self.create_DAG(sentence)
        route = {}
        self.calc_route(sentence, DAG, route)
        x = 0
        N = len(sentence)
        buf = ''
        while x < N:
            y = route[x][1] + 1
            l_word = sentence[x:y]
            if re_eng.match(l_word) and len(l_word) == 1:
                buf += l_word
                x = y
            else:
                if buf:
                    yield buf
                    buf = ''
                yield l_word
                x = y
        if buf:
            yield buf
            buf = ''

    def model_cut(self, sentence):
        if sentence == '':
            return ['']

        sentence = list(sentence)
        labels = self.model.predict(sentence)
        return self.__lab2word(sentence, labels)

    def __lab2word(self, sentence, labels):
        sen_len = len(sentence)
        tmp_word = ""
        words = []
        for i in range(sen_len):
            label = labels[i]
            w = sentence[i]
            if label == "B":
                tmp_word += w
            elif label == "M":
                tmp_word += w
            elif label == "E":
                tmp_word += w
                words.append(tmp_word)
                tmp_word = ""
            else:
                if tmp_word != '':
                    words.append(tmp_word)
                    tmp_word = ""
                words.append(w)
        if tmp_word:
            words.append(tmp_word)
        return words

    def seg_default(self, sentence):
        blocks = re_han.split(sentence)
        cut_block = self.cut_words
        cut_all = False
        for block in blocks:
            if not block:
                continue
            if re_han.match(block):
                for word in cut_block(block):
                    yield word
            else:
                tmp = re_skip.split(block)
                for x in tmp:
                    if re_skip.match(x):
                        yield x
                    elif not cut_all:
                        for xx in x:
                            yield xx
                    else:
                        yield x

    def seg_new_word(self, sentence):
        blocks = re_han.split(sentence)
        cut_block = self.cut_words
        cut_all = False
        for block in blocks:
            if not block:
                continue
            if re_han.match(block):
                words1 = list(cut_block(block))
                # print(words1)

                words2 = self.model_cut(block)
                # print(words2)

                new_word = []  # 有冲突的不加,长度大于4的不加,加完记得删除
                length = len(words1)
                for n in range(3):
                    can_limit = length - n + 1
                    for i in range(0, can_limit):
                        ngram = ''.join(words1[i:i + n])
                        word_len = len(ngram)
                        if word_len > 4 or word_len == 1:
                            continue
                        if ngram in words2 and ngram not in words1:
                            # print(ngram)
                            new_word.append([ngram, 1])

                # new_word = []
                # for word in words2:
                #   if word not in words1 and len(word)>1 and len(word) < 4 :#and not re_eng.match(word):
                #       new_word.append([word, 1])

                self.load_userdict(new_word)

                # print('------------------')

                for word in cut_block(block):
                    yield word

                # 删除字典
                self.del_userdict(new_word)

            else:
                tmp = re_skip.split(block)
                for x in tmp:
                    if re_skip.match(x):
                        yield x
                    elif not cut_all:
                        for xx in x:
                            yield xx
                    else:
                        yield x

    def seg(self, sentence, mode="default"):
        if self.initialized == False:
            self.init()

        if mode == 'probe':
            return list(self.seg_new_word(sentence))
        else:
            return list(self.seg_default(sentence))

perceptron

# -*- coding:utf-8 -*-
import gzip
import pickle
import random
from collections import defaultdict

class AveragedPerceptron(object):
    def __init__(self):
        # Each feature gets its own weight vector, so weights is a dict-of-dicts
        self.weights = {}
        self.classes = set()
        # The accumulated values, for the averaging. These will be keyed by
        # feature/clas tuples
        self._totals = defaultdict(int)
        # The last time the feature was changed, for the averaging. Also
        # keyed by feature/clas tuples
        # (tstamps is short for timestamps)
        self._tstamps = defaultdict(int)
        # Number of instances seen
        self.i = 0

    def predict(self, features):
        '''Dot-product the features and current weights and return the best label.'''
        scores = defaultdict(float)
        for feat, value in features.items():
            if feat not in self.weights or value == 0:
                continue
            weights = self.weights[feat]
            for label, weight in weights.items():
                scores[label] += value * weight
        # Do a secondary alphabetic sort, for stability
        return max(self.classes, key=lambda label: (scores[label], label))

    def update(self, truth, guess, features):
        '''Update the feature weights.'''

        def upd_feat(c, f, w, v):
            param = (f, c)
            self._totals[param] += (self.i - self._tstamps[param]) * w
            self._tstamps[param] = self.i
            self.weights[f][c] = w + v

        self.i += 1
        if truth == guess:
            return None
        for f in features:
            weights = self.weights.setdefault(f, {})
            upd_feat(truth, f, weights.get(truth, 0.0), 1.0)
            upd_feat(guess, f, weights.get(guess, 0.0), -1.0)
        return None

    def average_weights(self):
        '''Average weights from all iterations.'''
        for feat, weights in self.weights.items():
            new_feat_weights = {}
            for clas, weight in weights.items():
                param = (feat, clas)
                total = self._totals[param]
                total += (self.i - self._tstamps[param]) * weight
                averaged = round(total / float(self.i), 3)
                if averaged:
                    new_feat_weights[clas] = averaged
            self.weights[feat] = new_feat_weights
        return None

class Perceptron:
    def __init__(self, loc=None):
        self.START = ['-START-', '-START2-']
        self.END = ['-END-', '-END2-']
        self.model = AveragedPerceptron()

        if loc != None:
            self.load(loc)

    def predict(self, words):
        prev, prev2 = self.START
        labels = []
        context = self.START + words + self.END
        for i, word in enumerate(words):
            features = self._get_features(i, word, context, prev, prev2)
            tag = self.model.predict(features)
            labels.append(tag)
            prev2 = prev
            prev = tag
        return labels

    def train(self, sentences, save_loc=None, nr_iter=5, shuf=False):
        self._make_tagdict(sentences)
        for iter_ in range(nr_iter):
            c = 0
            n = 0
            for words, tags in sentences:
                prev, prev2 = self.START
                context = self.START + words + self.END
                for i, word in enumerate(words):
                    feats = self._get_features(i, word, context, prev, prev2)
                    guess = self.model.predict(feats)
                    self.model.update(tags[i], guess, feats)

                    prev2 = prev
                    prev = guess
                    c += guess == tags[i]
                    n += 1
            if shuf == True:
                random.shuffle(sentences)

            print("Iter {0}: {1}/{2}={3}".format(iter_, c, n, (float(c) / n) * 100))
            self.save(save_loc)

        self.model.average_weights()
        self.save(save_loc)

    def save(self, loc='model/ap.model', zip=True):
        if zip == False:
            pickle.dump((self.model.weights, self.model.classes), open(loc, 'wb'))
        else:
            pickle.dump((self.model.weights, self.model.classes), gzip.open(loc, 'wb'))

    def load(self, loc='model/ap.model', zip=True):
        if zip == False:
            self.model.weights, self.model.classes = pickle.load(open(loc, 'rb'))
        else:
            self.model.weights, self.model.classes = pickle.load(gzip.open(loc, 'rb'))

    def _get_features(self, i, word, context, prev, prev2):
        '''Map tokens into a feature representation, implemented as a
        {hashable: float} dict. If the features change, a new model must be
        trained.
        '''

        def add(name, *args):
            features[' '.join((name,) + tuple(args))] += 1

        i += len(self.START)
        features = defaultdict(int)
        # It's useful to have a constant feature, which acts sort of like a prior
        add('bias')
        add('i suffix', word[-3:])
        add('i pref1', word[0])
        add('i-1 tag', prev)
        add('i-2 tag', prev2)
        add('i tag+i-2 tag', prev, prev2)
        add('i word', context[i])
        add('i-1 tag+i word', prev, context[i])
        add('i-1 word', context[i - 1])
        add('i-1 suffix', context[i - 1][-3:])
        add('i-2 word', context[i - 2])
        add('i+1 word', context[i + 1])
        add('i+1 suffix', context[i + 1][-3:])
        add('i+2 word', context[i + 2])
        return features

    def _make_tagdict(self, sentences):
        '''Make a tag dictionary for single-tag words.'''
        for words, tags in sentences:
            for word, tag in zip(words, tags):
                self.model.classes.add(tag)

def train(filepath='data/train.txt', model='model/ap.model', nr_iter=1):
    tagger = Perceptron()
    print('Reading corpus...')
    training_data = []
    sentence = ([], [])
    fin = open(filepath, 'r', encoding='utf8')
    for index, line in enumerate(fin):
        line = line.strip()
        if line == '':
            training_data.append(sentence)
            sentence = ([], [])
        else:
            params = line.split()
            if len(params) != 2: continue
            sentence[0].append(params[0])
            sentence[1].append(params[1])
    fin.close()
    print('training corpus size : %d', len(training_data))
    print('Start training...')
    tagger.train(training_data, save_loc=model, nr_iter=nr_iter)

def eval(filepath='data/test.txt', model='model/ap.model'):
    tagger = Perceptron(model)

    print('Start testing...')
    right = 0.0
    total = 0.0
    sentence = ([], [])
    fin = open(filepath, 'r', encoding='utf8')
    for index, line in enumerate(fin):
        line = line.strip()
        if line == '':
            words = sentence[0]
            tags = sentence[1]
            outputs = tagger.predict(words)
            assert len(tags) == len(outputs)
            total += len(tags)
            for o, t in zip(outputs, tags):
                if o == t: right += 1
            sentence = ([], [])
        else:
            params = line.split()
            if len(params) != 2: continue
            sentence[0].append(params[0])
            sentence[1].append(params[1])
    fin.close()
    print("Precision : %f", right / total)

def predict(model='model/ap.model'):
    tagger = Perceptron(model)

    while True:
        text = input('>')
        words = list(text)
        labels = tagger.predict(words)

        for word, label in zip(words, labels):
            print(word, label)

if __name__ == '__main__':
    train()
    eval()
# predict()

similarity包

Text_similarity


import nltk
from datasketch import MinHash

ngrams_num = 3

def calc_similarity(text1, text2):
    """
    计算两段文本的相似度
    :param text1:
    :param text2:
    :return:
    """
    # print(text1)
    # print(text2)
    m1, m2 = MinHash(), MinHash()
    for d in nltk.ngrams(text1, ngrams_num):
        m1.update("".join(d).encode('utf8'))
    for d in nltk.ngrams(text2, ngrams_num):
        m2.update("".join(d).encode('utf8'))

    s1 = set(text1)
    s2 = set(text2)
    sty = float(len(s1.intersection(s2))) / float(len(s1.union(s2)))

    return round(sty, 2)

utils包

File_util

import hashlib
import os
import traceback

import chardet

DEFAULT_ENCODING = 'utf-8'

def write_lines(lines, file_name):
    """
    把字符串数组写入到指定文件里
    :param lines:
    :param file_name:
    :return:
    """
    with open(file_name, 'w', encoding=DEFAULT_ENCODING) as text_file:
        text_file.write('\n'.join(lines))

def append_line(line, file_name):
    """
    往指定的文件追加一行
    :param line:
    :param file_name:
    :return:
    """
    with open(file_name, 'a', encoding=DEFAULT_ENCODING) as text_file:
        text_file.write(line + '\n')

def append_lines(lines, file_name):
    """
    往指定的文件追加多行
    :param lines:
    :param file_name:
    :return:
    """
    with open(file_name, 'a', encoding=DEFAULT_ENCODING) as text_file:
        text_file.write('\n'.join(lines))

def read_whole_file(file_name):
    """
    读取文本文件的所有内容
    :param file_name:
    :return:
    """
    if not os.path.exists(file_name):
        return ''

    with open(file_name, 'rb') as text_file:
        content = text_file.read()
        encoding = chardet.detect(content)['encoding']
        return content.decode(encoding)

def read_lines(file_name):
    """
    按行读取文本文件并返回
    :param file_name:
    :return:
    """
    content = read_whole_file(file_name).replace('\r', '')
    return content.split('\n')

def calc_file_md5(file_name):
    with open(file_name, 'rb') as reader:
        content = reader.read()

    return hashlib.md5(content).hexdigest()

def get_text_file_encoding(file_name):
    if not os.path.exists(file_name):
        return ''

    with open(file_name, 'rb') as text_file:
        content = text_file.read()
        return chardet.detect(content)['encoding']

def move_file_2_dic(source_file, dest_dic):
    """
    移动文件到目标文件夹,若目标文件夹下有同名文件直接覆盖
    :param source_file:
    :param dest_dic:
    :return:
    """
    target_file = os.path.join(dest_dic, source_file[source_file.rfind('/') + 1:])

    if os.path.exists(target_file):
        os.remove(target_file)
    os.rename(source_file, target_file)

def get_file_suffix(file_name):
    """
    获取文件后缀
    :param file_name:
    :return:
    """
    if file_name is None:
        return ''

    strs = file_name.split('.')
    if len(strs) > 1:
        return strs[len(strs) - 1]

    return ''

def delete_file(file_path):
    """
    删除文件(不会抛异常)
    :param file_path:
    :return:
    """
    try:
        if os.path.exists(file_path):
            os.remove(file_path)
    except:
        print('delete_file: {} error!! {}'.format(file_path, traceback.format_exc()))

Nlp_util

import codecs
import math

from jieba import posseg

from nlp.abstract.nroute import Segment

def different(scores, old_scores, tol=0.0001):
    flag = False
    for i in range(len(scores)):
        if math.fabs(scores[i] - old_scores[i]) >= tol:  # 原始是0.0001
            flag = True
            break
    return flag

def sentences_similarity(s1, s2):
    """计算两个句子的相似度

    :param s1: list
    :param s2: list
    :return: float
    """
    counter = 0
    for sent in s1:
        if sent in s2:
            counter += 1
    if counter == 0:
        return 0
    return counter / (math.log(len(s1) + len(s2)))

def cut_words(text, stop_flag=['x', 'c', 'u', 'd', 'p', 't', 'uj', 'm', 'f', 'r']):
    """ 将文本切成词语 """
    stop_words = codecs.open('etc/stopwords.txt', 'r', encoding='utf8').readlines()
    stop_words = [w.strip() for w in stop_words]

    result = []
    words = posseg.cut(text)
    for word, flag in words:
        if flag not in stop_flag and word not in stop_words:
            result.append(word)
    return result

def cut_sentences(text):
    """
    将文本切成句子
    :param text: 待切分文本
    :return: 句子列表
    """
    text = text.translate(str.maketrans('!?!?\r\n', '。。。。  '))
    return [sen for sen in text.split('。') if len(sen.strip()) > 1]

def as_text(v):
    """生成unicode字符串"""
    if v is None:
        return None
    elif isinstance(v, bytes):
        return v.decode('utf-8', errors='ignore')
    elif isinstance(v, str):
        return v
    else:
        raise ValueError('Unknown type %r' % type(v))

def cut_filter_words(cutted_sentences, stopwords, use_stopwords=False):
    seg = Segment()

    sentences = []
    sents = []
    for sent in cutted_sentences:
        sentences.append(sent)
        if use_stopwords:
            sents.append([word for word in seg.seg(sent) if word and word not in stopwords])  # 把句子分成词语
        else:
            sents.append([word for word in seg.seg(sent) if word])
    return sentences, sents

def weight_map_rank(weight_graph, max_iter, tol):
    # 初始分数设置为0.5
    # 初始化每个句子的分子和老分数
    scores = [0.5 for _ in range(len(weight_graph))]
    old_scores = [0.0 for _ in range(len(weight_graph))]
    denominator = get_degree(weight_graph)

    # 开始迭代
    count = 0
    while different(scores, old_scores, tol):
        for i in range(len(weight_graph)):
            old_scores[i] = scores[i]
        # 计算每个句子的分数
        for i in range(len(weight_graph)):
            scores[i] = get_score(weight_graph, denominator, i)
        count += 1
        if count > max_iter:
            break
    return scores

def get_degree(weight_graph):
    length = len(weight_graph)
    denominator = [0.0 for _ in range(len(weight_graph))]
    for j in range(length):
        for k in range(length):
            denominator[j] += weight_graph[j][k]
        if denominator[j] == 0:
            denominator[j] = 1.0
    return denominator

def get_score(weight_graph, denominator, i):
    """

    :param weight_graph:
    :param denominator:
    :param i: int
        第i个句子
    :return: float
    """
    length = len(weight_graph)
    d = 0.85
    added_score = 0.0

    for j in range(length):
        # [j,i]是指句子j指向句子i
        fraction = weight_graph[j][i] * 1.0
        # 除以j的出度
        added_score += fraction / denominator[j]
    weighted_score = (1 - d) + d * added_score
    return weighted_score

测试

from nlp.similarity import text_similarity
from nlp.utils import file_util

def text_similarity_demo():
    """ 文本相似度 """
    file1 = '/Users/davidlee/Desktop/python/sentence_demo02/data/test001.txt'
    file2 = '/Users/davidlee/Desktop/python/sentence_demo02/data/test002.txt'
    file3 = '/Users/davidlee/Desktop/python/sentence_demo02/data/test005.txt'
    text1 = file_util.read_whole_file(file1)
    text2 = file_util.read_whole_file(file2)
    text3 = file_util.read_whole_file(file3)
    sty1 = text_similarity.calc_similarity(text1, text2)
    sty2 = text_similarity.calc_similarity(text1, text3)

    print("{} and {} similarity: {}".format(file1, file2, sty1))
    print("{} and {} similarity: {}".format(file1, file3, sty2))
    print('--' * 55)

if __name__ == '__main__':
    text_similarity_demo()

requirments

nltk~=3.6.7
setuptools~=28.8.0
datasketch~=1.5.9
chardet~=5.0.0
jieba~=0.42.1

setup

from distutils.core import setup

from setuptools import find_packages

LONGDOC = """
nlp tools
"""
setup(name='sentence_demo1',
      version='1.0.0',
      description='sentence_demo1',
      long_description=LONGDOC,
      author='Li',
      url='https://xn--rgv085f.tech/',
      classifiers=[
          'Intended Audience :: Developers',
          'Operating System :: OS Independent',
          'Natural Language :: Chinese (Simplified)',
          'Natural Language :: Chinese (Traditional)',
          'Programming Language :: Python',
          'Programming Language :: Python :: 3.6',
          'Topic :: sentence_demo1'
      ],
      keywords='sentence',
      packages=find_packages('sentence'),
      package_dir={'': 'sentence'},
      package_data={'sentence': ['*.*', '*/*']}
      )

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注