参考文献的作者数变成6

This commit is contained in:
wangjinlei
2026-08-07 17:51:18 +08:00
parent a1e151048c
commit f87057488b
10 changed files with 386 additions and 49 deletions

View File

@@ -0,0 +1,201 @@
<?php
namespace app\common;
/**
* 作者列表著录规则:作者数不超过 MAX_LISTED 时全部列出,超过时只列前 KEEP 位再加 et al。
*
* 单独抽出来是因为"数作者个数"并不能简单按逗号切:
* - 温哥华式 "Smith AB, Jones AC" 逗号 = 作者分隔
* - APA 式 "Smith, A. B., Jones, A. C." 逗号既分隔作者,也分隔姓与名
* - 机构作者 "Department of Health, Education, and Welfare" 逗号只是机构名的一部分
* 按逗号数会把 APA 的 3 位作者数成 6 位,凭空触发截断。
*
* 拿不准的一律少切(宁可多列几位,也不要把不该截的截掉)。
*/
class AuthorListFormatter
{
/** 全部列出的上限 */
const MAX_LISTED = 6;
/** 超过上限时保留的作者数 */
const KEEP = 3;
/**
* 按著录规则输出作者串
*/
public static function format($author, $max = self::MAX_LISTED, $keep = self::KEEP)
{
$author = self::normalize($author);
if ($author === '') {
return '';
}
// 已经是 "…, et al" 形态的(历史数据或上游截断过),保持原样,不再叠加
if (self::isTruncated($author)) {
return $author;
}
$units = self::split($author);
if (empty($units)) {
return $author;
}
$max = max(1, (int)$max);
$keep = min(max(1, (int)$keep), $max);
if (count($units) > $max) {
return implode(', ', array_slice($units, 0, $keep)) . ', et al';
}
return implode(', ', $units);
}
public static function countAuthors($author)
{
return count(self::split($author));
}
public static function isTruncated($author)
{
return (bool)preg_match('/\bet\s+al\.?\s*$/iu', (string)$author);
}
/**
* 拆成单个作者
*
* @return string[]
*/
public static function split($author)
{
$author = self::normalize($author);
$author = preg_replace('/[,;]?\s*\bet\s+al\.?\s*$/iu', '', $author);
$author = trim($author, " ,;.");
if ($author === '') {
return [];
}
if (strpos($author, ';') !== false) {
// 分号是无歧义的作者分隔符
$units = preg_split('/\s*;\s*/u', $author);
} else {
$text = $author;
// "A, B, and C" 里的 and/& 才是分隔符;没有逗号时它多半是机构名的一部分
// (如 "National Institute for Health and Care Excellence"),不能切
if (strpos($text, ',') !== false) {
$text = preg_replace('/\s*,?\s+(?:and|&)\s+/iu', ', ', $text);
}
$units = preg_split('/\s*,\s*/u', $text);
}
$units = array_values(array_filter(array_map(function ($u) {
return trim($u, " ,;");
}, (array)$units), 'strlen'));
$units = self::mergeGivenNames($units);
// 整串没有一个单元像人名,多半是带逗号的机构名,算一位作者
$hasPerson = false;
foreach ($units as $unit) {
if (self::looksLikePerson($unit)) {
$hasPerson = true;
break;
}
}
return $hasPerson ? $units : [$author];
}
/**
* 合并 APA 的 "姓, 名" —— "Smith", "A. B." → "Smith, A. B."
*
* @param string[] $units
* @return string[]
*/
private static function mergeGivenNames(array $units)
{
$merged = [];
foreach ($units as $unit) {
// 上一位作者已经带了名缩写,说明这一段是新作者的姓,不能再往上并
$last = empty($merged) ? '' : $merged[count($merged) - 1];
if ($last !== '' && !self::hasInitials($last) && self::looksLikeGivenName($unit)) {
$merged[count($merged) - 1] .= ', ' . $unit;
continue;
}
$merged[] = $unit;
}
return $merged;
}
private static function hasInitials($unit)
{
return (bool)preg_match('/\p{Lu}\./u', $unit) || (bool)preg_match('/\s\p{Lu}{1,4}$/u', $unit);
}
/**
* 是否只是名/缩写(属于上一个姓):每个词都是首字母大写,且至少有一个是"单字母 + 点"
* "A. B." / "H. Karl." → 是;"Jones AC" / "Bruce Alberts" → 否
*/
private static function looksLikeGivenName($unit)
{
$unit = trim((string)$unit);
if ($unit === '') {
return false;
}
// 末位作者的名常被上游去掉尾点,"K." 会变成 "K"
if (preg_match('/^\p{Lu}\.?$/u', $unit)) {
return true;
}
if (!preg_match('/\b\p{Lu}\./u', $unit)) {
return false;
}
foreach (preg_split('/\s+/u', $unit) as $word) {
$word = trim($word, " .");
if ($word === '') {
continue;
}
if (!preg_match('/^\p{Lu}[\p{L}]*$/u', $word)) {
return false;
}
}
return true;
}
/**
* 判定要偏严:机构名里的逗号片段("Department of Health" / "Education")不能算人名,
* 否则 "Department of Health, Education, and Welfare" 会被当成三位作者
*/
private static function looksLikePerson($unit)
{
$unit = trim(preg_replace('/\s+/u', ' ', str_replace(',', ' ', (string)$unit)));
if ($unit === '') {
return false;
}
// 中文姓名
if (preg_match('/^[\x{4e00}-\x{9fff}·]{2,10}$/u', $unit)) {
return true;
}
// 以缩写结尾:"Smith AB" / "van den Berg AB"
if (preg_match('/^\p{Lu}.*\s\p{Lu}{1,4}$/u', $unit)) {
return true;
}
// 带点的名缩写:"Smith A. B." / "Butcher H. Karl."
if (preg_match('/\b\p{Lu}\./u', $unit)) {
return true;
}
// 两个词的全名:"Bruce Alberts"
return (bool)preg_match('/^\p{Lu}[\p{L}\'\x{2019}\-]+\s+\p{Lu}[\p{L}\'\x{2019}\-]+$/u', $unit);
}
private static function normalize($author)
{
$author = trim((string)$author);
$author = str_replace(['', '', '', ' '], [',', ';', '.', ' '], $author);
$author = preg_replace('/\s+/u', ' ', $author);
return trim($author, " ,;");
}
}

