master
lion 1 month ago
parent 6ba8d2ab51
commit 58b4b55054

@ -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
// 电院等:<div class="tit"><p>姓名</p><span>教授</span></div>
if (preg_match('#<div class="tit"[^>]*>\s*<p>[^<]+</p>\s*<span>\s*([^<]+?)\s*</span>#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
// 材料学院等:<div class="people-name"><p>姓名</p><em>教授</em></div>
if (preg_match('#<(?:div|p)[^>]*class="[^"]*people-name[^"]*"[^>]*>.*?<em>\s*([^<]+?)\s*</em>#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('/<em>\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('#<div class="tit"[^>]*>.*?<span>\s*([^<]+?)\s*</span>#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);

@ -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。
*

@ -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;
}

@ -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();
}
}
}

Loading…
Cancel
Save