package com.smartor.common;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 阿里云自然语言处理(NLP)工具类。
*
* 文档:NLP 自学习平台 / 基础文本服务
*
* 调用方式基于 {@code aliyun-java-sdk-alinlp} 的 {@link CommonRequest}:
* 统一向 {@code alinlp.cn-hangzhou.aliyuncs.com} 发送 POST 请求,版本号 2020-06-29,
* 通过传入不同的 {@code Action} 与 {@code ServiceCode} 调用具体能力。
*
* 配置项(application.yml):
*
* aliyun:
* nlp:
* access-key-id: ${accessKeyId} # 默认复用主账号 accessKeyId
* access-key-secret: ${accessKeySecret}
* region-id: cn-hangzhou
* domain: alinlp.cn-hangzhou.aliyuncs.com
* version: 2020-06-29
* read-timeout: 5000
* connect-timeout: 3000
*
*/
@Slf4j
@Component
public class AliNlpUtil {
/** 阿里云 AccessKey ID,默认复用主账号配置 */
@Value("${aliyun.nlp.access-key-id:${accessKeyId:}}")
private String accessKeyId;
/** 阿里云 AccessKey Secret */
@Value("${aliyun.nlp.access-key-secret:${accessKeySecret:}}")
private String accessKeySecret;
/** 地域 ID,公共云固定为 cn-hangzhou */
@Value("${aliyun.nlp.region-id:cn-hangzhou}")
private String regionId;
/** 接入域名 */
@Value("${aliyun.nlp.domain:alinlp.cn-hangzhou.aliyuncs.com}")
private String domain = "alinlp.cn-hangzhou.aliyuncs.com";
/** API 版本号 */
@Value("${aliyun.nlp.version:2020-06-29}")
private String version = "2020-06-29";
/** 读取超时(毫秒) */
@Value("${aliyun.nlp.read-timeout:5000}")
private int readTimeout = 5000;
/** 连接超时(毫秒) */
@Value("${aliyun.nlp.connect-timeout:3000}")
private int connectTimeout = 3000;
// NLP地域固定 cn-hangzhou
private static final String REGION_ID = "cn-hangzhou";
private static final String PRODUCT = "nlp";
private static final String VERSION = "2020-06-08";
private static final Gson gson = new Gson();
private IAcsClient client;
@PostConstruct
private void init() {
// String accessKeyId = "LTAI5tPfc1VJzz7VuhzcBwug";
// String accessKeySecret = "gG1srKxPFDBNWe2oHfqmK1qsSQkf1e";
String accessKeyId = "LTAI5t6GBf9nUXfX37CUDi4H";
String accessKeySecret = "BtjYAES4yjvde5Lwg46sTqnZefPnOE";
String regionId = "cn-hangzhou";
if (StringUtils.isBlank(accessKeyId) || StringUtils.isBlank(accessKeySecret)) {
log.warn("阿里云 NLP 未配置 AccessKey,AliNlpUtil 将不可用");
return;
}
IClientProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
this.client = new DefaultAcsClient(profile);
}
// ============================== 基础调用 ==============================
/**
* 通用调用入口:传入 Action 与业务参数,返回解析后的 JSON 对象。
*
* @param action API 动作名,例如 {@code GetWsChGeneral}
* @param params 业务参数,至少需要包含 {@code ServiceCode}、{@code Text} 等
* @return 接口返回的 JSON 对象;失败时返回 {@code null}
*/
public JSONObject invoke(String action, Map params) {
if (client == null) {
log.error("阿里云 NLP 客户端未初始化,无法调用 action={}", action);
return null;
}
if (StringUtils.isBlank(action)) {
throw new IllegalArgumentException("action 不能为空");
}
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain(domain);
request.setSysVersion(version);
request.setSysAction(action);
request.setSysReadTimeout(readTimeout);
request.setSysConnectTimeout(connectTimeout);
if (params != null) {
for (Map.Entry e : params.entrySet()) {
if (e.getValue() != null) {
request.putBodyParameter(e.getKey(), e.getValue());
}
}
}
try {
CommonResponse response = client.getCommonResponse(request);
String data = response.getData();
if (StringUtils.isBlank(data)) {
log.error("阿里云 NLP 返回为空,action={}, params={}", action, params);
return null;
}
return JSONObject.parseObject(data);
} catch (ClientException e) {
log.error("阿里云 NLP 调用失败 action={}, params={}, errCode={}, errMsg={}",
action, params, e.getErrCode(), e.getErrMsg(), e);
return null;
} catch (Exception e) {
log.error("阿里云 NLP 调用异常 action={}, params={}", action, params, e);
return null;
}
}
// ============================== 便捷方法 ==============================
/**
* 中文分词(通用领域,高级版)。
* 对应 Action:{@code GetWsChGeneral},ServiceCode:{@code alinlp}。
*
* @param text 待分词文本
* @param tokenizer 分词粒度,可空。常用值:{@code GENERAL_CHN}(通用基础粒度)
* @return 分词结果列表;调用失败返回空列表
*/
public List segmentChinese(String text, String tokenizer) {
if (StringUtils.isBlank(text)) {
return Collections.emptyList();
}
Map params = new LinkedHashMap<>();
params.put("ServiceCode", "alinlp");
params.put("Text", text);
if (StringUtils.isNotBlank(tokenizer)) {
params.put("TokenizerId", tokenizer);
}
JSONObject resp = invoke("GetWsChGeneral", params);
return parseWordsFromData(resp);
}
/** 中文分词,使用默认通用粒度 {@code GENERAL_CHN}。 */
public List segmentChinese(String text) {
return segmentChinese(text, "GENERAL_CHN");
}
/**
* 情感分析(通用领域,基础版)。
* 对应 Action:{@code GetSaChGeneral}。
*
* @param text 待分析文本
* @return 形如 {@code {"sentiment":"positive","positive_prob":0.97,...}} 的对象;失败返回 {@code null}
*/
public JSONObject sentimentAnalysis(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
Map params = new LinkedHashMap<>();
params.put("ServiceCode", "alinlp");
params.put("Text", text);
JSONObject resp = invoke("GetSaChGeneral", params);
return extractDataObject(resp);
}
// 测试入口
@Test
public void main() {
String text1 = "这家奶茶超级好喝,下次还来!";
String text2 = "味道很差,服务态度也不好,不推荐";
String text3 = "今天出门买了一瓶矿泉水";
this.init();
JSONObject res1 = sentimentAnalysis(text1);
JSONObject res2 = sentimentAnalysis(text2);
JSONObject res3 = sentimentAnalysis(text3);
System.out.println("正面文本结果:" + gson.toJson(res1));
System.out.println("负面文本结果:" + gson.toJson(res2));
System.out.println("中性文本结果:" + gson.toJson(res3));
}
/**
* 命名实体识别(通用领域)。
* 对应 Action:{@code GetNerChGeneral}。
*
* @param text 待识别文本
* @return 实体列表(每个元素含 word、tag、startIndex 等字段);失败返回空列表
*/
public JSONArray namedEntityRecognition(String text) {
if (StringUtils.isBlank(text)) {
return new JSONArray();
}
Map params = new LinkedHashMap<>();
params.put("ServiceCode", "alinlp");
params.put("Text", text);
JSONObject resp = invoke("GetNerChGeneral", params);
JSONObject data = extractDataObject(resp);
if (data == null) {
return new JSONArray();
}
JSONArray arr = data.getJSONArray("result");
return arr == null ? new JSONArray() : arr;
}
// ============================== 响应解析 ==============================
/**
* 解析阿里 NLP 的 {@code Data} 字段(字符串化的 JSON)。
* 顶层响应形如:{@code {"Data":"{...}","RequestId":"..."}}。
*/
private JSONObject extractDataObject(JSONObject resp) {
if (resp == null) {
return null;
}
String dataStr = resp.getString("Data");
if (StringUtils.isBlank(dataStr)) {
log.warn("阿里云 NLP 响应缺少 Data 字段,resp={}", resp);
return null;
}
try {
return JSONObject.parseObject(dataStr);
} catch (Exception e) {
log.error("阿里云 NLP Data 字段解析失败,dataStr={}", dataStr, e);
return null;
}
}
/**
* 从分词响应里抽取词列表。result 形如 {@code [{"word":"今天","tag":"t"},...]}。
*/
private List parseWordsFromData(JSONObject resp) {
JSONObject data = extractDataObject(resp);
if (data == null) {
return Collections.emptyList();
}
JSONArray result = data.getJSONArray("result");
if (result == null || result.isEmpty()) {
return Collections.emptyList();
}
String[] words = new String[result.size()];
for (int i = 0; i < result.size(); i++) {
JSONObject item = result.getJSONObject(i);
words[i] = item == null ? "" : item.getString("word");
}
return Arrays.asList(words);
}
@Test
public void test() {
List result = segmentChinese("阿里巴巴集团的使命是让天下没有难做的生意。");
log.info(result.toString());
}
@Test
public void testP() {
JSONObject result = sentimentAnalysis("我觉得你可能是对的");
log.info(result.toString());
}
}