View File

@@ -47,6 +47,15 @@ class BookCitationParser
$out['isbn'] = $this->matchIsbn($clean);
$clean = $this->stripTail($clean);
// APA 格式:"作者 (年份). 书名 (第N版). 出版社." —— 年份括号是很硬的锚点,优先按它切
if ($this->takeApa($clean, $out)) {
if ($this->needsLlm($out)) {
$out = $this->refineByLlm($clean, $out);
}
return $out;
}
// 章节引用:"章节作者. 章节名. In: 编者. 书名. 版次. 地点: 出版社; 年. 页码"
// 出版信息属于 In: 之后的那本书,必须分开解析,否则会把编者当成书名
if (preg_match('/^(.*?)\bIn\s*:\s*(.+)$/isu', $clean, $m) && trim($m[1]) !== '' && trim($m[2]) !== '') {
@@ -126,6 +135,146 @@ class BookCitationParser
return preg_replace('/[^0-9Xx]/', '', $m[1]);
}
// ------------------------------------------------------------------
// APA 格式
// ------------------------------------------------------------------
/**
* "Duffy, E., Hockenberry, M., & Gibbs, K. (2023). Wong's Nursing Care of
* Infants and Children (12th ed.). Elsevier."
*/
private function takeApa($text, array &$out)
{
if (!preg_match('/^(.{2,300}?)\s*\(\s*(\d{4})[a-z]?\s*\)\s*\.\s*(.+)$/su', $text, $m)) {
return false;
}
$author = preg_replace('/\(\s*(?:Ed|Eds|Editor|Editors)\.?\s*\)/iu', '', $m[1]);
$out['author'] = $this->normalizeApaAuthors(trim($author, " .,&"));
$out['year'] = $m[2];
// 章节引用:"章节名. In A. Editor (Ed.), 书名 (pp. 1-10). 出版社."
// 必须先认出 (Ed.) 再摘括注,否则 (Ed.) 会被当成版次括注先摘掉
$rest = trim($m[3]);
$containerPart = '';
if (preg_match('/^(.+?)[\.,]\s*\bIn\b\s+.+?\(\s*Eds?\.?\s*\)\s*,\s*(.+)$/isu', $rest, $cm)) {
$rest = trim($cm[1]);
$containerPart = trim($cm[2]);
}
$rest = $this->takeApaParentheticals($rest, $out);
if ($containerPart !== '') {
$containerPart = $this->takeApaParentheticals($containerPart, $out);
}
$segments = $this->apaSegments($containerPart !== '' ? $containerPart : $rest);
if (empty($segments)) {
return false;
}
// APA 的出版社在最后一段,其余都算书名
if (count($segments) >= 2) {
$out['publisher'] = array_pop($segments);
}
$mainTitle = $this->cleanTitle(implode('. ', $segments));
if ($containerPart !== '') {
$out['container'] = $mainTitle;
$chapter = $this->apaSegments($rest);
$out['title'] = $this->cleanTitle(implode('. ', $chapter));
} else {
$out['title'] = $mainTitle;
}
return $out['title'] !== '';
}
/**
* 摘掉 APA 的括注版次与页码:"(12th ed.)"、"(pp. 1-10)"、"(2nd ed., pp. 1-10)"
* 书名自带的括注(如 "(NIC)")不含 ed./pp.,会原样留下
*/
private function takeApaParentheticals($text, array &$out)
{
if (!preg_match_all('/\(([^)]*(?:\bedn?\.|\bedition\b|\bpp?\.)[^)]*)\)/iu', $text, $ms, PREG_SET_ORDER)) {
return $text;
}
foreach ($ms as $item) {
$inner = $item[1];
$edition = $this->matchEdition($inner);
if ($edition !== '' && $out['edition'] === '') {
$out['edition'] = $edition;
}
if ($out['pages'] === ''
&& preg_match('/\bpp?\.\s*([\dA-Za-z]+(?:\s*[-\x{2013}]\s*[\dA-Za-z]+)?)/iu', $inner, $pm)) {
$out['pages'] = preg_replace('/\s+/', '', $pm[1]);
}
$text = str_replace($item[0], ' ', $text);
}
return trim(preg_replace('/\s+/u', ' ', $text));
}
/**
* @return string[]
*/
private function apaSegments($text)
{
$segments = [];
foreach ($this->splitSegments($text) as $seg) {
$seg = trim($seg, " .,;");
if ($seg !== '') {
$segments[] = $seg;
}
}
return $segments;
}
/**
* APA 姓名表 → 温哥华式,与期刊那条链路和 References 的渲染方式保持一致
* "Duffy, E., Hockenberry, M., & Gibbs, K." → "Duffy E, Hockenberry M, Gibbs K"
*/
private function normalizeApaAuthors($author)
{
$author = trim((string)$author);
$author = preg_replace('/\s*&\s*/u', ', ', $author);
$author = preg_replace('/(\s*,\s*)+/u', ', ', $author);
if ($author === '') {
return '';
}
// 没有"姓, 名缩写"结构的(机构作者等)原样保留
if (!preg_match('/,\s*\p{Lu}[\p{L}]*\./u', $author)) {
return $author;
}
$tokens = preg_split('/\s*,\s*/u', $author);
if (!is_array($tokens) || count($tokens) < 2 || count($tokens) % 2 !== 0) {
return $author;
}
$names = [];
for ($i = 0; $i < count($tokens); $i += 2) {
$surname = trim($tokens[$i], " .");
$given = trim($tokens[$i + 1], " .");
if (!preg_match('/^\p{Lu}[\p{L}\'\-\s]*$/u', $surname)
|| !preg_match('/^\p{Lu}[\p{L}\.\s]*$/u', $given)) {
return $author;
}
$initials = '';
foreach (preg_split('/[\s\.]+/u', $given) as $word) {
if ($word !== '') {
$initials .= mb_strtoupper(mb_substr($word, 0, 1), 'UTF-8');
}
}
$names[] = $initials === '' ? $surname : $surname . ' ' . $initials;
}
return implode(', ', $names);
}
// ------------------------------------------------------------------
// 作者
// ------------------------------------------------------------------

