100py小服务: FastAPI+spaCy+jieba+scikit-learn+sumy简单中英文 NLP

399 阅读10分钟

自然语言处理(NLP)是计算机科学中的一个重要领域,涉及到计算机对人类语言的理解、解释和生成。本文将介绍如何使用 FastAPI 和多个 NLP 库(如 spaCy、jieba、scikit-learn 和 sumy)构建一个支持中英文的多功能 NLP API。

依赖包介绍

spaCy

spaCy 是一个先进的自然语言处理库,提供了高效且易用的 NLP 工具。它支持多种语言,并提供了分词、词性标注、命名实体识别、依存句法分析等功能。

jieba

jieba 是一个中文分词库,提供了高效的中文分词功能。它支持三种分词模式:精确模式、全模式和搜索引擎模式,并提供了关键词提取等功能。

scikit-learn

scikit-learn 是一个用于数据挖掘和数据分析的 Python 库,提供了各种机器学习算法和工具。在本文中,我们将使用它的 TF-IDF 向量化工具进行关键词提取。

sumy

sumy 是一个文本摘要库,支持多种文本摘要算法。在本文中,我们将使用 LSA(Latent Semantic Analysis,潜在语义分析)算法来生成英文文本摘要。

安装依赖

首先,确保你已经安装了 FastAPI 和上述的 NLP 库。你可以使用以下命令来安装这些依赖:

pip install fastapi uvicorn spacy jieba scikit-learn sumy
python -m spacy download en_core_web_sm
python -m spacy download zh_core_web_sm

创建 FastAPI 应用

创建一个新的 Python 文件(例如 main.py),并在其中编写基础代码:

from fastapi import FastAPI
from pydantic import BaseModel
import spacy

# 加载 spaCy 模型
nlp_en = spacy.load("en_core_web_sm")
nlp_zh = spacy.load("zh_core_web_sm")

# 创建 FastAPI 应用
app = FastAPI()

# 定义请求体模型
class TextRequest(BaseModel):
    text: str
    lang: str  # 添加语言字段

# 根据语言选择模型
def get_nlp(lang: str):
    if lang == 'en':
        return nlp_en
    elif lang == 'zh':
        return nlp_zh
    else:
        return None

# 运行应用
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

功能实现

1. 分词 (Tokenization)

分词是将文本分割成单独的词或标记的过程。它是 NLP 的基础步骤之一。在中文中,分词尤为重要,因为中文文本中没有明确的单词边界。

实现

@app.post("/tokenize")
async def tokenize(request: TextRequest):
    if request.lang == 'zh':
        tokens = list(jieba.cut(request.text))
    else:
        nlp = get_nlp(request.lang)
        if not nlp:
            return {"error": "Unsupported language"}
        doc = nlp(request.text)
        tokens = [token.text for token in doc]
    return {"tokens": tokens}

2. 词性标注 (Part-of-Speech Tagging)

词性标注为每个词分配词性标签,如名词、动词、形容词等。这对于理解句子的语法结构和意义至关重要。

实现

@app.post("/pos")
async def pos_tagging(request: TextRequest):
    if request.lang == 'zh':
        words = list(jieba.cut(request.text))
        pos_tags = [{"text": word, "pos": "N/A"} for word in words]  # jieba 不支持词性标注,返回 "N/A"
    else:
        nlp = get_nlp(request.lang)
        if not nlp:
            return {"error": "Unsupported language"}
        doc = nlp(request.text)
        pos_tags = [{"text": token.text, "pos": token.pos_} for token in doc]
    return {"pos_tags": pos_tags}

3. 命名实体识别 (Named Entity Recognition, NER)

命名实体识别用于识别并分类文本中的命名实体,如人名、地名、组织名等。这有助于提取文本中的关键信息。

实现

@app.post("/ner")
async def named_entity_recognition(request: TextRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "Unsupported language"}
    
    doc = nlp(request.text)
    entities = [{"text": ent.text, "label": ent.label_} for ent in doc.ents]
    return {"entities": entities}

4. 依存句法分析 (Dependency Parsing)

依存句法分析用于分析句子的语法结构,确定词与词之间的依存关系。这对于理解句子的深层结构和意义非常重要。

实现

@app.post("/dependency")
async def dependency_parsing(request: TextRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "Unsupported language"}
    
    doc = nlp(request.text)
    dependencies = [{"text": token.text, "dep": token.dep_, "head": token.head.text} for token in doc]
    return {"dependencies": dependencies}

5. 词向量相似度 (Word Vector Similarity)

词向量相似度用于计算两个文本的相似度分数。通过将文本转换为向量,可以计算它们在向量空间中的距离,从而确定它们的相似程度。

