Files
tougao/application/common/ReferenceTypeClassifier.php
2026-08-06 17:19:40 +08:00

243 lines
9.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common;
use app\common\service\LLMService;
/**
* 参考文献类型识别器
*
* 识别顺序(分层,从权威到兜底):
* 1. CrossRef 的 type 字段(若有 DOI 且命中)——最权威
* 2. 英文规则启发式(基于作者原文特征词/结构)
* 3. 大模型兜底(规则判为 other 或低置信度时)
* 4. 仍无法判定 → other
*
* 支持类型journal / book / conference / thesis / web / other
*/
class ReferenceTypeClassifier
{
const TYPE_JOURNAL = 'journal';
const TYPE_BOOK = 'book';
const TYPE_CONFERENCE = 'conference';
const TYPE_THESIS = 'thesis';
const TYPE_WEB = 'web';
const TYPE_OTHER = 'other';
/** 是否允许使用大模型兜底 */
private $useLlm = true;
public function __construct($config = [])
{
if (is_array($config) && isset($config['use_llm'])) {
$this->useLlm = (bool)$config['use_llm'];
}
}
/**
* 主入口:根据作者原文(可选 CrossRef type识别类型
*
* @param string $referContent 作者提供的原始参考文献字符串
* @param string $crossrefType CrossRef 返回的 type可选
* @return array ['type' => string, 'confidence' => float, 'source' => string]
*/
public function classify($referContent, $crossrefType = '')
{
$text = trim((string)$referContent);
// 1. CrossRef type 优先
$byCrossref = $this->mapCrossrefType($crossrefType);
if ($byCrossref !== '') {
return ['type' => $byCrossref, 'confidence' => 0.95, 'source' => 'crossref'];
}
if ($text === '') {
return ['type' => self::TYPE_OTHER, 'confidence' => 0.0, 'source' => 'fallback'];
}
// 2. 规则启发式
$byRule = $this->classifyByRule($text);
if ($byRule['type'] !== self::TYPE_OTHER && $byRule['confidence'] >= 0.8) {
return ['type' => $byRule['type'], 'confidence' => $byRule['confidence'], 'source' => 'rule'];
}
// 3. 大模型兜底
if ($this->useLlm) {
$byLlm = $this->classifyByLlm($text);
if ($byLlm !== '') {
return ['type' => $byLlm, 'confidence' => 0.7, 'source' => 'llm'];
}
}
// 规则给出的弱结果(若有)优先于纯兜底
if ($byRule['type'] !== self::TYPE_OTHER) {
return ['type' => $byRule['type'], 'confidence' => $byRule['confidence'], 'source' => 'rule'];
}
// 4. 兜底
return ['type' => self::TYPE_OTHER, 'confidence' => 0.0, 'source' => 'fallback'];
}
/**
* CrossRef type → 内部枚举映射;未命中返回空串
*/
/**
* 将细分类归并为排版用的三类journal / book / other
*/
public function normalizeDispatchType($type)
{
$type = strtolower(trim((string)$type));
if ($type === self::TYPE_BOOK) {
return self::TYPE_BOOK;
}
if ($type === self::TYPE_JOURNAL) {
return self::TYPE_JOURNAL;
}
return self::TYPE_OTHER;
}
public function mapCrossrefType($crossrefType)
{
$t = strtolower(trim((string)$crossrefType));
if ($t === '') {
return '';
}
$map = [
// 期刊
'journal-article' => self::TYPE_JOURNAL,
'journal' => self::TYPE_JOURNAL,
'journal-volume' => self::TYPE_JOURNAL,
'journal-issue' => self::TYPE_JOURNAL,
// 图书
'book' => self::TYPE_BOOK,
'book-chapter' => self::TYPE_BOOK,
'book-part' => self::TYPE_BOOK,
'book-section' => self::TYPE_BOOK,
'book-set' => self::TYPE_BOOK,
'book-series' => self::TYPE_BOOK,
'book-track' => self::TYPE_BOOK,
'reference-book' => self::TYPE_BOOK,
'edited-book' => self::TYPE_BOOK,
'monograph' => self::TYPE_BOOK,
// 会议
'proceedings-article' => self::TYPE_CONFERENCE,
'proceedings' => self::TYPE_CONFERENCE,
'proceedings-series' => self::TYPE_CONFERENCE,
// 学位论文
'dissertation' => self::TYPE_THESIS,
// 在线/预印本
'posted-content' => self::TYPE_WEB,
];
return isset($map[$t]) ? $map[$t] : '';
}
/**
* 英文规则启发式:按「最独特 → 最普通」顺序判定
*
* @return array ['type' => string, 'confidence' => float]
*/
public function classifyByRule($text)
{
$hasDoi = (bool)preg_match('/\bdoi:\s*10\./i', $text) || (bool)preg_match('#doi\.org/#i', $text);
$hasUrl = (bool)preg_match('#https?://#i', $text);
// 期刊卷期页结构,如 2020;382(8):727-733 或 2020;10:100
// 年份与卷号之间容忍多余标点与全角符号(作者原文常见 "2026;, 44(5): 24-31"
$hasJournalVol = (bool)preg_match(
'/\b(19|20)\d{2}[\s;:,]+\d+\s*(\(\s*[^)]{1,12}\))?\s*[:]\s*[A-Za-z]?\d+/u',
$text
);
// 1) 学位论文
if (preg_match('/\[(ph\.?d\.?|master(\'s)?|doctoral|masters)?\s*(thesis|dissertation)\]/i', $text)
|| preg_match('/\b(ph\.?d\.?|master\'?s|doctoral|doctorate)\b[^.]{0,40}\b(thesis|dissertation)\b/i', $text)
|| preg_match('/\b(thesis|dissertation)\b/i', $text)) {
return ['type' => self::TYPE_THESIS, 'confidence' => 0.9];
}
// 2) 会议论文
if (preg_match('/\bproceedings\b/i', $text)
|| preg_match('/\bin:\s*proc\b/i', $text)
|| preg_match('/\b(conference|symposium|workshop|congress)\b/i', $text)
|| preg_match('/\bannual meeting\b/i', $text)) {
return ['type' => self::TYPE_CONFERENCE, 'confidence' => 0.85];
}
// 3) 网页 / 在线资源
// "Available at:"/"Accessed" 同样出现在带 DOI 的期刊著录里(本系统自己的输出就带这个后缀),
// 故这些短语只在既无 DOI 也无卷期页结构时才作为网页依据
$hasWebPhrase = preg_match('/\[(internet|online)\]/i', $text)
|| preg_match('/\baccessed\b/i', $text)
|| preg_match('/\bavailable\s+(from|at)\b/i', $text)
|| preg_match('/\bcited\s+(19|20)\d{2}/i', $text);
if (($hasWebPhrase || $hasUrl) && !$hasDoi && !$hasJournalVol) {
return ['type' => self::TYPE_WEB, 'confidence' => 0.8];
}
// 4) 图书
if (preg_match('/\b\d+(st|nd|rd|th)\s+ed(ition)?\.?/i', $text)
|| preg_match('/\bisbn\b/i', $text)
|| preg_match('/\b(press|publisher|publishing house)\b/i', $text)
|| preg_match('/[A-Z][A-Za-z .]+:\s*[A-Z][A-Za-z .&]+;\s*(19|20)\d{2}/', $text)) {
// 期刊卷期结构更强时不判为书
if (!$hasJournalVol) {
return ['type' => self::TYPE_BOOK, 'confidence' => 0.8];
}
}
// 5) 期刊
if ($hasJournalVol || $hasDoi
|| preg_match('/\bvol\.?\s*\d+/i', $text)
|| preg_match('/\bpp?\.\s*\d+/i', $text)) {
return ['type' => self::TYPE_JOURNAL, 'confidence' => $hasJournalVol ? 0.85 : 0.6];
}
return ['type' => self::TYPE_OTHER, 'confidence' => 0.0];
}
/**
* 大模型兜底:只返回枚举值之一,失败返回空串
*/
public function classifyByLlm($text)
{
try {
$llm = new LLMService();
$system = 'You are a bibliography classifier. Classify the reference into exactly one type. '
. 'Allowed types: journal, book, conference, thesis, web, other. '
. 'journal=journal article; book=book or book chapter; conference=conference/proceedings paper; '
. 'thesis=dissertation/thesis; web=website or online resource; other=none of the above. '
. 'Respond with ONLY a JSON object: {"type":"<one of the allowed types>"}. No explanation.';
$user = "Reference:\n" . mb_substr($text, 0, 2000);
$content = $llm->requestChat([
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user],
], 0);
if ($content === null || $content === '') {
return '';
}
$parsed = $llm->parseJsonResponse($content);
$type = '';
if (is_array($parsed) && isset($parsed['type'])) {
$type = strtolower(trim((string)$parsed['type']));
} else {
// 兜底:直接从文本里找枚举词
if (preg_match('/\b(journal|book|conference|thesis|web|other)\b/i', $content, $m)) {
$type = strtolower($m[1]);
}
}
$allowed = [
self::TYPE_JOURNAL, self::TYPE_BOOK, self::TYPE_CONFERENCE,
self::TYPE_THESIS, self::TYPE_WEB, self::TYPE_OTHER,
];
return in_array($type, $allowed, true) ? $type : '';
} catch (\Throwable $e) {
return '';
}
}
}