View File

@@ -399,14 +399,16 @@ class CrossrefService
}
/**
* 引用格式作者串:姓全写 + 名首字母,超过 $maxAuthors 个取前 N 个 + et al
* Smith JA, Jones B, Lee C, et al
* 引用格式作者串:姓全写 + 名首字母
* 著录规则:作者数 <= $maxAuthors 时全部列出,超过时只列前 $keep 个再加 et al
* 例7 个作者 → Smith JA, Jones B, Lee C, et al
*
* @param array $aDoiInfo Crossref message
* @param int $maxAuthors 最多展示作者数,超过则截断加 et al
* @param int $maxAuthors 全部列出的上限,超过则截断
* @param int $keep 截断后保留的作者数
* @return string
*/
public function getAuthorsCitation($aDoiInfo = [], $maxAuthors = 3)
public function getAuthorsCitation($aDoiInfo = [], $maxAuthors = 6, $keep = 3)
{
$list = [];
if (!empty($aDoiInfo['author'])) {
@@ -436,8 +438,9 @@ class CrossrefService
}
$maxAuthors = max(1, (int)$maxAuthors);
$keep = min(max(1, (int)$keep), $maxAuthors);
if (count($list) > $maxAuthors) {
$list = array_slice($list, 0, $maxAuthors);
$list = array_slice($list, 0, $keep);
return implode(', ', $list) . ', et al';
}

View File

@@ -163,12 +163,9 @@ class JournalArticle
}
$sDoi = empty($v['doi']) ? '' : self::$sDoiUrl.$v['doi'];
//作者
$aAuthorInfo = empty($v['abbr']) ? [] : explode(', ', str_replace([', ',','], ', ', $v['abbr']));
if(count($aAuthorInfo) > 3){
$sAuthorInfo = implode(', ', array_slice($aAuthorInfo,0,3)).", et al.";
}else{
$sAuthorInfo = empty($aAuthorInfo) ? '' : implode(', ', $aAuthorInfo).'.';
}
//作者不超过 6 个全部列出,超过则只列前 3 个加 et al
$sAuthorInfo = empty($v['abbr']) ? '' : \app\common\AuthorListFormatter::format($v['abbr']);
$sAuthorInfo = $sAuthorInfo === '' ? '' : $sAuthorInfo.'.';
$sArticleInfo .= $i.'. Article Title: '.$v['title'].'<br>Author(s): '.$sAuthorInfo.'<br>Link or DOI: '.$sDoi.'<br><br>';
$i++;
}