实现

class SimilarityRequest(BaseModel):
    text1: str
    text2: str
    lang: str

@app.post("/similarity")
async def text_similarity(request: SimilarityRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "Unsupported language"}
    
    doc1 = nlp(request.text1)
    doc2 = nlp(request.text2)
    similarity = doc1.similarity(doc2)
    return {"similarity": similarity}

6. 自定义规则匹配 (Rule-based Matching)

规则匹配用于根据特定的模式在文本中查找匹配项。通过定义规则,可以灵活地查找特定的文本模式。

实现

from spacy.matcher import Matcher

@app.post("/match")
async def rule_based_matching(request: TextRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "Unsupported language"}
    
    doc = nlp(request.text)
    matcher = Matcher(nlp.vocab)
    pattern = [{"LOWER": "apple"}, {"LEMMA": "be"}, {"POS": "VERB"}, {"POS": "ADV", "OP": "?"}, {"LOWER": "buy"}]
    matcher.add("APPLE_BUY", [pattern])
    matches = matcher(doc)
    matched_texts = [doc[start:end].text for match_id, start, end in matches]
    return {"matches": matched_texts}

7. 提取关键词 (Keyword Extraction)

关键词提取用于从文本中提取出最重要的词或短语。这对于理解文本的主要内容非常有用。

实现

class KeywordsRequest(TextRequest):
    topK: Optional[int] = Field(10, example=10, description="要提取的关键词数量")

