diff --git a/app/Services/Crawl/Adapters/FacultyListHtmlAdapter.php b/app/Services/Crawl/Adapters/FacultyListHtmlAdapter.php index cf2abb8..dac7e20 100644 --- a/app/Services/Crawl/Adapters/FacultyListHtmlAdapter.php +++ b/app/Services/Crawl/Adapters/FacultyListHtmlAdapter.php @@ -250,7 +250,19 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface return ! $this->itemHasEmail($item) || ! $this->itemHasPhone($item) || ! $this->itemHasResearchDirections($item) - || ! $this->itemHasBio($item); + || ! $this->itemHasBio($item) + || ! $this->itemHasAcademicTitle($item); + } + + protected function itemHasAcademicTitle(CrawlItemDto $item): bool + { + $lead = $item->extra['lead_author'] ?? null; + $title = is_array($lead) ? trim((string) ($lead['academic_title'] ?? '')) : ''; + if ($title === '') { + $title = trim((string) ($item->extra['academic_title'] ?? '')); + } + + return CrawlAuthorParser::looksLikeAcademicTitle($title); } protected function itemHasPhone(CrawlItemDto $item): bool @@ -1958,7 +1970,7 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface $lead = is_array($item->extra['lead_author'] ?? null) ? $item->extra['lead_author'] : []; $changed = false; - if (empty($lead['academic_title']) || ! $this->looksLikeAcademicTitle((string) $lead['academic_title'])) { + if (empty($lead['academic_title']) || ! CrawlAuthorParser::looksLikeAcademicTitle((string) $lead['academic_title'])) { $title = $this->extractAcademicTitleFromProfileHtml($html); if ($title !== null && $title !== '') { $lead['academic_title'] = $title; @@ -2058,7 +2070,7 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface // 电院等:

姓名

教授
if (preg_match('#
]*>\s*

[^<]+

\s*\s*([^<]+?)\s*#su', $html, $match)) { $title = CrawlAuthorParser::cleanText($match[1]); - if ($this->looksLikeAcademicTitle((string) $title)) { + if (CrawlAuthorParser::looksLikeAcademicTitle((string) $title)) { return $title; } } @@ -2066,14 +2078,14 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface // 材料学院等:

姓名

教授
if (preg_match('#<(?:div|p)[^>]*class="[^"]*people-name[^"]*"[^>]*>.*?\s*([^<]+?)\s*#su', $html, $titleMatch)) { $title = CrawlAuthorParser::cleanText($titleMatch[1]); - if ($this->looksLikeAcademicTitle((string) $title)) { + if (CrawlAuthorParser::looksLikeAcademicTitle((string) $title)) { return $title; } } foreach (['职称', '职务'] as $label) { $title = $this->parseLabeledField($html, $label); - if ($this->looksLikeAcademicTitle((string) $title)) { + if (CrawlAuthorParser::looksLikeAcademicTitle((string) $title)) { return $title; } } @@ -2081,7 +2093,7 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface if (preg_match_all('/\s*([^<]+?)\s*<\/em>/u', $html, $ems)) { foreach ($ems[1] as $candidate) { $title = CrawlAuthorParser::cleanText($candidate); - if ($this->looksLikeAcademicTitle((string) $title)) { + if (CrawlAuthorParser::looksLikeAcademicTitle((string) $title)) { return $title; } } @@ -2089,7 +2101,7 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface if (preg_match('#
]*>.*?\s*([^<]+?)\s*#su', $html, $match)) { $title = CrawlAuthorParser::cleanText($match[1]); - if ($this->looksLikeAcademicTitle((string) $title)) { + if (CrawlAuthorParser::looksLikeAcademicTitle((string) $title)) { return $title; } } @@ -2097,23 +2109,6 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface return null; } - protected function looksLikeAcademicTitle(?string $title): bool - { - $title = trim((string) $title); - if ($title === '' || mb_strlen($title) > 30) { - return false; - } - - if (preg_match('/首页|导航|菜单|返回|更多|搜索|登录|关于/u', $title)) { - return false; - } - - return (bool) preg_match( - '/教授|副教授|讲师|助教|研究员|副研究员|助理研究员|工程师|院士|博导|导师|专家|学者|长聘|准聘|特聘|兼职|访问|青年|副高|正高|中级|初级/u', - $title, - ); - } - protected function extractPhoneFromProfileHtml(string $html): ?string { $scoped = $this->profileContentHtml($html); diff --git a/app/Services/Crawl/CrawlAuthorParser.php b/app/Services/Crawl/CrawlAuthorParser.php index 8b72a8a..0e43a0d 100644 --- a/app/Services/Crawl/CrawlAuthorParser.php +++ b/app/Services/Crawl/CrawlAuthorParser.php @@ -104,6 +104,26 @@ class CrawlAuthorParser return $text; } + /** + * 判断文本是否像真实职称(过滤导航文案如「电气首页」)。 + */ + public static function looksLikeAcademicTitle(?string $title): bool + { + $title = trim((string) $title); + if ($title === '' || mb_strlen($title) > 30) { + return false; + } + + if (preg_match('/首页|导航|菜单|返回|更多|搜索|登录|关于/u', $title)) { + return false; + } + + return (bool) preg_match( + '/教授|副教授|讲师|助教|研究员|副研究员|助理研究员|工程师|院士|博导|导师|专家|学者|长聘|准聘|特聘|兼职|访问|青年|副高|正高|中级|初级/u', + $title, + ); + } + /** * 递归清洗数组/字符串,确保可安全 json_encode。 * diff --git a/app/Services/Crawl/CrawlImportService.php b/app/Services/Crawl/CrawlImportService.php index 8477d55..8059787 100644 --- a/app/Services/Crawl/CrawlImportService.php +++ b/app/Services/Crawl/CrawlImportService.php @@ -381,7 +381,8 @@ class CrawlImportService } $title = trim((string) ($incoming['title'] ?? '')); - if ($this->isBlankTeacherValue($teacher->title) && $title !== '') { + if ($title !== '' && CrawlAuthorParser::looksLikeAcademicTitle($title) + && ($this->isBlankTeacherValue($teacher->title) || ! CrawlAuthorParser::looksLikeAcademicTitle((string) $teacher->title))) { $updates['title'] = $title; } diff --git a/tests/Unit/CrawlImportTeacherFillEmptyTest.php b/tests/Unit/CrawlImportTeacherFillEmptyTest.php index a10923a..effc03a 100644 --- a/tests/Unit/CrawlImportTeacherFillEmptyTest.php +++ b/tests/Unit/CrawlImportTeacherFillEmptyTest.php @@ -73,4 +73,39 @@ class CrawlImportTeacherFillEmptyTest extends TestCase $teacher->forceDelete(); } } + + public function test_fill_empty_overwrites_invalid_nav_title(): void + { + $teacher = Teacher::query()->create([ + 'name' => '误抓职称测试老师', + 'university_id' => null, + 'department' => '电子信息与电气工程学院', + 'bio' => '已有简介', + 'city' => '上海', + 'title' => '电气首页', + 'email' => 'nav-title-test@sjtu.edu.cn', + 'phone' => '021-11112222', + 'library_status' => TeacherLibraryStatus::Active, + 'remark' => '高校抓取入库', + ]); + + try { + $service = app(CrawlImportService::class); + $changed = $service->fillEmptyTeacherFields($teacher, [ + 'title' => '教授', + 'email' => 'other@sjtu.edu.cn', + 'phone' => '021-99998888', + 'bio' => '不应覆盖的简介', + ]); + + $this->assertTrue($changed); + $teacher->refresh(); + $this->assertSame('教授', $teacher->title); + $this->assertSame('nav-title-test@sjtu.edu.cn', $teacher->email); + $this->assertSame('021-11112222', $teacher->phone); + $this->assertSame('已有简介', $teacher->bio); + } finally { + $teacher->forceDelete(); + } + } }