130 lines
3.9 KiB
PHP
130 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace app\common\command;
|
|
|
|
use app\api\logic\ArticleLogic;
|
|
use app\common\enum\YesNoEnum;
|
|
use app\common\model\article\Article;
|
|
use think\console\Command;
|
|
use think\console\Input;
|
|
use think\console\Output;
|
|
|
|
class WorldcupArticleTranslate extends Command
|
|
{
|
|
protected function configure()
|
|
{
|
|
$this->setName('worldcup_article_translate')
|
|
->setDescription('预热世界杯英文资讯翻译缓存');
|
|
}
|
|
|
|
protected function execute(Input $input, Output $output)
|
|
{
|
|
$articles = Article::field('id,cid,title,desc,content,ext,is_show,delete_time')
|
|
->where('cid', 22)
|
|
->where('is_show', YesNoEnum::YES)
|
|
// 与前台世界杯资讯列表保持一致,只预热用户最先看到的 50 条。
|
|
->order('published_at', 'desc')
|
|
->order('id', 'desc')
|
|
->limit(50)
|
|
->select()
|
|
->toArray();
|
|
|
|
if (empty($articles)) {
|
|
$output->writeln('<comment>[WorldcupTranslate] 没有找到可处理的世界杯文章</comment>');
|
|
return 0;
|
|
}
|
|
|
|
$translated = 0;
|
|
$skippedExist = 0;
|
|
$skippedNonEnglish = 0;
|
|
$failed = 0;
|
|
|
|
foreach ($articles as $article) {
|
|
$ext = $this->decodeExt($article['ext'] ?? []);
|
|
if (!empty($ext['translated_content'])) {
|
|
$skippedExist++;
|
|
continue;
|
|
}
|
|
|
|
if (!$this->isProbablyEnglish($article)) {
|
|
$skippedNonEnglish++;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$result = ArticleLogic::autoTranslateArticle($article);
|
|
if ($result !== '') {
|
|
$translated++;
|
|
$output->writeln(sprintf('[WorldcupTranslate] 已翻译文章 #%d %s', $article['id'], $article['title'] ?? ''));
|
|
} else {
|
|
$failed++;
|
|
$output->writeln(sprintf('<comment>[WorldcupTranslate] 翻译失败或无结果 #%d %s</comment>', $article['id'], $article['title'] ?? ''));
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$failed++;
|
|
$output->writeln(sprintf('<error>[WorldcupTranslate] 异常 #%d %s - %s</error>', $article['id'], $article['title'] ?? '', $e->getMessage()));
|
|
}
|
|
}
|
|
|
|
$output->writeln(sprintf(
|
|
'[WorldcupTranslate] 完成,翻译 %d 条,已存在 %d 条,跳过非英文 %d 条,失败 %d 条',
|
|
$translated,
|
|
$skippedExist,
|
|
$skippedNonEnglish,
|
|
$failed
|
|
));
|
|
|
|
return 0;
|
|
}
|
|
|
|
private function isProbablyEnglish(array $article): bool
|
|
{
|
|
$text = trim(implode(' ', [
|
|
(string) ($article['title'] ?? ''),
|
|
(string) ($article['desc'] ?? ''),
|
|
$this->normalizeText((string) ($article['content'] ?? '')),
|
|
]));
|
|
|
|
if ($text === '') {
|
|
return false;
|
|
}
|
|
|
|
if (preg_match('/[\x{4e00}-\x{9fff}]/u', $text)) {
|
|
return false;
|
|
}
|
|
|
|
$letterCount = preg_match_all('/[A-Za-z]/', $text);
|
|
$wordCount = preg_match_all('/\b[A-Za-z]{2,}\b/', $text);
|
|
|
|
// 世界杯资讯里不少正文并不长,阈值过高会误杀正常英文稿件。
|
|
return $letterCount >= 30 && $wordCount >= 5;
|
|
}
|
|
|
|
private function normalizeText(string $content): string
|
|
{
|
|
if ($content === '') {
|
|
return '';
|
|
}
|
|
|
|
$content = strip_tags($content);
|
|
$content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$content = preg_replace('/\s+/u', ' ', $content);
|
|
|
|
return trim($content);
|
|
}
|
|
|
|
private function decodeExt($ext): array
|
|
{
|
|
if (is_array($ext)) {
|
|
return $ext;
|
|
}
|
|
|
|
if (is_string($ext) && $ext !== '') {
|
|
$data = json_decode($ext, true);
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|