Files
tougao/application/common/ReferenceTypeClassifier.php

222 lines
8.4 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 → 内部枚举映射;未命中返回空串
*/
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
$hasJournalVol = (bool)preg_match('/\b(19|20)\d{2}\s*[;:]\s*\d+\s*(\(\d+\))?\s*:\s*[A-Za-z]?\d+/', $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) 网页 / 在线资源
if (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)
|| ($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 '';
}
}
}