@app.post("/keywords", summary="关键词提取", description="提取输入文本中的关键词。")
async def extract_keywords(request: KeywordsRequest):
    if request.lang == 'zh':
        # 使用 jieba.analyse.textrank 提取关键词
        keywords = jieba.analyse.textrank(request.text, topK=request.topK, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'))
    else:
        # 使用 TF-IDF 提取英文关键词
        vectorizer = TfidfVectorizer(stop_words='english')
        X = vectorizer.fit_transform([request.text])
        indices = X[0].nonzero()[1]
        scores = zip(indices, [X[0, x] for x in indices])
        sorted_scores = sorted(scores, key=lambda x: x[1], reverse=True)
        feature_names = vectorizer.get_feature_names_out()
        keywords = [feature_names[i] for i, score in sorted_scores[:request.topK]]
    return {"keywords": keywords}

8. 文本摘要 (Text Summarization)

文本摘要用于生成文本的简短摘要,提取出文本中的主要信息。这对于快速理解长文本非常有用。

实现

class SummaryRequest(TextRequest):
    sentence_count: Optional[int] = Field(2, example=2, description="摘要中的句子数量")

@app.post("/summarize", summary="文本摘要", description="对输入文本进行摘要,并返回摘要结果。")
async def summarize_text(request: SummaryRequest):
    if request.lang == 'zh':
        # 使用 jieba 分词和简单的句子分割来处理中文文本摘要
        sentences = request.text.split('。')  # 简单的句子分割
        sentences = [sentence for sentence in sentences if sentence]  # 去除空句子

        # 如果句子数量少于请求的摘要句子数量,返回所有句子
        if len(sentences) <= request.sentence_count:
            return {"summary": sentences}

        # 使用 TF-IDF 提取重要句子
        vectorizer = TfidfVectorizer()
        X = vectorizer.fit_transform(sentences)
        sentence_scores = X.sum(axis=1).A1  # 计算每个句子的TF-IDF得分
        ranked_sentences = [sentences[i] for i in sentence_scores.argsort()[::-1]]  # 按得分降序排列句子

        summary = ranked_sentences[:request.sentence_count]
    else:
        parser = PlaintextParser.from_string(request.text, Tokenizer("english"))
        summarizer = LsaSummarizer()
        summary = summarizer(parser.document, request.sentence_count)
        summary = [str(sentence) for sentence in summary]

    return {"summary": summary}

完整代码

以下是包含所有功能实现和 FastAPI 应用完整结构的代码示例:

from fastapi import FastAPI
from pydantic import BaseModel, Field
import spacy
import jieba
import jieba.analyse
from sklearn.feature_extraction.text import TfidfVectorizer
from spacy.matcher import Matcher
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lsa import LsaSummarizer
from typing import List, Optional

# 加载 spaCy 模型
nlp_en = spacy.load("en_core_web_sm")
nlp_zh = spacy.load("zh_core_web_sm")

# 创建 FastAPI 应用
app = FastAPI(title="多语言 NLP API", description="一个支持中英文的多功能 NLP API 应用", version="1.0")

# 定义请求体模型
class TextRequest(BaseModel):
    text: str = Field(..., example="苹果公司正在考虑收购一家英国初创公司。")
    lang: str = Field(..., example="zh")

class SimilarityRequest(BaseModel):
    text1: str = Field(..., example="我喜欢猫。")
    text2: str = Field(..., example="我喜欢狗。")
    lang: str = Field(..., example="zh")

class KeywordsRequest(TextRequest):
    topK: Optional[int] = Field(10, example=10, description="要提取的关键词数量")

class SummaryRequest(TextRequest):
    sentence_count: Optional[int] = Field(2, example=2, description="摘要中的句子数量")

# 根据语言选择模型
def get_nlp(lang: str):
    if lang == 'en':
        return nlp_en
    elif lang == 'zh':
        return nlp_zh
    else:
        return None

# 分词
@app.post("/tokenize", summary="分词", description="对输入文本进行分词,并返回分词结果。")
async def tokenize(request: TextRequest):
    if request.lang == 'zh':
        tokens = list(jieba.cut(request.text))
    else:
        nlp = get_nlp(request.lang)
        if not nlp:
            return {"error": "不支持的语言"}
        doc = nlp(request.text)
        tokens = [token.text for token in doc]
    return {"tokens": tokens}

# 词性标注
@app.post("/pos", summary="词性标注", description="对输入文本进行词性标注,并返回标注结果。")
async def pos_tagging(request: TextRequest):
    if request.lang == 'zh':
        words = list(jieba.cut(request.text))
        pos_tags = [{"text": word, "pos": "N/A"} for word in words]  # jieba 不支持词性标注,返回 "N/A"
    else:
        nlp = get_nlp(request.lang)
        if not nlp:
            return {"error": "不支持的语言"}
        doc = nlp(request.text)
        pos_tags = [{"text": token.text, "pos": token.pos_} for token in doc]
    return {"pos_tags": pos_tags}

# 命名实体识别
@app.post("/ner", summary="命名实体识别", description="对输入文本进行命名实体识别,并返回识别结果。")
async def named_entity_recognition(request: TextRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "不支持的语言"}
    
    doc = nlp(request.text)
    entities = [{"text": ent.text, "label": ent.label_} for ent in doc.ents]
    return {"entities": entities}

# 依存句法分析
@app.post("/dependency", summary="依存句法分析", description="对输入文本进行依存句法分析,并返回分析结果。")
async def dependency_parsing(request: TextRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "不支持的语言"}
    
    doc = nlp(request.text)
    dependencies = [{"text": token.text, "dep": token.dep_, "head": token.head.text} for token in doc]
    return {"dependencies": dependencies}

# 词向量相似度
@app.post("/similarity", summary="文本相似度计算", description="计算两个文本之间的相似度,并返回相似度分数。")
async def text_similarity(request: SimilarityRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "不支持的语言"}
    
    doc1 = nlp(request.text1)
    doc2 = nlp(request.text2)
    similarity = doc1.similarity(doc2)
    return {"similarity": similarity}

# 自定义规则匹配
@app.post("/match", summary="规则匹配", description="对输入文本进行自定义规则匹配,并返回匹配结果。")
async def rule_based_matching(request: TextRequest):
    nlp = get_nlp(request.lang)
    if not nlp:
        return {"error": "不支持的语言"}
    
    doc = nlp(request.text)
    matcher = Matcher(nlp.vocab)
    pattern = [{"LOWER": "apple"}, {"LEMMA": "be"}, {"POS": "VERB"}, {"POS": "ADV", "OP": "?"}, {"LOWER": "buy"}]
    matcher.add("APPLE_BUY", [pattern])
    matches = matcher(doc)
    matched_texts = [doc[start:end].text for match_id, start, end in matches]
    return {"matches": matched_texts}

# 提取关键词
@app.post("/keywords", summary="关键词提取", description="提取输入文本中的关键词。")
async def extract_keywords(request: KeywordsRequest):
    if request.lang == 'zh':
        # 使用 jieba.analyse.textrank 提取关键词
        keywords = jieba.analyse.textrank(request.text, topK=request.topK, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'))
    else:
        # 使用 TF-IDF 提取英文关键词
        vectorizer = TfidfVectorizer(stop_words='english')
        X = vectorizer.fit_transform([request.text])
        indices = X[0].nonzero()[1]
        scores = zip(indices, [X[0, x] for x in indices])
        sorted_scores = sorted(scores, key=lambda x: x[1], reverse=True)
        feature_names = vectorizer.get_feature_names_out()
        keywords = [feature_names[i] for i, score in sorted_scores[:request.topK]]
    return {"keywords": keywords}

# 文本摘要
@app.post("/summarize", summary="文本摘要", description="对输入文本进行摘要,并返回摘要结果。")
async def summarize_text(request: SummaryRequest):
    if request.lang == 'zh':
        # 使用 jieba 分词和简单的句子分割来处理中文文本摘要
        sentences = request.text.split('。')  # 简单的句子分割
        sentences = [sentence for sentence in sentences if sentence]  # 去除空句子

        # 如果句子数量少于请求的摘要句子数量,返回所有句子
        if len(sentences) <= request.sentence_count:
            return {"summary": sentences}

        # 使用 TF-IDF 提取重要句子
        vectorizer = TfidfVectorizer()
        X = vectorizer.fit_transform(sentences)
        sentence_scores = X.sum(axis=1).A1  # 计算每个句子的TF-IDF得分
        ranked_sentences = [sentences[i] for i in sentence_scores.argsort()[::-1]]  # 按得分降序排列句子

        summary = ranked_sentences[:request.sentence_count]
    else:
        parser = PlaintextParser.from_string(request.text, Tokenizer("english"))
        summarizer = LsaSummarizer()
        summary = summarizer(parser.document, request.sentence_count)
        summary = [str(sentence) for sentence in summary]
        
    return {"summary": summary}

# 运行应用
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

运行和测试

保存完整代码到 main.py 文件中,然后在终端运行以下命令启动 FastAPI 应用:

uvicorn main:app --reload

启动后,你可以访问 http://127.0.0.1:8000/docs 查看自动生成的 API 文档,并进行测试。

总结

本文介绍了如何使用 FastAPI 和多个 NLP 库(如 spaCy、jieba、scikit-learn 和 sumy)构建一个支持中英文的多功能 NLP API。我们实现了分词、词性标注、命名实体识别、依存句法分析、文本相似度计算、关键词提取和文本摘要等功能。通过这些功能,我们可以对文本进行多方面的分析和处理,为各种应用场景提供支持。

附件1 项目结构

spaCy
├── dockerfile
├── download_models.py
├── i.sh
├── main.py
└── requirements.txt

附件2 项目代码

dockerfile

# 使用官方的 Python 基础镜像
FROM python:3.10-slim

# 设置工作目录
WORKDIR /home

# 复制 requirements.txt 文件并安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制 main.py
COPY main.py .

# 复制 main.py
COPY download_models.py .

# 下载 spaCy 语言模型
RUN python download_models.py

# 暴露端口
EXPOSE 8000

# 运行 FastAPI 应用
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

download_models.py

主要用来下载模型文件和库内部的一些依赖

# download_models.py
import spacy
import nltk

def download_models():
    spacy.cli.download("en_core_web_sm")
    spacy.cli.download("zh_core_web_sm")
    nltk.download('punkt')
    nltk.download('punkt_tab')

if __name__ == "__main__":
    download_models()

requirements.txt

annotated-types==0.7.0
anyio==4.4.0
blis==0.7.11
catalogue==2.0.10
certifi==2024.7.4
charset-normalizer==3.3.2
click==8.1.7
cloudpathlib==0.18.1
confection==0.1.5
cymem==2.0.8
en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.7.1/en_core_web_sm-3.7.1-py3-none-any.whl
exceptiongroup==1.2.2
fastapi==0.112.1
h11==0.14.0
hanlp-common==0.0.20
hanlp-restful==0.0.23
idna==3.8
jieba==0.42.1
Jinja2==3.1.4
joblib==1.4.2
langcodes==3.4.0
language_data==1.2.0
marisa-trie==1.2.0
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
murmurhash==1.0.10
numpy==1.26.4
packaging==24.1
phrasetree==0.0.9
preshed==3.0.9
pydantic==2.8.2
pydantic_core==2.20.1
Pygments==2.18.0
requests==2.32.3
rich==13.7.1
scikit-learn==1.5.1
scipy==1.14.1
shellingham==1.5.4
smart-open==7.0.4
sniffio==1.3.1
spacy==3.7.6
spacy-legacy==3.0.12
spacy-loggers==1.0.5
spacy-pkuseg==0.0.33
srsly==2.4.8
starlette==0.38.2
thinc==8.2.5
threadpoolctl==3.5.0
tqdm==4.66.5
typer==0.12.4
typing_extensions==4.12.2
urllib3==2.2.2
uvicorn==0.30.6
wasabi==1.1.3
weasel==0.4.1
wrapt==1.16.0
zh-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/zh_core_web_sm-3.7.0/zh_core_web_sm-3.7.0-py3-none-any.whl
sumy==0.11.0
nltk==3.9.1