作者:小傅哥
博客:https://bugstack.cn
沉澱、分享、成長,讓自己和他人都能有所收穫!😄
哈嘍,大家好我是技術UP主小傅哥。
常聽到一句話:你很難賺到你認知以外的錢💰,屁!不是很難,是壓根賺不到。 你以爲要是你做也能做,但其實除了你能看見的以外,還有很多東西都不知道。
我看過不少小夥伴自己上線過帶有評論功能的博客,或是能進行通信的聊天室。但最後都沒運營多久就關停了,除了能花錢解決的服務器成本,還有是自身的研發的系統流程不夠健全。其中非常重要的一點是輿情敏感內容
的審覈,如果你做這類應用的處理,一定要對接上相應的內容安全審覈。
那麼,接下來小傅哥就給大家分享下,如何對接內容安全審覈,並在 DDD 分層結構下實現一個對應的規則過濾服務。
文末提供了「星球:碼農會鎖」🧧優惠加入方式,以及本節課程的代碼地址。項目演示地址:https://gaga.plus
一、場景說明
在本節小傅哥會通過 DDD 分層架構設計,開發出一個敏感詞、內容安全審覈過濾操作的規則處理器。在這個過程大家可以學習到 DDD 分層調用流程、規則模型的搭建、敏感詞和內容審覈的使用。
如圖,上半部分是業務流程,下半部分是 DDD 分層結構中的實現。
- 業務流程上,以用戶發送的提交給服務端的內容進行審覈過濾,優先使用敏感詞進行替換單詞組。過濾後過內容審覈,一般各個雲平臺都有提供內容審覈的接口,如;京東雲、百度雲、騰訊雲都有提供。一般價格在
0.0015 元/條
- 系統實現上,以 DDD 分層架構實現一個內容審覈的流程。app 配置組件和啓動應用、trigger 提供 http 調用、domain 編寫核心邏輯和流程、infrastructure 提供 dao 的基礎操作。
二、內容審覈 - SDK 使用
一般輿情內容審覈分爲兩種,一種是靜態配置數據的 SDK 組件,也叫敏感詞過濾。另外一種是實時動態的由各個第三方提供的內容審覈接口服務。這類的就是前面提到的,在各個雲平臺都有提供。
這裏小傅哥先帶着大家做下最基本的調用案例,之後再基於 DDD 工程實現整個代碼開發。
1. 敏感詞
地址:https://github.com/houbb/sensitive-word - 開源的敏感詞庫組件
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-word</artifactId>
<version>0.8.0</version>
</dependency>
案例代碼
@Test
public void test_sensitive_word() {
boolean contains = sensitiveWordBs.contains("小傅哥喜歡燒烤臭毛蛋,豆包愛喫粑粑,如果想喫訂購請打電話:13900901878");
log.info("是否被敏感詞攔截:{}", contains);
}
@Test
public void test_sensitive_word_findAll() {
List<String> list = sensitiveWordBs.findAll("小傅哥喜歡燒烤臭毛蛋,豆包愛喫粑粑,如果想喫訂購請打電話:13900901878");
log.info("測試結果:{}", JSON.toJSONString(list));
}
@Test
public void test_sensitive_word_replace() {
String replace = sensitiveWordBs.replace("小傅哥喜歡燒烤臭毛蛋,豆包愛喫粑粑,如果想喫訂購請打電話:13900901878");
log.info("測試結果:{}", replace);
}
- 敏感詞組件提供了大量的風險詞過濾,同時可以基於組件的文檔完成自定義敏感詞的增改刪減操作。
本文在工程中已提供
- 敏感詞組件提供了判斷、查找、過濾操作。還有你可以把檢測到的敏感詞替換爲
*
或者空格
。
2. 內容審覈
- 京東雲:https://www.jdcloud.com/cn/products/censor
- 百度雲:https://ai.baidu.com/censoring#/strategylist
- 騰訊雲:https://cloud.tencent.com/product/tms
這裏小傅哥以其中的一個百度云爲例,爲大家展示內容安全審覈的使用。
<!-- 百度內容審覈 https://mvnrepository.com/artifact/com.baidu.aip/java-sdk -->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.17</version>
</dependency>
2.1 配置應用
- 先領取免費的調用次數,之後創建應用。創建應用後就可以獲得連接信息;appid、apikey、secretkey
- 另外是策略配置,如果你在過濾中不需要檢測用戶發的應用營銷信息,那麼是可以不檢測的。
2.2 測試服務
//設置APPID/AK/SK
public static final String APP_ID = "{APP_ID}";
public static final String API_KEY = "{API_KEY}";
public static final String SECRET_KEY = "{SECRET_KEY}";
private AipContentCensor client;
@Before
public void init() {
client = new AipContentCensor(APP_ID, API_KEY, SECRET_KEY);
// 可選:設置網絡連接參數
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
}
@Test
public void test_textCensorUserDefined() throws JSONException {
for (int i = 0; i < 1; i++) {
JSONObject jsonObject = client.textCensorUserDefined("小傅哥喜歡燒烤臭毛蛋,豆包愛喫粑粑,如果想喫訂購請打電話:13900901878");
if (!jsonObject.isNull("error_code")) {
log.info("測試結果:{}", jsonObject.get("error_code"));
} else {
log.info("測試結果:{}", jsonObject.toString());
}
}
}
測試結果
13:41:16.393 [main] INFO com.baidu.aip.client.BaseClient - get access_token success. current state: STATE_AIP_AUTH_OK
13:41:16.396 [main] DEBUG com.baidu.aip.client.BaseClient - current state after check priviledge: STATE_TRUE_AIP_USER
13:41:16.495 [main] INFO cn.bugstack.x.api.test.BaiduAipContentCensorTest - 測試結果:{"conclusion":"合規","log_id":17046060767025067,"isHitMd5":false,"conclusionType":1}
- 應爲過濾掉了營銷信息,比如手機號。那麼就會返回
合規
三、應用實現 - DDD 架構
做了以上的基本調用案例以後,我們來看下在系統中怎麼運用這些基礎功能完成業務訴求。
1. 工程結構
- docs 下提供了 docker 安裝 mysql 以及初始化數據庫配置的腳本。因爲本文的案例,可以滿足你在數據庫中增加敏感詞配置。
- app 是應用的啓動層,如上我們所需的敏感詞和內容審覈,都在app層下配置啓動處理。
- domain 領域層通過策略+工廠,實現規則過濾服務。
2. 數據庫表
- 在docs 提供了數據庫初始化的腳本語句,你可以導入到自己的數據庫,或者使用 docker 腳本安裝測試。—— 注意已經安裝過 mysql 佔用了 3306 端口的話,記得修改 docker 腳本安裝 mysql 的端口。
- 配置到數據庫中的敏感詞方便管理和使用,爲了性能考慮也可以考慮使用 redis 做一層緩存。
3. 配置加載
3.1 敏感詞初始化
@Configuration
public class SensitiveWordConfig {
@Bean
public SensitiveWordBs sensitiveWordBs(IWordDeny wordDeny, IWordAllow wordAllow) {
return SensitiveWordBs.newInstance()
.wordDeny(wordDeny)
.wordAllow(wordAllow)
.ignoreCase(true)
.ignoreWidth(true)
.ignoreNumStyle(true)
.ignoreChineseStyle(true)
.ignoreEnglishStyle(true)
.ignoreRepeat(false)
.enableNumCheck(true)
.enableEmailCheck(true)
.enableUrlCheck(true)
.enableWordCheck(true)
.numCheckLen(1024)
.init();
}
@Bean
public IWordDeny wordDeny(ISensitiveWordDao sensitiveWordDao) {
return new IWordDeny() {
@Override
public List<String> deny() {
return sensitiveWordDao.queryValidSensitiveWordConfig("deny");
}
};
}
@Bean
public IWordAllow wordAllow(ISensitiveWordDao sensitiveWordDao) {
return new IWordAllow() {
@Override
public List<String> allow() {
return sensitiveWordDao.queryValidSensitiveWordConfig("allow");
}
};
}
}
- wordDeny、wordAllow 是兩個自定義的攔截和放行的敏感詞列表,這裏小傅哥設計從數據庫中查詢。可以方便動態的維護。
3.2 內容安全初始化
# 內容安全
baidu:
aip:
app_id: 46573000
api_key: XKOalQOgDBUrvgLBplvu****
secret_key: kwRh1bEhETYWpq9thzyySdFDPKUk****
- 自定義一個配置文件類 AipContentCensorConfigProperties
@Bean
public AipContentCensor aipContentCensor(AipContentCensorConfigProperties properties) {
AipContentCensor client = new AipContentCensor(properties.getApp_id(), properties.getApi_key(), properties.getSecret_key());
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
return client;
}
- 這裏我們來統一創建 AipContentCensor 對象,用於有需要使用的地方處理內容審覈。
4. 規則實現
源碼: cn.bugstack.xfg.dev.tech.domain.service.IRuleLogicFilter
public interface IRuleLogicFilter {
RuleActionEntity<RuleMatterEntity> filter(RuleMatterEntity ruleMatterEntity);
}
- 定義一個統一的規則過濾接口
4.1 敏感詞
@Slf4j
@Component
@LogicStrategy(logicMode = DefaultLogicFactory.LogicModel.SENSITIVE_WORD)
public class SensitiveWordFilter implements IRuleLogicFilter {
@Resource
private SensitiveWordBs words;
@Override
public RuleActionEntity<RuleMatterEntity> filter(RuleMatterEntity ruleMatterEntity) {
// 敏感詞過濾
String content = ruleMatterEntity.getContent();
String replace = words.replace(content);
// 返回結果
return RuleActionEntity.<RuleMatterEntity>builder()
.type(LogicCheckTypeVO.SUCCESS)
.data(RuleMatterEntity.builder().content(replace).build())
.build();
}
}
4.2 安全內容
@Slf4j
@Component
@LogicStrategy(logicMode = DefaultLogicFactory.LogicModel.CONTENT_SECURITY)
public class ContentSecurityFilter implements IRuleLogicFilter {
@Resource
private AipContentCensor aipContentCensor;
@Override
public RuleActionEntity<RuleMatterEntity> filter(RuleMatterEntity ruleMatterEntity) {
JSONObject jsonObject = aipContentCensor.textCensorUserDefined(ruleMatterEntity.getContent());
if (!jsonObject.isNull("conclusion") && "不合規".equals(jsonObject.get("conclusion"))) {
return RuleActionEntity.<RuleMatterEntity>builder()
.type(LogicCheckTypeVO.REFUSE)
.data(RuleMatterEntity.builder().content("內容不合規").build())
.build();
}
// 返回結果
return RuleActionEntity.<RuleMatterEntity>builder()
.type(LogicCheckTypeVO.SUCCESS)
.data(ruleMatterEntity)
.build();
}
}
5. 工廠使用
public class DefaultLogicFactory {
public Map<String, IRuleLogicFilter> logicFilterMap = new ConcurrentHashMap<>();
public DefaultLogicFactory(List<IRuleLogicFilter> logicFilters) {
logicFilters.forEach(logic -> {
LogicStrategy strategy = AnnotationUtils.findAnnotation(logic.getClass(), LogicStrategy.class);
if (null != strategy) {
logicFilterMap.put(strategy.logicMode().getCode(), logic);
}
});
}
public RuleActionEntity<RuleMatterEntity> doCheckLogic(RuleMatterEntity ruleMatterEntity, LogicModel... logics) {
RuleActionEntity<RuleMatterEntity> entity = null;
for (LogicModel model : logics) {
entity = logicFilterMap.get(model.code).filter(ruleMatterEntity);
if (!LogicCheckTypeVO.SUCCESS.equals(entity.getType())) return entity;
ruleMatterEntity = entity.getData();
}
return entity != null ? entity :
RuleActionEntity.<RuleMatterEntity>builder()
.type(LogicCheckTypeVO.SUCCESS)
.data(ruleMatterEntity)
.build();
}
}
- 定義出規則的使用工廠,通過構造函數的方式注入已經實現了接口 IRuleLogicFilter 的 N 個規則,注入到 Map 中
Map<String, IRuleLogicFilter> logicFilterMap
- doCheckLogic 根據入參來過濾需要處理的規則。這裏可以看到每過濾一個規則都會把參數繼續傳遞給下一個規則繼續篩選。
有點像層層過篩子的感覺
四、測試驗證
- 測試前確保已經初始化了庫表
docs/dev-ops/sql/xfg-dev-tech-content-moderation.sql
application-dev.yml
配置百度內容安全參數和數據庫連接參數。
1. 功能測試
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class RuleLogicTest {
@Resource
private DefaultLogicFactory defaultLogicFactory;
@Test
public void test() {
RuleActionEntity<RuleMatterEntity> entity = defaultLogicFactory.doCheckLogic(
RuleMatterEntity.builder().content("小傅哥喜歡燒烤臭毛蛋,豆包愛喫粑粑,如果想喫訂購請打電話:13900901878").build(),
DefaultLogicFactory.LogicModel.SENSITIVE_WORD,
DefaultLogicFactory.LogicModel.CONTENT_SECURITY
);
log.info("測試結果:{}", JSON.toJSONString(entity));
}
}
測試結果
24-01-07.14:17:16.988 [main ] INFO BaseClient - get access_token success. current state: STATE_AIP_AUTH_OK
24-01-07.14:17:17.328 [main ] INFO RuleLogicTest - 測試結果:{"data":{"content":"小傅哥喜歡燒烤***,豆包愛喫**,如果想喫訂購請打電話:13900901878"},"type":"SUCCESS"}
2. 接口測試
@RequestMapping(value = "sensitive/rule", method = RequestMethod.GET)
public String rule(String content) {
try {
log.info("內容審覈開始 content: {}", content);
RuleActionEntity<RuleMatterEntity> entity = defaultLogicFactory.doCheckLogic(RuleMatterEntity.builder().content(content).build(),
DefaultLogicFactory.LogicModel.SENSITIVE_WORD,
DefaultLogicFactory.LogicModel.CONTENT_SECURITY
);
log.info("內容審覈完成 content: {}", entity.getData());
return JSON.toJSONString(entity);
} catch (Exception e) {
log.error("內容審覈異常 content: {}", content, e);
return "Err!";
}
}
接口:http://localhost:8091/api/v1/content/sensitive/rule?content=小傅哥喜歡燒烤臭毛蛋,豆包愛喫粑粑,如果想喫訂購請打電話:13900901878
- 那麼現在就可以對內容進行審覈過濾了。