View File

@@ -406,10 +406,11 @@ class PubmedService
}
/**
* 引用格式作者串:姓全写 + 名首字母,超过 $maxAuthors 个取前 N 个 + et al
* Smith JA, Jones B, Lee C, et al
* 引用格式作者串:姓全写 + 名首字母
* 著录规则:作者数 <= $maxAuthors 时全部列出,超过时只列前 $keep 个再加 et al
* 例7 个作者 → Smith JA, Jones B, Lee C, et al
*/
public function authorsCitation(array $authors, int $maxAuthors = 3): string
public function authorsCitation(array $authors, int $maxAuthors = 6, int $keep = 3): string
{
$list = [];
foreach ($authors as $a) {
@@ -435,8 +436,9 @@ class PubmedService
}
$maxAuthors = max(1, $maxAuthors);
$keep = min(max(1, $keep), $maxAuthors);
if (count($list) > $maxAuthors) {
return implode(', ', array_slice($list, 0, $maxAuthors)) . ', et al';
return implode(', ', array_slice($list, 0, $keep)) . ', et al';
}
return implode(', ', $list);
}

View File

@@ -140,12 +140,12 @@ class ReferenceMetadataService
{
$authors = isset($pub['authors']) && is_array($pub['authors']) ? $pub['authors'] : [];
if (!empty($authors)) {
$citation = $this->pubmed->authorsCitation($authors, 3);
$citation = $this->pubmed->authorsCitation($authors);
if ($citation !== '') {
return $citation;
}
}
return $this->crossref->getAuthorsCitation($cr['raw'] ?? [], 3);
return $this->crossref->getAuthorsCitation($cr['raw'] ?? []);
}
/**