参考文献模型校对换阿里云百炼,完善各种校对细节
This commit is contained in:
@@ -18,13 +18,43 @@ class PubmedService
|
||||
private $timeout = 20;
|
||||
private $tool = 'tmrjournals';
|
||||
private $email = '';
|
||||
// NCBI 限流:无 key 时 3 次/秒,配 key 后 10 次/秒。多 worker 并行处理参考文献时必须配。
|
||||
private $apiKey = '';
|
||||
private $maxRetry = 3;
|
||||
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->email = (string)$this->envGet('pubmed_email', '');
|
||||
$this->apiKey = (string)$this->envGet('pubmed_api_key', '');
|
||||
$tool = trim((string)$this->envGet('pubmed_tool', ''));
|
||||
if ($tool !== '') $this->tool = $tool;
|
||||
|
||||
if (isset($config['base'])) $this->base = rtrim((string)$config['base'], '/') . '/';
|
||||
if (isset($config['timeout'])) $this->timeout = max(5, intval($config['timeout']));
|
||||
if (isset($config['tool'])) $this->tool = (string)$config['tool'];
|
||||
if (isset($config['email'])) $this->email = (string)$config['email'];
|
||||
if (!empty($config['tool'])) $this->tool = (string)$config['tool'];
|
||||
if (!empty($config['email'])) $this->email = (string)$config['email'];
|
||||
if (!empty($config['api_key'])) $this->apiKey = (string)$config['api_key'];
|
||||
if (isset($config['max_retry'])) $this->maxRetry = max(1, intval($config['max_retry']));
|
||||
}
|
||||
|
||||
private function envGet($key, $default = '')
|
||||
{
|
||||
if (!class_exists('\think\Env')) {
|
||||
return $default;
|
||||
}
|
||||
return \think\Env::get($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* tool/email/api_key 是 NCBI 要求的调用方标识,缺失会被更严格限流
|
||||
*/
|
||||
private function commonParams(): array
|
||||
{
|
||||
$params = ['tool' => $this->tool, 'email' => $this->email];
|
||||
if ($this->apiKey !== '') {
|
||||
$params['api_key'] = $this->apiKey;
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,18 +90,18 @@ class PubmedService
|
||||
$pmid = trim($pmid);
|
||||
if ($pmid === '') return null;
|
||||
|
||||
// v3:解析结果新增 language / journal_country / affiliations,换 key 避免命中旧缓存
|
||||
// v3:解析结果新增 authors / volume / issue / pages / doilanguage / journal_country / affiliations换 key 避免命中旧缓存
|
||||
$cacheKey = 'pmid_v3_' . $pmid;
|
||||
$cached = $this->cacheGet($cacheKey, 30 * 86400);
|
||||
if (is_array($cached)) return $cached;
|
||||
|
||||
$url = $this->base . 'efetch.fcgi?' . http_build_query([
|
||||
$url = $this->base . 'efetch.fcgi?' . http_build_query(array_merge([
|
||||
'db' => 'pubmed',
|
||||
'id' => $pmid,
|
||||
'retmode' => 'xml',
|
||||
'tool' => $this->tool,
|
||||
'email' => $this->email,
|
||||
]);
|
||||
], $this->commonParams()));
|
||||
|
||||
$xml = $this->httpGet($url);
|
||||
if (!is_string($xml) || trim($xml) === '') return null;
|
||||
@@ -283,6 +313,11 @@ class PubmedService
|
||||
return null;
|
||||
}
|
||||
|
||||
$doi = $this->xpText($xp, '//PubmedArticle//ArticleIdList/ArticleId[@IdType="doi"]');
|
||||
if ($doi === '') {
|
||||
$doi = $this->xpText($xp, '//PubmedArticle//ELocationID[@EIdType="doi"]');
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'abstract' => $abstract,
|
||||
@@ -296,9 +331,133 @@ class PubmedService
|
||||
'languages' => $languages,
|
||||
'journal_country' => $journalCountry,
|
||||
'affiliations' => $affiliations,
|
||||
'authors' => $this->parseAuthors($xp),
|
||||
'volume' => $this->xpText($xp, '//PubmedArticle//JournalIssue//Volume'),
|
||||
'issue' => $this->xpText($xp, '//PubmedArticle//JournalIssue//Issue'),
|
||||
'pages' => $this->parsePagination($xp),
|
||||
'doi' => $doi,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 作者列表:LastName/ForeName/Initials,机构作者用 CollectiveName
|
||||
*/
|
||||
private function parseAuthors(\DOMXPath $xp): array
|
||||
{
|
||||
$out = [];
|
||||
$nodes = $xp->query('//PubmedArticle//AuthorList/Author');
|
||||
if (!$nodes) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
foreach ($nodes as $n) {
|
||||
$family = $given = $initials = $collective = '';
|
||||
foreach ($n->childNodes as $c) {
|
||||
switch ($c->nodeName) {
|
||||
case 'LastName':
|
||||
$family = trim($c->textContent);
|
||||
break;
|
||||
case 'ForeName':
|
||||
$given = trim($c->textContent);
|
||||
break;
|
||||
case 'Initials':
|
||||
$initials = trim($c->textContent);
|
||||
break;
|
||||
case 'CollectiveName':
|
||||
$collective = trim($c->textContent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($family === '' && $given === '' && $collective === '') {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'family' => $family,
|
||||
'given' => $given,
|
||||
'initials' => $initials,
|
||||
'collective' => $collective,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页码:StartPage/EndPage → MedlinePgn → 电子刊文章号(ELocationID pii)
|
||||
*
|
||||
* MedlinePgn 用 NLM 缩写式尾页(210-8 表示 210–218),展开逻辑由调用方处理。
|
||||
*/
|
||||
private function parsePagination(\DOMXPath $xp): string
|
||||
{
|
||||
$start = $this->xpText($xp, '//PubmedArticle//Pagination//StartPage');
|
||||
if ($start !== '') {
|
||||
$end = $this->xpText($xp, '//PubmedArticle//Pagination//EndPage');
|
||||
return $end !== '' ? $start . '-' . $end : $start;
|
||||
}
|
||||
|
||||
$medlinePgn = $this->xpText($xp, '//PubmedArticle//Pagination//MedlinePgn');
|
||||
if ($medlinePgn !== '') {
|
||||
return $medlinePgn;
|
||||
}
|
||||
|
||||
// 无连续页码的电子刊(PLOS 等)以文章号著录;pii 也可能是出版社流水号,故只认 e12345/12345
|
||||
$pii = $this->xpText($xp, '//PubmedArticle//ELocationID[@EIdType="pii"]');
|
||||
return preg_match('/^e?\d+$/i', $pii) === 1 ? $pii : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 引用格式作者串:姓全写 + 名首字母,超过 $maxAuthors 个取前 N 个 + et al
|
||||
* 例:Smith JA, Jones B, Lee C, et al
|
||||
*/
|
||||
public function authorsCitation(array $authors, int $maxAuthors = 3): string
|
||||
{
|
||||
$list = [];
|
||||
foreach ($authors as $a) {
|
||||
$collective = trim((string)($a['collective'] ?? ''));
|
||||
if ($collective !== '') {
|
||||
$list[] = $collective;
|
||||
continue;
|
||||
}
|
||||
|
||||
$family = trim((string)($a['family'] ?? ''));
|
||||
if ($family === '') {
|
||||
continue;
|
||||
}
|
||||
$initials = trim((string)($a['initials'] ?? ''));
|
||||
if ($initials === '') {
|
||||
$initials = $this->givenToInitials((string)($a['given'] ?? ''));
|
||||
}
|
||||
$list[] = $initials !== '' ? $family . ' ' . $initials : $family;
|
||||
}
|
||||
|
||||
if (empty($list)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$maxAuthors = max(1, $maxAuthors);
|
||||
if (count($list) > $maxAuthors) {
|
||||
return implode(', ', array_slice($list, 0, $maxAuthors)) . ', et al';
|
||||
}
|
||||
return implode(', ', $list);
|
||||
}
|
||||
|
||||
private function givenToInitials($given): string
|
||||
{
|
||||
$given = trim((string)$given);
|
||||
if ($given === '') {
|
||||
return '';
|
||||
}
|
||||
$parts = preg_split('/[\s\-\.]+/u', $given, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$initials = '';
|
||||
foreach ($parts as $p) {
|
||||
$first = mb_substr($p, 0, 1);
|
||||
if ($first !== '') {
|
||||
$initials .= mb_strtoupper($first);
|
||||
}
|
||||
}
|
||||
return $initials;
|
||||
}
|
||||
|
||||
private function xpText(\DOMXPath $xp, string $query): string
|
||||
{
|
||||
$n = $xp->query($query);
|
||||
|
||||
Reference in New Issue
Block a user