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
// 电院等:
]*>\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();
+ }
+ }
}