ai写作辅助检测
This commit is contained in:
168
application/common/AiTemplateSentenceRuleDetectService.php
Normal file
168
application/common/AiTemplateSentenceRuleDetectService.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 模板句规则检测(t_ai_template_sentence)
|
||||
*/
|
||||
class AiTemplateSentenceRuleDetectService
|
||||
{
|
||||
/** @var array<int,array>|null */
|
||||
private static $ruleCache;
|
||||
|
||||
/**
|
||||
* @param array<string,string> $sectionTexts 键:abstract/introduction/...
|
||||
* @return array{hits:array,rule_score:int}
|
||||
*/
|
||||
public function detect(array $sectionTexts): array
|
||||
{
|
||||
$rules = $this->loadActiveRules();
|
||||
$hits = [];
|
||||
$ruleScore = 0;
|
||||
|
||||
$normalized = [];
|
||||
foreach ($sectionTexts as $key => $text) {
|
||||
$section = strtolower(trim((string) $key));
|
||||
if ($section === '') {
|
||||
continue;
|
||||
}
|
||||
$normalized[$section] = ManuscriptTextCleanService::clean((string) $text);
|
||||
}
|
||||
$allText = ManuscriptTextCleanService::clean(implode("\n\n", array_filter($normalized)));
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$section = strtolower(trim((string) ($rule['section'] ?? '')));
|
||||
$pattern = trim((string) ($rule['sentence_pattern'] ?? ''));
|
||||
if ($pattern === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetText = '';
|
||||
if ($section === 'all') {
|
||||
$targetText = $allText;
|
||||
} elseif (isset($normalized[$section])) {
|
||||
$targetText = $normalized[$section];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
if ($targetText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matches = $this->matchPattern($targetText, $pattern);
|
||||
foreach ($matches as $matchedText) {
|
||||
$sentence = $this->extractSentenceContaining($targetText, $matchedText);
|
||||
$hitScore = max(1, intval($rule['weight'] ?? 1)) * max(1, intval($rule['risk_level'] ?? 1));
|
||||
$hits[] = [
|
||||
'section' => $section,
|
||||
'type' => '模板化表达',
|
||||
'sentence' => $sentence !== '' ? $sentence : $matchedText,
|
||||
'pattern' => $pattern,
|
||||
'score' => $hitScore,
|
||||
'remark' => (string) ($rule['remark'] ?? ''),
|
||||
];
|
||||
$ruleScore += $hitScore;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'hits' => $hits,
|
||||
'rule_score' => min(100, $ruleScore),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array>
|
||||
*/
|
||||
private function loadActiveRules(): array
|
||||
{
|
||||
if (self::$ruleCache !== null) {
|
||||
return self::$ruleCache;
|
||||
}
|
||||
try {
|
||||
self::$ruleCache = Db::name('ai_template_sentence')
|
||||
->where('status', 1)
|
||||
->field('id,sentence_pattern,section,language,risk_level,weight,remark')
|
||||
->order('section asc,id asc')
|
||||
->select();
|
||||
} catch (\Throwable $e) {
|
||||
self::$ruleCache = [];
|
||||
}
|
||||
return self::$ruleCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function matchPattern(string $text, string $pattern): array
|
||||
{
|
||||
// 支持 regex: 前缀;否则优先按正则,失败则按字面量匹配
|
||||
$raw = $pattern;
|
||||
$forceRegex = false;
|
||||
if (stripos($pattern, 'regex:') === 0) {
|
||||
$raw = trim(substr($pattern, 6));
|
||||
$forceRegex = true;
|
||||
}
|
||||
|
||||
$out = $this->runPregMatchAll($text, '#' . $raw . '#iu');
|
||||
if (!empty($out) || $forceRegex) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
// 字面量回退(库内普通句子)
|
||||
return $this->runPregMatchAll($text, '#' . preg_quote($raw, '#') . '#iu');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function runPregMatchAll(string $text, string $regex): array
|
||||
{
|
||||
$error = null;
|
||||
set_error_handler(function ($errno, $errstr) use (&$error) {
|
||||
$error = $errstr;
|
||||
return true;
|
||||
});
|
||||
$ok = preg_match_all($regex, $text, $matches);
|
||||
restore_error_handler();
|
||||
if ($ok === false || $error !== null || empty($matches[0])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($matches[0] as $match) {
|
||||
$match = trim((string) $match);
|
||||
if ($match !== '') {
|
||||
$out[] = $match;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function extractSentenceContaining(string $text, string $needle): string
|
||||
{
|
||||
$pos = mb_stripos($text, $needle);
|
||||
if ($pos === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$before = mb_substr($text, 0, $pos);
|
||||
$after = mb_substr($text, $pos);
|
||||
|
||||
$start = $before;
|
||||
if (preg_match('/.*[.!?。!?]\s*/us', $before, $m)) {
|
||||
$start = substr($before, strlen($m[0]));
|
||||
}
|
||||
|
||||
$sentence = $start . $after;
|
||||
if (preg_match('/^(.+?[.!?。!?])/us', $sentence, $m)) {
|
||||
return trim($m[1]);
|
||||
}
|
||||
if (preg_match('/^.{1,500}/us', $sentence, $m)) {
|
||||
return trim($m[0]);
|
||||
}
|
||||
return trim(mb_substr($sentence, 0, 300));
|
||||
}
|
||||
}
|
||||
176
application/common/AiTemplateSentenceService.php
Normal file
176
application/common/AiTemplateSentenceService.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* AI 写作模版句规则匹配(t_ai_template_sentence)
|
||||
*/
|
||||
class AiTemplateSentenceService
|
||||
{
|
||||
/** @var array<int,array>|null */
|
||||
private static $ruleCache;
|
||||
|
||||
/**
|
||||
* 按稿件分节统计模版句出现次数
|
||||
*
|
||||
* @param array<string,string> $sectionTexts 键:abstract/introduction/methods/results/discussion/conclusion
|
||||
* @return array{total_matches:int,matched_rule_count:int,items:array,by_section:array}
|
||||
*/
|
||||
public function countInManuscript(array $sectionTexts): array
|
||||
{
|
||||
$rules = $this->loadActiveRules();
|
||||
if (empty($rules)) {
|
||||
return [
|
||||
'total_matches' => 0,
|
||||
'matched_rule_count' => 0,
|
||||
'items' => [],
|
||||
'by_section' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$normalizedSections = [];
|
||||
foreach ($sectionTexts as $key => $text) {
|
||||
$sectionKey = strtolower(trim((string) $key));
|
||||
if ($sectionKey === '') {
|
||||
continue;
|
||||
}
|
||||
$normalizedSections[$sectionKey] = $this->normalizeText((string) $text);
|
||||
}
|
||||
|
||||
$allText = $this->normalizeText(implode("\n\n", array_filter($normalizedSections)));
|
||||
|
||||
$items = [];
|
||||
$bySection = [];
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$ruleSection = strtolower(trim((string) ($rule['section'] ?? '')));
|
||||
$pattern = trim((string) ($rule['sentence_pattern'] ?? ''));
|
||||
if ($pattern === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetText = '';
|
||||
if ($ruleSection === 'all') {
|
||||
$targetText = $allText;
|
||||
} elseif (isset($normalizedSections[$ruleSection])) {
|
||||
$targetText = $normalizedSections[$ruleSection];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($targetText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = $this->countPatternMatches($targetText, $pattern);
|
||||
if ($count <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = [
|
||||
'id' => intval($rule['id'] ?? 0),
|
||||
'section' => $ruleSection,
|
||||
'sentence_pattern' => $pattern,
|
||||
'count' => $count,
|
||||
'risk_level' => intval($rule['risk_level'] ?? 0),
|
||||
'weight' => intval($rule['weight'] ?? 0),
|
||||
'remark' => (string) ($rule['remark'] ?? ''),
|
||||
];
|
||||
$items[] = $item;
|
||||
|
||||
if (!isset($bySection[$ruleSection])) {
|
||||
$bySection[$ruleSection] = [
|
||||
'total_matches' => 0,
|
||||
'matched_rule_count' => 0,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
$bySection[$ruleSection]['items'][] = $item;
|
||||
$bySection[$ruleSection]['total_matches'] += $count;
|
||||
$bySection[$ruleSection]['matched_rule_count']++;
|
||||
}
|
||||
|
||||
usort($items, function ($a, $b) {
|
||||
if ($a['count'] !== $b['count']) {
|
||||
return $b['count'] - $a['count'];
|
||||
}
|
||||
if ($a['weight'] !== $b['weight']) {
|
||||
return $b['weight'] - $a['weight'];
|
||||
}
|
||||
return $a['id'] - $b['id'];
|
||||
});
|
||||
|
||||
foreach ($bySection as &$sectionStats) {
|
||||
usort($sectionStats['items'], function ($a, $b) {
|
||||
if ($a['count'] !== $b['count']) {
|
||||
return $b['count'] - $a['count'];
|
||||
}
|
||||
return $b['weight'] - $a['weight'];
|
||||
});
|
||||
}
|
||||
unset($sectionStats);
|
||||
|
||||
$totalMatches = 0;
|
||||
foreach ($items as $item) {
|
||||
$totalMatches += intval($item['count']);
|
||||
}
|
||||
|
||||
return [
|
||||
'total_matches' => $totalMatches,
|
||||
'matched_rule_count' => count($items),
|
||||
'items' => $items,
|
||||
'by_section' => $bySection,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array>
|
||||
*/
|
||||
private function loadActiveRules(): array
|
||||
{
|
||||
if (self::$ruleCache !== null) {
|
||||
return self::$ruleCache;
|
||||
}
|
||||
|
||||
try {
|
||||
self::$ruleCache = Db::name('ai_template_sentence')
|
||||
->where('status', 1)
|
||||
->field('id,sentence_pattern,section,language,risk_level,weight,remark,article_type,version')
|
||||
->order('section asc,id asc')
|
||||
->select();
|
||||
} catch (\Throwable $e) {
|
||||
self::$ruleCache = [];
|
||||
}
|
||||
|
||||
return self::$ruleCache;
|
||||
}
|
||||
|
||||
private function normalizeText(string $text): string
|
||||
{
|
||||
return ManuscriptTextCleanService::clean($text);
|
||||
}
|
||||
|
||||
private function countPatternMatches(string $text, string $pattern): int
|
||||
{
|
||||
if ($text === '' || $pattern === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$regex = '#' . $pattern . '#iu';
|
||||
$error = null;
|
||||
set_error_handler(function ($errno, $errstr) use (&$error) {
|
||||
$error = $errstr;
|
||||
return true;
|
||||
});
|
||||
$count = preg_match_all($regex, $text, $matches);
|
||||
restore_error_handler();
|
||||
|
||||
if ($count === false || $error !== null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return intval($count);
|
||||
}
|
||||
}
|
||||
230
application/common/AiWritingRiskAnalysisService.php
Normal file
230
application/common/AiWritingRiskAnalysisService.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
/**
|
||||
* 编辑辅助:AI 辅助写作风险综合分析
|
||||
*/
|
||||
class AiWritingRiskAnalysisService
|
||||
{
|
||||
/**
|
||||
* @param array $parsedData parseManuscriptStructure 的 data 字段
|
||||
*/
|
||||
public function buildReport(array $parsedData): array
|
||||
{
|
||||
$templateStats = (array) ($parsedData['template_sentence_stats'] ?? []);
|
||||
$repeatedStats = (array) ($parsedData['repeated_phrases'] ?? []);
|
||||
$sentenceStats = (array) ($parsedData['sentence_stats'] ?? []);
|
||||
|
||||
$dimensions = [
|
||||
$this->analyzeTemplateSentenceRisk($templateStats),
|
||||
$this->analyzeRepeatedPhraseRisk($repeatedStats),
|
||||
$this->analyzeSentenceLengthRisk($sentenceStats),
|
||||
];
|
||||
|
||||
$overallScore = 0;
|
||||
$levelRank = ['low' => 1, 'medium' => 2, 'high' => 3];
|
||||
$maxRank = 1;
|
||||
$flags = [];
|
||||
|
||||
foreach ($dimensions as $dimension) {
|
||||
$overallScore += intval($dimension['score'] ?? 0);
|
||||
$rank = $levelRank[$dimension['level'] ?? 'low'] ?? 1;
|
||||
if ($rank > $maxRank) {
|
||||
$maxRank = $rank;
|
||||
}
|
||||
foreach ((array) ($dimension['flags'] ?? []) as $flag) {
|
||||
if ($flag !== '') {
|
||||
$flags[] = $flag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$overallLevel = array_search($maxRank, $levelRank, true) ?: 'low';
|
||||
|
||||
return [
|
||||
'overall_level' => $overallLevel,
|
||||
'overall_score' => min(100, $overallScore),
|
||||
'summary' => $this->buildSummary($overallLevel, $dimensions, $flags),
|
||||
'dimensions' => $dimensions,
|
||||
'flags' => array_values(array_unique($flags)),
|
||||
];
|
||||
}
|
||||
|
||||
private function analyzeTemplateSentenceRisk(array $stats): array
|
||||
{
|
||||
$items = (array) ($stats['items'] ?? []);
|
||||
$totalMatches = intval($stats['total_matches'] ?? 0);
|
||||
$matchedRuleCount = intval($stats['matched_rule_count'] ?? 0);
|
||||
|
||||
$score = 0;
|
||||
$flags = [];
|
||||
foreach ($items as $item) {
|
||||
$count = intval($item['count'] ?? 0);
|
||||
$riskLevel = max(1, intval($item['risk_level'] ?? 1));
|
||||
$weight = max(1, intval($item['weight'] ?? 1));
|
||||
$score += $count * $riskLevel * $weight;
|
||||
|
||||
if ($count > 0) {
|
||||
$section = (string) ($item['section'] ?? '');
|
||||
$remark = trim((string) ($item['remark'] ?? ''));
|
||||
$label = $remark !== '' ? $remark : (string) ($item['sentence_pattern'] ?? '');
|
||||
$flags[] = sprintf('%s段命中模版句:%s(%d次)', $section, $label, $count);
|
||||
}
|
||||
}
|
||||
|
||||
$level = $this->scoreToLevel($score, 15, 40);
|
||||
|
||||
return [
|
||||
'key' => 'template_sentence',
|
||||
'name' => '模版句特征',
|
||||
'level' => $level,
|
||||
'score' => min(100, $score),
|
||||
'summary' => $totalMatches > 0
|
||||
? sprintf('命中 %d 条模版句规则,共 %d 处', $matchedRuleCount, $totalMatches)
|
||||
: '未检测到典型 AI 模版句',
|
||||
'metrics' => [
|
||||
'total_matches' => $totalMatches,
|
||||
'matched_rule_count' => $matchedRuleCount,
|
||||
],
|
||||
'details' => $stats,
|
||||
'flags' => array_slice($flags, 0, 8),
|
||||
];
|
||||
}
|
||||
|
||||
private function analyzeRepeatedPhraseRisk(array $stats): array
|
||||
{
|
||||
$items = (array) ($stats['suspicious_items'] ?? []);
|
||||
if (empty($items)) {
|
||||
$items = array_values(array_filter((array) ($stats['items'] ?? []), function ($item) {
|
||||
return ($item['category'] ?? '') === 'suspicious';
|
||||
}));
|
||||
}
|
||||
$uniqueCount = count($items);
|
||||
$expectedCount = count((array) ($stats['expected_topic_items'] ?? []));
|
||||
$topCount = 0;
|
||||
$flags = [];
|
||||
|
||||
foreach (array_slice($items, 0, 5) as $item) {
|
||||
$count = intval($item['count'] ?? 0);
|
||||
if ($count > $topCount) {
|
||||
$topCount = $count;
|
||||
}
|
||||
if ($count >= 4) {
|
||||
$flags[] = sprintf('可疑重复短语「%s」出现 %d 次', (string) ($item['phrase'] ?? ''), $count);
|
||||
}
|
||||
}
|
||||
|
||||
$score = min(100, $uniqueCount * 3 + max(0, $topCount - 3) * 6);
|
||||
$level = 'low';
|
||||
if ($uniqueCount >= 10 || $topCount >= 8) {
|
||||
$level = 'high';
|
||||
} elseif ($uniqueCount >= 4 || $topCount >= 5) {
|
||||
$level = 'medium';
|
||||
}
|
||||
|
||||
$summary = '未发现可疑机械重复短语';
|
||||
if ($uniqueCount > 0) {
|
||||
$summary = sprintf('发现 %d 组可疑重复短语,最高重复 %d 次', $uniqueCount, $topCount);
|
||||
} elseif ($expectedCount > 0) {
|
||||
$summary = sprintf('主题词正常重复 %d 组,未触发可疑短语', $expectedCount);
|
||||
}
|
||||
|
||||
return [
|
||||
'key' => 'repeated_phrase',
|
||||
'name' => '重复短语',
|
||||
'level' => $level,
|
||||
'score' => $score,
|
||||
'summary' => $summary,
|
||||
'metrics' => [
|
||||
'suspicious_count' => $uniqueCount,
|
||||
'expected_topic_count' => $expectedCount,
|
||||
'top_count' => $topCount,
|
||||
],
|
||||
'details' => $stats,
|
||||
'flags' => $flags,
|
||||
];
|
||||
}
|
||||
|
||||
private function analyzeSentenceLengthRisk(array $stats): array
|
||||
{
|
||||
$avg = floatval($stats['avg'] ?? 0);
|
||||
$std = floatval($stats['std'] ?? 0);
|
||||
$max = intval($stats['max'] ?? 0);
|
||||
$sentenceCount = intval($stats['sentence_count'] ?? 0);
|
||||
|
||||
$score = 0;
|
||||
$flags = [];
|
||||
$level = 'low';
|
||||
|
||||
if ($sentenceCount >= 8) {
|
||||
if ($avg >= 32) {
|
||||
$score += 20;
|
||||
$flags[] = sprintf('平均句长偏高(%.1f 词/句)', $avg);
|
||||
}
|
||||
if ($std > 0 && $std <= 5 && $avg >= 12 && $avg <= 28) {
|
||||
$score += 25;
|
||||
$flags[] = sprintf('句长分布过于均匀(均值 %.1f,标准差 %.1f)', $avg, $std);
|
||||
}
|
||||
if ($max >= 50) {
|
||||
$score += 10;
|
||||
$flags[] = sprintf('存在超长句(最长 %d 词)', $max);
|
||||
}
|
||||
}
|
||||
|
||||
$level = $this->scoreToLevel($score, 10, 25);
|
||||
|
||||
return [
|
||||
'key' => 'sentence_length',
|
||||
'name' => '句长特征',
|
||||
'level' => $level,
|
||||
'score' => min(100, $score),
|
||||
'summary' => $sentenceCount > 0
|
||||
? sprintf('平均句长 %.1f,标准差 %.1f,共 %d 句', $avg, $std, $sentenceCount)
|
||||
: '正文句长数据不足',
|
||||
'metrics' => [
|
||||
'avg' => $avg,
|
||||
'std' => $std,
|
||||
'max' => $max,
|
||||
'min' => intval($stats['min'] ?? 0),
|
||||
'sentence_count' => $sentenceCount,
|
||||
],
|
||||
'details' => $stats,
|
||||
'flags' => $flags,
|
||||
];
|
||||
}
|
||||
|
||||
private function scoreToLevel(int $score, int $mediumThreshold, int $highThreshold): string
|
||||
{
|
||||
if ($score >= $highThreshold) {
|
||||
return 'high';
|
||||
}
|
||||
if ($score >= $mediumThreshold) {
|
||||
return 'medium';
|
||||
}
|
||||
return 'low';
|
||||
}
|
||||
|
||||
private function buildSummary(string $overallLevel, array $dimensions, array $flags): string
|
||||
{
|
||||
$levelText = [
|
||||
'low' => '低风险',
|
||||
'medium' => '中等风险',
|
||||
'high' => '较高风险',
|
||||
];
|
||||
$parts = [$levelText[$overallLevel] ?? '低风险'];
|
||||
|
||||
foreach ($dimensions as $dimension) {
|
||||
if (($dimension['level'] ?? 'low') === 'low') {
|
||||
continue;
|
||||
}
|
||||
$parts[] = $dimension['name'] . '需关注';
|
||||
}
|
||||
|
||||
if (!empty($flags)) {
|
||||
$parts[] = '重点:' . $flags[0];
|
||||
}
|
||||
|
||||
return implode(';', $parts);
|
||||
}
|
||||
}
|
||||
121
application/common/ManuscriptTextCleanService.php
Normal file
121
application/common/ManuscriptTextCleanService.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace app\common;
|
||||
|
||||
/**
|
||||
* 稿件文本简单清洗(检测前预处理,不改变接口返回的原始正文)
|
||||
*/
|
||||
class ManuscriptTextCleanService
|
||||
{
|
||||
/**
|
||||
* 检测前文本清洗:解码、去噪、去引用标记、统一空白
|
||||
*/
|
||||
public static function clean(string $text): string
|
||||
{
|
||||
$text = trim($text);
|
||||
if ($text === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
$text = self::normalizeEncoding($text);
|
||||
$text = self::normalizeQuotesAndDashes($text);
|
||||
$text = self::stripInvisibleChars($text);
|
||||
$text = self::stripUrlsAndEmails($text);
|
||||
$text = self::stripCitationMarkers($text);
|
||||
$text = self::stripFigureTableTags($text);
|
||||
$text = self::stripFootnoteMarkers($text);
|
||||
$text = self::stripSearchStrategyNoise($text);
|
||||
$text = preg_replace('/\s+/u', ' ', $text);
|
||||
$text = trim((string) preg_replace('/\s+([,.;:!?])/u', '$1', $text));
|
||||
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $sections
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public static function cleanSections(array $sections): array
|
||||
{
|
||||
$cleaned = [];
|
||||
foreach ($sections as $key => $text) {
|
||||
$cleaned[$key] = self::clean((string) $text);
|
||||
}
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
private static function normalizeEncoding(string $text): string
|
||||
{
|
||||
if (!mb_check_encoding($text, 'UTF-8')) {
|
||||
$text = mb_convert_encoding($text, 'UTF-8', 'GBK,GB2312,GB18030,ISO-8859-1');
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function normalizeQuotesAndDashes(string $text): string
|
||||
{
|
||||
return strtr($text, [
|
||||
'‘' => "'",
|
||||
'’' => "'",
|
||||
'“' => '"',
|
||||
'”' => '"',
|
||||
'„' => '"',
|
||||
'‟' => '"',
|
||||
'—' => '-',
|
||||
'–' => '-',
|
||||
'‑' => '-',
|
||||
' ' => ' ',
|
||||
]);
|
||||
}
|
||||
|
||||
private static function stripInvisibleChars(string $text): string
|
||||
{
|
||||
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text);
|
||||
$text = preg_replace('/[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}\x{FEFF}]/u', '', $text);
|
||||
$text = str_replace([chr(0xC2) . chr(0xA0), ' '], ' ', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function stripUrlsAndEmails(string $text): string
|
||||
{
|
||||
$text = preg_replace('/https?:\/\/\S+/iu', ' ', $text);
|
||||
$text = preg_replace('/\b[\w.+-]+@[\w.-]+\.[a-z]{2,}\b/iu', ' ', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function stripCitationMarkers(string $text): string
|
||||
{
|
||||
// [1] [1,2] [1-3] [1, 2-4]
|
||||
$text = preg_replace('/\[\s*\d+(?:\s*[,,\-–—]\s*\d+)*\s*\]/u', ' ', $text);
|
||||
// 上标残留数字(如 word 提取后的 ^1^)
|
||||
$text = preg_replace('/\b\^\d+\^/u', ' ', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function stripFigureTableTags(string $text): string
|
||||
{
|
||||
$text = preg_replace('/\((?:Fig(?:ure)?|Table|Tab\.)\s*[A-Za-z0-9.-]+\)/iu', ' ', $text);
|
||||
$text = preg_replace('/\b(?:Fig(?:ure)?|Table|Tab\.)\s*[A-Za-z0-9.-]+\b/iu', ' ', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function stripFootnoteMarkers(string $text): string
|
||||
{
|
||||
$text = preg_replace('/[*#†‡§]+\d*/u', ' ', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除文献检索式、数据库字段等 Methods 常见残留
|
||||
*/
|
||||
private static function stripSearchStrategyNoise(string $text): string
|
||||
{
|
||||
$text = preg_replace('/\btitle\s*[\/\-]\s*abstract\b/iu', ' ', $text);
|
||||
$text = preg_replace('/\babstract\s*[\/\-]\s*title\b/iu', ' ', $text);
|
||||
$text = preg_replace('/\bti\s*[\-\/]?\s*ab\b/iu', ' ', $text);
|
||||
$text = preg_replace('/\bmesh\s+terms?\b/iu', ' ', $text);
|
||||
$text = preg_replace('/\b(search\s+terms?|search\s+strategy)\b/iu', ' ', $text);
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user