642 lines
22 KiB
PHP
642 lines
22 KiB
PHP
<?php
|
||
|
||
namespace app\common;
|
||
|
||
use think\Env;
|
||
use Smalot\PdfParser\Parser as PdfParser;
|
||
|
||
/**
|
||
* 参考文献内容抓取(cma.j.cn 优先中文源 → Europe PMC → PubMed → PMC全文 → Unpaywall PDF → Crossref)
|
||
*/
|
||
class ReferenceLiteratureFetchService
|
||
{
|
||
/** @var EuropePmcService */
|
||
private $epmc;
|
||
/** @var PubmedService */
|
||
private $pubmed;
|
||
/** @var CrossrefService */
|
||
private $crossref;
|
||
/** @var UnpaywallService */
|
||
private $unpaywall;
|
||
/** @var CmaJournalLiteratureService */
|
||
private $cmaJournal;
|
||
/** @var ReferenceCheckService */
|
||
private $refUtil;
|
||
/** @var \app\common\service\BibliographicMetaService */
|
||
private $bibMeta;
|
||
|
||
/** @var bool 预抓取阶段暂不调用 Yiigle 机构 API */
|
||
private $skipYiigle = false;
|
||
|
||
public function __construct()
|
||
{
|
||
$this->epmc = new EuropePmcService();
|
||
$this->pubmed = new PubmedService([
|
||
'email' => trim((string)Env::get('pubmed_email', '')),
|
||
'tool' => trim((string)Env::get('pubmed_tool', 'tmrjournals')),
|
||
]);
|
||
$this->crossref = new CrossrefService([
|
||
'mailto' => trim((string)Env::get('crossref_mailto', '')),
|
||
]);
|
||
$this->unpaywall = new UnpaywallService();
|
||
$this->cmaJournal = new CmaJournalLiteratureService();
|
||
$this->refUtil = new ReferenceCheckService();
|
||
$this->bibMeta = new \app\common\service\BibliographicMetaService();
|
||
}
|
||
|
||
public function setSkipYiigle($skip = true)
|
||
{
|
||
$this->skipYiigle = (bool)$skip;
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* @return array{
|
||
* doi:string,pmid:string,pmcid:string,title:string,journal:string,year:string,
|
||
* abstract:string,raw_content:string,pdf_url:string,mesh_terms:array,sources:array,fetch_log:string
|
||
* }
|
||
*/
|
||
public function fetchForRefer(array $refer)
|
||
{
|
||
DbReconnectHelper::release();
|
||
|
||
// 图书/ISBN 类参考文献不走 DOI 管道(书上的 DOI 常为错误挂接的期刊文章)
|
||
if ($this->shouldSkipDoiFetchForRefer($refer)) {
|
||
return $this->emptyResult('book_skip_doi_fetch');
|
||
}
|
||
|
||
$dois = $this->resolveDoiCandidatesForFetch($refer);
|
||
foreach ($dois as $doi) {
|
||
$result = $this->fetchByDoiPipeline($doi, $refer);
|
||
if ($this->fetchedResultMatchesRefer($result, $refer)) {
|
||
return $result;
|
||
}
|
||
}
|
||
|
||
$title = trim((string)($refer['title'] ?? ''));
|
||
$author = trim((string)($refer['author'] ?? ''));
|
||
$year = $this->extractYearFromRefer($refer);
|
||
if ($title === '') {
|
||
$title = $this->guessTitleFromReferContent($refer);
|
||
}
|
||
|
||
$resolvedDoi = '';
|
||
$meta = null;
|
||
if ($title !== '') {
|
||
$meta = $this->epmc->searchByBibliographic($title, $author, $year);
|
||
if (is_array($meta) && trim((string)($meta['doi'] ?? '')) !== '') {
|
||
$resolvedDoi = trim((string)$meta['doi']);
|
||
}
|
||
if ($resolvedDoi === '') {
|
||
$pub = $this->pubmed->searchByBibliographic($title, $author, $year);
|
||
if (is_array($pub)) {
|
||
$resolvedDoi = trim((string)($pub['doi'] ?? ''));
|
||
if ($meta === null) {
|
||
$meta = $pub;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($resolvedDoi !== '') {
|
||
$result = $this->fetchByDoiPipeline($resolvedDoi, $refer);
|
||
if (is_array($meta)) {
|
||
if ($result['title'] === '' && trim((string)($meta['title'] ?? '')) !== '') {
|
||
$result['title'] = trim((string)$meta['title']);
|
||
}
|
||
}
|
||
if ($this->fetchedResultMatchesRefer($result, $refer)) {
|
||
$result['fetch_log'] = 'no_doi_in_refer; resolved_doi=' . $resolvedDoi . '; ' . $result['fetch_log'];
|
||
return $result;
|
||
}
|
||
}
|
||
|
||
return $this->emptyResult('no_doi_and_bibliographic_search_failed');
|
||
}
|
||
|
||
/**
|
||
* 判断已入库/已清洗内容与 refer 元数据是否明显错配(用于跳过错误缓存、触发重抓)。
|
||
*/
|
||
public function storedContentMatchesRefer(array $refer, $abstract, $cleaned)
|
||
{
|
||
$text = trim((string)$abstract . "\n" . (string)$cleaned);
|
||
if ($text === '') {
|
||
return true;
|
||
}
|
||
|
||
// 子表内容已按 p_refer_id 绑定;用 refer 标题锚定,避免中文摘要因不含英文作者姓氏被误判为错配
|
||
$expectedTitle = trim((string)($refer['title'] ?? ''));
|
||
|
||
return $this->fetchedResultMatchesRefer([
|
||
'title' => $expectedTitle,
|
||
'abstract' => $text,
|
||
'raw_content' => $text,
|
||
], $refer);
|
||
}
|
||
|
||
/**
|
||
* 图书类参考文献:refer_type=book 或带 ISBN 且呈教材/专著特征时,不通过 DOI 抓外部摘要。
|
||
*/
|
||
private function shouldSkipDoiFetchForRefer(array $refer)
|
||
{
|
||
if (strtolower(trim((string)($refer['refer_type'] ?? ''))) === 'book') {
|
||
return true;
|
||
}
|
||
$isbn = trim((string)($refer['isbn'] ?? ''));
|
||
if ($isbn === '') {
|
||
return false;
|
||
}
|
||
$blob = strtolower(
|
||
trim((string)($refer['joura'] ?? '')) . ' '
|
||
. trim((string)($refer['dateno'] ?? '')) . ' '
|
||
. trim((string)($refer['title'] ?? ''))
|
||
);
|
||
|
||
return preg_match('/\bed\.?|edition|publishing|press|lippincott|elsevier|springer|wiley|company|图书|教材|专著/u', $blob);
|
||
}
|
||
|
||
/**
|
||
* 相关性校对抓取:优先 refer_doi/doilink(结构化字段),再 refer_content(原始文本,可能错链)。
|
||
*
|
||
* @return string[]
|
||
*/
|
||
private function resolveDoiCandidatesForFetch(array $refer)
|
||
{
|
||
$result = [];
|
||
foreach (['refer_doi', 'doilink', 'doi', 'refer_content', 'refer_frag'] as $field) {
|
||
$slice = array_merge($refer, ['refer_content' => (string)($refer[$field] ?? '')]);
|
||
foreach ($this->refUtil->extractAllDoiCandidatesFromRefer($slice) as $doi) {
|
||
if (!in_array($doi, $result, true)) {
|
||
$result[] = $doi;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 校验抓取结果标题/作者是否与 refer 行一致,防止 refer_content 错链到另一篇文献。
|
||
*/
|
||
private function fetchedResultMatchesRefer(array $result, array $refer)
|
||
{
|
||
$expectedTitle = trim((string)($refer['title'] ?? ''));
|
||
$fetchedTitle = trim((string)($result['title'] ?? ''));
|
||
$blob = strtolower(
|
||
$fetchedTitle . ' '
|
||
. trim((string)($result['abstract'] ?? '')) . ' '
|
||
. trim((string)($result['raw_content'] ?? ''))
|
||
);
|
||
|
||
$titleConfirmed = false;
|
||
if ($expectedTitle !== '' && $fetchedTitle !== '') {
|
||
if (!$this->titlesLikelyMatch($expectedTitle, $fetchedTitle)) {
|
||
return false;
|
||
}
|
||
$titleConfirmed = true;
|
||
}
|
||
|
||
$author = trim((string)($refer['author'] ?? ''));
|
||
// 标题已能确认同一文献时,不再要求摘要/正文里出现作者姓氏(PubMed 摘要通常不含作者)
|
||
if ($author !== '' && !$titleConfirmed) {
|
||
$needles = $this->extractAuthorNeedles($author);
|
||
$matched = 0;
|
||
foreach ($needles as $needle) {
|
||
if ($needle !== '' && strpos($blob, $needle) !== false) {
|
||
$matched++;
|
||
}
|
||
}
|
||
if (!empty($needles) && $matched === 0) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private function titlesLikelyMatch($expected, $fetched)
|
||
{
|
||
$a = $this->normalizeTitleForMatch($expected);
|
||
$b = $this->normalizeTitleForMatch($fetched);
|
||
if ($a === '' || $b === '') {
|
||
return true;
|
||
}
|
||
if ($a === $b || strpos($a, $b) !== false || strpos($b, $a) !== false) {
|
||
return true;
|
||
}
|
||
similar_text($a, $b, $pct);
|
||
|
||
return $pct >= 38;
|
||
}
|
||
|
||
private function normalizeTitleForMatch($title)
|
||
{
|
||
$title = strtolower(trim((string)$title));
|
||
$title = preg_replace('/[^a-z0-9\s]+/u', ' ', $title);
|
||
|
||
return trim(preg_replace('/\s+/u', ' ', $title));
|
||
}
|
||
|
||
/**
|
||
* @return string[]
|
||
*/
|
||
private function extractAuthorNeedles($author)
|
||
{
|
||
$author = trim((string)$author);
|
||
if ($author === '') {
|
||
return [];
|
||
}
|
||
$needles = [];
|
||
if (preg_match_all('/[A-Za-z]{3,}/', $author, $m)) {
|
||
foreach ($m[0] as $part) {
|
||
$needles[] = strtolower($part);
|
||
}
|
||
}
|
||
|
||
return array_values(array_unique($needles));
|
||
}
|
||
|
||
/**
|
||
* 抓取 + LLM 清洗(校对执行时调用)
|
||
*
|
||
* @return array 含 abstract_final, content_cleaned
|
||
*/
|
||
public function fetchAndCleanForRefer(array $refer)
|
||
{
|
||
$fetched = $this->fetchForRefer($refer);
|
||
DbReconnectHelper::ensure();
|
||
|
||
$raw = trim((string)($fetched['raw_content'] ?? ''));
|
||
if ($raw === '') {
|
||
return array_merge($fetched, [
|
||
'abstract_final' => trim((string)($fetched['abstract'] ?? '')),
|
||
'content_cleaned' => '',
|
||
'content_clean_skip'=> true,
|
||
]);
|
||
}
|
||
|
||
DbReconnectHelper::release();
|
||
$fetchedForClean = $fetched;
|
||
$clean = (new \app\common\service\ReferenceContentCleanLlmService())->clean($raw, $fetchedForClean);
|
||
DbReconnectHelper::ensure();
|
||
|
||
$abstractFinal = trim((string)($clean['abstract'] ?? ''));
|
||
if ($abstractFinal === '') {
|
||
$abstractFinal = trim((string)($fetched['abstract'] ?? ''));
|
||
}
|
||
|
||
return array_merge($fetched, [
|
||
'abstract_final' => $abstractFinal,
|
||
'content_cleaned' => trim((string)($clean['cleaned'] ?? '')),
|
||
'content_clean_skip' => !empty($clean['skipped']),
|
||
]);
|
||
}
|
||
|
||
private function fetchByDoiPipeline($doi, array $refer)
|
||
{
|
||
$doi = trim((string)$doi);
|
||
$blocks = [];
|
||
$sources = [];
|
||
$abstract = '';
|
||
$title = trim((string)($refer['title'] ?? ''));
|
||
$pmid = '';
|
||
$pmcid = '';
|
||
$journal = '';
|
||
$year = $this->extractYearFromRefer($refer);
|
||
$fetchLogs = [];
|
||
$pdfUrl = '';
|
||
$meshTerms = [];
|
||
$language = '';
|
||
$country = '';
|
||
|
||
// 0) 中华医学会期刊 DOI:OpenAlex 中文摘要(可选 Yiigle 机构 API)
|
||
if (CmaJournalLiteratureService::isCmaJournalDoi($doi)) {
|
||
$cmaSvc = $this->cmaJournal;
|
||
if ($this->skipYiigle) {
|
||
$cmaSvc = new CmaJournalLiteratureService(['skip_yiigle' => true]);
|
||
}
|
||
$cma = $cmaSvc->fetchByDoi($doi);
|
||
if (is_array($cma)) {
|
||
$sources = array_merge($sources, (array)($cma['sources'] ?? []));
|
||
if ($title === '' && trim((string)($cma['title'] ?? '')) !== '') {
|
||
$title = trim((string)$cma['title']);
|
||
}
|
||
if ($journal === '' && trim((string)($cma['journal'] ?? '')) !== '') {
|
||
$journal = trim((string)$cma['journal']);
|
||
}
|
||
if ($year === '' && trim((string)($cma['year'] ?? '')) !== '') {
|
||
$year = trim((string)$cma['year']);
|
||
}
|
||
if ($pmid === '' && trim((string)($cma['pmid'] ?? '')) !== '') {
|
||
$pmid = trim((string)$cma['pmid']);
|
||
}
|
||
if (trim((string)($cma['abstract'] ?? '')) !== '') {
|
||
$abstract = trim((string)$cma['abstract']);
|
||
}
|
||
foreach ((array)($cma['blocks'] ?? []) as $block) {
|
||
$block = trim((string)$block);
|
||
if ($block !== '') {
|
||
$blocks[] = $block;
|
||
}
|
||
}
|
||
$cmaContent = trim((string)($cma['content'] ?? ''));
|
||
if ($cmaContent !== '') {
|
||
$blocks[] = "=== 中华医学期刊全文 ===\n" . $this->truncate($cmaContent, 20000);
|
||
}
|
||
if (trim((string)($cma['fetch_log'] ?? '')) !== '') {
|
||
$fetchLogs[] = (string)$cma['fetch_log'];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 1) Europe PMC by DOI
|
||
$epmc = $this->epmc->searchByDoi($doi);
|
||
if (is_array($epmc)) {
|
||
$sources[] = 'europe_pmc';
|
||
if ($title === '') {
|
||
$title = trim((string)($epmc['title'] ?? ''));
|
||
}
|
||
if (trim((string)($epmc['abstract'] ?? '')) !== '') {
|
||
$abstract = trim((string)$epmc['abstract']);
|
||
$blocks[] = "=== Europe PMC ===\n" . $abstract;
|
||
}
|
||
$pmid = trim((string)($epmc['pmid'] ?? ''));
|
||
$pmcid = trim((string)($epmc['pmcid'] ?? ''));
|
||
$journal = trim((string)($epmc['journal'] ?? ''));
|
||
if ($year === '' && trim((string)($epmc['year'] ?? '')) !== '') {
|
||
$year = trim((string)$epmc['year']);
|
||
}
|
||
}
|
||
|
||
// 2) PubMed metadata
|
||
$pub = $this->pubmed->fetchByDoi($doi);
|
||
if (is_array($pub)) {
|
||
$sources[] = 'pubmed';
|
||
if ($pmid === '' && trim((string)($pub['pmid'] ?? '')) !== '') {
|
||
$pmid = trim((string)$pub['pmid']);
|
||
}
|
||
if ($title === '' && trim((string)($pub['title'] ?? '')) !== '') {
|
||
$title = trim((string)$pub['title']);
|
||
}
|
||
if ($abstract === '' && trim((string)($pub['abstract'] ?? '')) !== '') {
|
||
$abstract = trim((string)$pub['abstract']);
|
||
}
|
||
if (!empty($pub['mesh_terms']) && is_array($pub['mesh_terms'])) {
|
||
$meshTerms = array_values(array_unique(array_merge($meshTerms, $pub['mesh_terms'])));
|
||
}
|
||
if ($language === '' && trim((string)($pub['language'] ?? '')) !== '') {
|
||
$language = strtolower(trim((string)$pub['language']));
|
||
}
|
||
if ($country === '') {
|
||
// 研究开展国以作者单位为准,期刊出版国只作兜底
|
||
$country = $this->bibMeta->detectCountry((array)($pub['affiliations'] ?? []));
|
||
if ($country === '' && trim((string)($pub['journal_country'] ?? '')) !== '') {
|
||
$country = $this->bibMeta->detectCountry((string)$pub['journal_country']);
|
||
}
|
||
}
|
||
$pubBlock = $this->formatPubmedBlock($pub, $doi);
|
||
if ($pubBlock !== '' && $abstract === '') {
|
||
$blocks[] = $pubBlock;
|
||
} elseif ($pubBlock !== '' && !CmaJournalLiteratureService::isCmaJournalDoi($doi)) {
|
||
$blocks[] = $pubBlock;
|
||
}
|
||
}
|
||
|
||
// 3) PMC full text
|
||
if ($pmcid !== '') {
|
||
$full = $this->epmc->fetchFullTextByPmcid($pmcid);
|
||
if ($full !== '') {
|
||
$sources[] = 'pmc_fulltext';
|
||
$blocks[] = "=== PMC Full Text ({$pmcid}) ===\n" . $this->truncate($full, 20000);
|
||
}
|
||
}
|
||
|
||
// 4) Unpaywall OA PDF + PDF parse
|
||
$oaPdfUrl = $this->unpaywall->findOaPdfUrl($doi);
|
||
if ($oaPdfUrl !== '') {
|
||
$pdfUrl = $oaPdfUrl;
|
||
$pdfText = $this->downloadAndExtractPdf($oaPdfUrl);
|
||
if ($pdfText !== '') {
|
||
$sources[] = 'unpaywall_pdf';
|
||
$blocks[] = "=== OA PDF Extract ===\nSource: {$oaPdfUrl}\n" . $this->truncate($pdfText, 25000);
|
||
}
|
||
}
|
||
|
||
if ($pdfUrl === '' && $pmcid !== '') {
|
||
$pdfUrl = $this->buildPmcPdfUrl($pmcid);
|
||
}
|
||
|
||
// 5) Crossref supplement(已有实质性摘要或全文时跳过,避免与 PubMed 等重复)
|
||
if (!$this->shouldSkipCrossrefSupplement($abstract, $sources, $blocks)) {
|
||
$cr = $this->refUtil->fetchCrossrefAbstractByReferDoi(['refer_doi' => $doi, 'doi' => $doi]);
|
||
if (is_array($cr) && trim((string)($cr['text'] ?? '')) !== '') {
|
||
$sources[] = 'crossref';
|
||
$blocks[] = trim((string)$cr['text']);
|
||
if ($abstract === '' && !empty($cr['has_abstract'])) {
|
||
if (preg_match('/Abstract:\s*(.+)/uis', (string)$cr['text'], $m)) {
|
||
$abstract = trim($m[1]);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
$fetchLogs[] = 'crossref=skipped_has_abstract_or_fulltext';
|
||
}
|
||
|
||
$raw = trim(implode("\n\n", array_filter($blocks)));
|
||
if ($raw === '' && $abstract !== '') {
|
||
$raw = $abstract;
|
||
}
|
||
|
||
return [
|
||
'doi' => $doi,
|
||
'pmid' => $pmid,
|
||
'pmcid' => $pmcid,
|
||
'title' => $title,
|
||
'journal' => $journal,
|
||
'year' => $year,
|
||
'abstract' => $abstract,
|
||
'raw_content' => $raw,
|
||
'pdf_url' => $pdfUrl,
|
||
'mesh_terms' => $meshTerms,
|
||
'language' => $language,
|
||
'country' => $country,
|
||
'sources' => array_values(array_unique($sources)),
|
||
'fetch_log' => trim('doi=' . $doi . '; sources=' . implode(',', $sources) . ($fetchLogs ? '; ' . implode('; ', $fetchLogs) : '')),
|
||
];
|
||
}
|
||
|
||
private function shouldSkipCrossrefSupplement($abstract, array $sources, array $blocks)
|
||
{
|
||
if (mb_strlen(trim((string)$abstract)) >= 40) {
|
||
return true;
|
||
}
|
||
if (!empty(array_intersect($sources, ['pmc_fulltext', 'unpaywall_pdf', 'cma_yiigle']))) {
|
||
return true;
|
||
}
|
||
foreach ($blocks as $block) {
|
||
if (preg_match('/===\s*(PMC Full Text|OA PDF Extract|中华医学期刊全文)/u', (string)$block)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private function buildPmcPdfUrl($pmcid)
|
||
{
|
||
$pmcid = strtoupper(trim((string)$pmcid));
|
||
if ($pmcid === '') {
|
||
return '';
|
||
}
|
||
if (strpos($pmcid, 'PMC') !== 0) {
|
||
$pmcid = 'PMC' . preg_replace('/\D/', '', $pmcid);
|
||
}
|
||
|
||
return 'https://pmc.ncbi.nlm.nih.gov/articles/' . rawurlencode($pmcid) . '/pdf/';
|
||
}
|
||
|
||
private function formatPubmedBlock(array $pub, $doi)
|
||
{
|
||
$lines = ['=== PubMed (DOI ' . $doi . ') ==='];
|
||
foreach (['title', 'journal', 'year'] as $k) {
|
||
if (!empty($pub[$k])) {
|
||
$lines[] = ucfirst($k) . ': ' . trim((string)$pub[$k]);
|
||
}
|
||
}
|
||
if (!empty($pub['publication_types'])) {
|
||
$lines[] = 'Publication Types: ' . implode('; ', (array)$pub['publication_types']);
|
||
}
|
||
if (!empty($pub['mesh_terms'])) {
|
||
$lines[] = 'MeSH: ' . implode('; ', (array)$pub['mesh_terms']);
|
||
}
|
||
if (!empty($pub['abstract'])) {
|
||
$lines[] = 'Abstract: ' . trim((string)$pub['abstract']);
|
||
}
|
||
return implode("\n", $lines);
|
||
}
|
||
|
||
private function downloadAndExtractPdf($url)
|
||
{
|
||
$url = trim((string)$url);
|
||
if ($url === '') {
|
||
return '';
|
||
}
|
||
|
||
$dir = ROOT_PATH . 'runtime' . DS . 'ref_literature_pdf';
|
||
if (!is_dir($dir)) {
|
||
@mkdir($dir, 0755, true);
|
||
}
|
||
$path = $dir . DS . date('YmdHis') . '_' . uniqid('', true) . '.pdf';
|
||
|
||
$ch = curl_init($url);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_FOLLOWLOCATION => true,
|
||
CURLOPT_CONNECTTIMEOUT => 15,
|
||
CURLOPT_TIMEOUT => 90,
|
||
CURLOPT_SSL_VERIFYPEER => false,
|
||
CURLOPT_HTTPHEADER => ['User-Agent: TMRjournals-RefFetch/1.0'],
|
||
]);
|
||
$body = curl_exec($ch);
|
||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($body === false || $code < 200 || $code >= 300 || strlen($body) < 1000) {
|
||
return '';
|
||
}
|
||
if (strlen($body) > 15 * 1024 * 1024) {
|
||
return '';
|
||
}
|
||
if (@file_put_contents($path, $body) === false) {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
$text = $this->extractPdfText($path);
|
||
} finally {
|
||
@unlink($path);
|
||
}
|
||
|
||
return $text;
|
||
}
|
||
|
||
private function extractPdfText($path)
|
||
{
|
||
if (!class_exists(PdfParser::class)) {
|
||
return $this->extractPdfTextByPython($path);
|
||
}
|
||
try {
|
||
$parser = new PdfParser();
|
||
$pdf = $parser->parseFile($path);
|
||
$text = $pdf->getText();
|
||
return is_string($text) ? trim($text) : '';
|
||
} catch (\Throwable $e) {
|
||
return $this->extractPdfTextByPython($path);
|
||
}
|
||
}
|
||
|
||
private function extractPdfTextByPython($path)
|
||
{
|
||
$script = ROOT_PATH . 'scripts' . DS . 'extract_pdf_text.py';
|
||
if (!is_file($script)) {
|
||
return '';
|
||
}
|
||
$cmd = 'python ' . escapeshellarg($script) . ' ' . escapeshellarg($path) . ' 2>nul';
|
||
$out = shell_exec($cmd);
|
||
return is_string($out) ? trim($out) : '';
|
||
}
|
||
|
||
private function extractYearFromRefer(array $refer)
|
||
{
|
||
$dateno = trim((string)($refer['dateno'] ?? ''));
|
||
if (preg_match('/(19|20)\d{2}/', $dateno, $m)) {
|
||
return $m[0];
|
||
}
|
||
return '';
|
||
}
|
||
|
||
private function guessTitleFromReferContent(array $refer)
|
||
{
|
||
$content = trim((string)($refer['refer_content'] ?? ''));
|
||
if ($content === '') {
|
||
return '';
|
||
}
|
||
$line = preg_split('/\n/', $content)[0] ?? $content;
|
||
$line = preg_replace('/^\[\d+\]\s*/', '', trim($line));
|
||
return mb_substr($line, 0, 300);
|
||
}
|
||
|
||
private function truncate($text, $max)
|
||
{
|
||
$text = trim((string)$text);
|
||
if ($text === '') {
|
||
return '';
|
||
}
|
||
if (mb_strlen($text) <= $max) {
|
||
return $text;
|
||
}
|
||
return mb_substr($text, 0, $max) . "\n...(truncated)";
|
||
}
|
||
|
||
private function emptyResult($reason)
|
||
{
|
||
return [
|
||
'doi' => '',
|
||
'pmid' => '',
|
||
'pmcid' => '',
|
||
'title' => '',
|
||
'journal' => '',
|
||
'year' => '',
|
||
'abstract' => '',
|
||
'raw_content' => '',
|
||
'pdf_url' => '',
|
||
'mesh_terms' => [],
|
||
'language' => '',
|
||
'country' => '',
|
||
'sources' => [],
|
||
'fetch_log' => (string)$reason,
|
||
];
|
||
}
|
||
}
|