电子信息与电气工程学院
HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromHtml'); $method->setAccessible(true); $items = $method->invoke( $adapter, $html, [], 'https://faculty.sjtu.edu.cn/xyjs_list.jsp?urltype=tsites.CollegeTeacherList&wbtreeid=1001&st=0&id=1701&lang=zh_CN', ); $this->assertCount(2, $items); $this->assertSame('沈备军', $items[0]->title); $this->assertSame('http://faculty.sjtu.edu.cn/bjshen/zh_CN/index.htm', $items[0]->canonicalUrl); $this->assertSame('上海交通大学', $items[0]->schoolName); $this->assertSame('faculty_html_tsites', $items[0]->extra['platform']); $this->assertSame('副教授', $items[0]->extra['academic_title']); $this->assertSame('电子信息与电气工程学院', $items[0]->extra['college_name']); $this->assertSame('副教授', $items[0]->extra['lead_author']['academic_title']); $this->assertSame('夏斌', $items[1]->title); } public function test_extracts_teacher_when_profile_href_is_empty(): void { $html = <<<'HTML'
HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromHtml'); $method->setAccessible(true); $items = $method->invoke( $adapter, $html, [], 'https://faculty.sjtu.edu.cn/xyjs_list.jsp?id=1701', ); $this->assertCount(1, $items); $this->assertSame('邵海滨', $items[0]->title); $this->assertStringContainsString('shaohaibin', (string) $items[0]->canonicalUrl); $this->assertSame('副研究员', $items[0]->extra['academic_title']); $this->assertSame('电子信息与电气工程学院', $items[0]->extra['college_name']); } public function test_extracts_email_from_teacher_profile_html(): void { $html = '
  • 电子邮箱:bjshen@sjtu.edu.cn
  • '; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractEmailFromProfileHtml'); $method->setAccessible(true); $email = $method->invoke($adapter, $html); $this->assertSame('bjshen@sjtu.edu.cn', $email); } public function test_apply_email_to_item_updates_lead_author(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'applyEmailToItem'); $method->setAccessible(true); $item = $method->invoke( $adapter, new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:test', title: '沈备军', canonicalUrl: 'http://faculty.sjtu.edu.cn/bjshen/zh_CN/index.htm', extra: [ 'lead_author' => [ 'name' => '沈备军', 'email' => null, 'university_name' => '上海交通大学', ], ], ), 'bjshen@sjtu.edu.cn', ); $this->assertSame('bjshen@sjtu.edu.cn', $item->extra['lead_author']['email']); } public function test_detects_total_pages_and_builds_pagenum_url(): void { $html = '下页'; $adapter = new FacultyListHtmlAdapter; $detect = new \ReflectionMethod($adapter, 'detectTotalPages'); $detect->setAccessible(true); $this->assertSame(20, $detect->invoke($adapter, $html)); $build = new \ReflectionMethod($adapter, 'buildPageUrl'); $build->setAccessible(true); $url = $build->invoke( $adapter, 'https://faculty.sjtu.edu.cn/xyjs_list.jsp?urltype=tsites.CollegeTeacherList&wbtreeid=1001&st=0&id=1701&lang=zh_CN', 3, $html, ); $this->assertStringContainsString('PAGENUM=3', $url); $this->assertStringContainsString('totalpage=20', $url); } public function test_extracts_smse_staff_panel_list(): void { $html = <<<'HTML' 教师名录 - 上海交通大学材料科学与工程学院
    塑性成形技术与装备研究院
    陈军 韩先洪
    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromHtml'); $method->setAccessible(true); $items = $method->invoke( $adapter, $html, [], 'https://smse.sjtu.edu.cn/people/staff_new/department', ); $this->assertCount(2, $items); $this->assertSame('陈军', $items[0]->title); $this->assertSame('https://smse.sjtu.edu.cn/people/detail_new/20092', $items[0]->canonicalUrl); $this->assertSame('上海交通大学', $items[0]->schoolName); $this->assertSame('faculty_html_smse', $items[0]->extra['platform']); $this->assertSame('塑性成形技术与装备研究院', $items[0]->extra['college_name']); } public function test_apply_profile_metadata_from_smse_detail_page(): void { $html = <<<'HTML'

    陈军

    教授
    所属二级机构:塑性成形技术与装备研究院
    电子邮箱:jun_chen@sjtu.edu.cn
    研究方向
    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'applyProfileMetadataToItem'); $method->setAccessible(true); $item = $method->invoke( $adapter, new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:test', title: '陈军', canonicalUrl: 'https://smse.sjtu.edu.cn/people/detail_new/20092', extra: [ 'lead_author' => [ 'name' => '陈军', 'email' => null, 'university_name' => '上海交通大学', ], ], ), $html, ); $this->assertSame('教授', $item->extra['lead_author']['academic_title']); $this->assertSame('塑性成形技术与装备研究院', $item->extra['college_name']); $this->assertSame( ['轻质高强薄板塑性成形与数控渐进成形', '塑性变形力学模型及其多物理场数值仿真'], $item->extra['research_direction_names'], ); } public function test_apply_profile_metadata_from_see_detail_page(): void { $html = <<<'HTML'

    办公电话:021-34204298

    电子邮件:aiqian@sjtu.edu.cn

    研究兴趣

    电力系统元件建模, 电能质量, 分布式发电

    教育背景

    1994.09 –1998.07清华大学

    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'applyProfileMetadataToItem'); $method->setAccessible(true); $item = $method->invoke( $adapter, new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:see', title: '艾芊', canonicalUrl: 'https://see.sjtu.edu.cn/jiaoshiml/aiqian.html', extra: ['lead_author' => ['name' => '艾芊']], ), $html, ); $this->assertSame('021-34204298', $item->extra['phone']); $this->assertSame(['电力系统元件建模', '电能质量', '分布式发电'], $item->extra['research_direction_names']); $emailMethod = new \ReflectionMethod($adapter, 'extractEmailFromProfileHtml'); $emailMethod->setAccessible(true); $this->assertSame('aiqian@sjtu.edu.cn', $emailMethod->invoke($adapter, $html)); } public function test_apply_profile_metadata_from_see_tit_span_not_nav_em(): void { $html = <<<'HTML' 电气首页

    电气首页

    王承民

    教授

    办公电话:021-12345678

    电子邮件:wcm@sjtu.edu.cn

    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'applyProfileMetadataToItem'); $method->setAccessible(true); $item = $method->invoke( $adapter, new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:see-wcm', title: '王承民', canonicalUrl: 'https://see.sjtu.edu.cn/jiaoshiml/wangchengmin.html', extra: [ 'lead_author' => [ 'name' => '王承民', // 模拟上次误抓导航文案,详情页应覆盖 'academic_title' => '电气首页', ], ], ), $html, ); $this->assertSame('教授', $item->extra['academic_title']); $this->assertSame('教授', $item->extra['lead_author']['academic_title']); } public function test_apply_profile_metadata_from_cs_and_frontier_pages(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'applyProfileMetadataToItem'); $method->setAccessible(true); $csHtml = <<<'HTML'
    张晓凡
    长聘教轨副教授

    邮箱:xiaofan.zhang@sjtu.edu.cn

    地址:剑川路930号A栋322B

    所在研究所:清源研究院

    个人主页:https://zhangxiaofan101.github.io

    个人简介

    上海交通大学计算机学院清源研究院长聘教轨副教授,博士生导师。研究领域:医学图像、文本分析,多模态决策。

    教育背景

    北京航空航天大学学士,美国北卡罗莱纳大学夏洛特分校博士。

    HTML; $csItem = $method->invoke( $adapter, new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:cs', title: '张晓凡', canonicalUrl: 'https://www.cs.sjtu.edu.cn/jiaoshiml/zhangxiaofan.html', extra: ['lead_author' => ['name' => '张晓凡']], ), $csHtml, ); $this->assertSame('长聘教轨副教授', $csItem->extra['academic_title']); $this->assertSame('清源研究院', $csItem->extra['college_name']); $this->assertStringContainsString('医学图像', (string) $csItem->extra['bio']); $this->assertStringNotContainsString('教育背景', (string) $csItem->extra['bio']); $this->assertStringNotContainsString('北京航空航天大学学士', (string) $csItem->extra['bio']); $emailMethod = new \ReflectionMethod($adapter, 'extractEmailFromProfileHtml'); $emailMethod->setAccessible(true); $this->assertSame('xiaofan.zhang@sjtu.edu.cn', $emailMethod->invoke($adapter, $csHtml)); $frontierHtml = <<<'HTML'
    电子邮件 baoming@nju.edu.cn
    联系电话 0512-68768786
    研究领域

    微纳材料与微纳系统

    个人简介

    2006.09-2010.07 厦门大学,本科;长期从事微纳材料研究。

    HTML; $frontierItem = $method->invoke( $adapter, new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:frontier', title: '王保明', canonicalUrl: 'https://frontier.nju.edu.cn/85/ef/c59286a689647/page.htm', extra: ['lead_author' => ['name' => '王保明']], ), $frontierHtml, ); $this->assertSame('0512-68768786', $frontierItem->extra['phone']); $this->assertSame(['微纳材料与微纳系统'], $frontierItem->extra['research_direction_names']); $this->assertStringContainsString('厦门大学', (string) $frontierItem->extra['bio']); } public function test_extracts_ra_teacher_list_with_research_directions(): void { $html = <<<'HTML' 专职教师-南京大学机器人与自动化学院 HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromHtml'); $method->setAccessible(true); $items = $method->invoke($adapter, $html, [], 'https://ra.nju.edu.cn/szll/zzjs/index.html'); $this->assertCount(1, $items); $this->assertSame('周克敏', $items[0]->title); $this->assertSame('http://ra.nju.edu.cn/szll/zzjs/20250901/i335910.html', $items[0]->canonicalUrl); $this->assertSame(['鲁棒控制', '多目标优化控制'], $items[0]->extra['research_direction_names']); } public function test_item_needs_profile_enrich_when_research_missing(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'itemNeedsProfileEnrich'); $method->setAccessible(true); $item = new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:x', title: '张三', canonicalUrl: 'https://see.sjtu.edu.cn/jiaoshiml/zhangsan.html', extra: [ 'lead_author' => [ 'name' => '张三', 'email' => 'zhangsan@sjtu.edu.cn', ], ], ); $this->assertTrue($method->invoke($adapter, $item)); } public function test_resolve_profile_enrich_max_caps_large_batches(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'resolveProfileEnrichMax'); $method->setAccessible(true); $this->assertSame(500, $method->invoke($adapter, [], 500)); $this->assertSame(10, $method->invoke($adapter, ['profile_enrich_max' => 10], 500)); $this->assertSame(300, $method->invoke($adapter, ['max_results' => 300], 300)); $this->assertSame(0, $method->invoke($adapter, ['skip_profile_enrich' => true], 500)); } public function test_extracts_sais_js_list_from_ajax_content(): void { $html = <<<'HTML' 教师名录-上海交通大学自动化与感知学院
  • 白洋
  • 陈新
  • HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromAjaxTeacherContent'); $method->setAccessible(true); $items = $method->invoke( $adapter, $html, [], 'https://sais.sjtu.edu.cn/faculty.html', 'faculty', ); $this->assertCount(2, $items); $this->assertSame('白洋', $items[0]->title); $this->assertSame('https://sais.sjtu.edu.cn/faculty/baiyang.html', $items[0]->canonicalUrl); $this->assertSame('faculty_html_ajax', $items[0]->extra['platform']); $this->assertSame('上海交通大学', $items[0]->schoolName); } public function test_extracts_cs_rc_item_teacher_list(): void { $html = <<<'HTML' 教师名录-上海交通大学计算机学院(网络空间安全学院、密码学院)
    并行与分布式系统研究所

    所长:臧斌宇

    陈海波

    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromAjaxTeacherContent'); $method->setAccessible(true); $items = $method->invoke( $adapter, $html, [], 'https://www.cs.sjtu.edu.cn/jiaoshiml.html', 'jiaoshiml', ); $this->assertCount(2, $items); $this->assertSame('臧斌宇', $items[0]->title); $this->assertSame('并行与分布式系统研究所', $items[0]->extra['college_name']); $this->assertSame('https://www.cs.sjtu.edu.cn/jiaoshiml/chenhaibo.html', $items[1]->canonicalUrl); } public function test_response_body_from_pool_result_ignores_connection_exception(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'responseBodyFromPoolResult'); $method->setAccessible(true); $this->assertNull($method->invoke($adapter, new \GuzzleHttp\Exception\ConnectException( 'Connection timed out', new \GuzzleHttp\Psr7\Request('GET', 'https://faculty.sjtu.edu.cn/test'), ))); $this->assertNull($method->invoke($adapter, null)); } public function test_parses_icisee_ajax_teacher_config_without_cat_id(): void { $html = <<<'HTML' HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'parseAjaxTeacherConfig'); $method->setAccessible(true); $config = $method->invoke($adapter, $html, 'https://icisee.sjtu.edu.cn/jiaoshiml.html'); $this->assertSame('simple', $config['variant']); $this->assertNull($config['cat_id']); $this->assertSame('jiaoshiml', $config['cat_code']); $this->assertSame('https://icisee.sjtu.edu.cn/active/ajax_teacher_list.html', $config['api_url']); $this->assertTrue($config['uses_page']); } public function test_extracts_icisee_card_style_teacher_list(): void { $html = <<<'HTML' 教师名录-上海交通大学集成电路学院(信息与电子工程学院)
    蔡星汉教授

    微纳全重党支部书记

    张三副教授
    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromAjaxTeacherContent'); $method->setAccessible(true); $items = $method->invoke( $adapter, $html, [], 'https://icisee.sjtu.edu.cn/jiaoshiml.html', 'jiaoshiml', ); $this->assertCount(2, $items); $this->assertSame('蔡星汉', $items[0]->title); $this->assertSame('教授', $items[0]->extra['academic_title']); $this->assertSame('https://icisee.sjtu.edu.cn/jiaoshiml/caixinghan.html', $items[0]->canonicalUrl); $this->assertSame('张三', $items[1]->title); $this->assertSame('副教授', $items[1]->extra['academic_title']); $this->assertSame('faculty_html_ajax', $items[0]->extra['platform']); } public function test_detects_nju_teacher_home_page(): void { $html = ''; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'isNjuTeacherHomePage'); $method->setAccessible(true); $this->assertTrue($method->invoke($adapter, $html)); $this->assertFalse($method->invoke($adapter, '')); } public function test_builds_nju_teacher_home_conditions_for_all_faculty(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'buildNjuTeacherHomeConditions'); $method->setAccessible(true); $conditions = $method->invoke($adapter, null, null); $this->assertCount(1, $conditions); $this->assertSame('published', $conditions[0]['field']); } public function test_builds_nju_teacher_home_conditions_for_professor_category(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'buildNjuTeacherHomeConditions'); $method->setAccessible(true); $conditions = $method->invoke($adapter, '教授', null); $this->assertCount(2, $conditions); $this->assertSame('exField2', $conditions[1]['field']); $this->assertSame('教授', $conditions[1]['value']); } public function test_parses_nju_site_id_from_html(): void { $html = ''; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'parseNjuSiteId'); $method->setAccessible(true); $this->assertSame(786, $method->invoke($adapter, $html)); } public function test_infers_college_from_meta_description(): void { $html = '师资力量'; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'inferCollegeFromPageTitle'); $method->setAccessible(true); $this->assertSame('智能科学与技术学院', $method->invoke($adapter, $html)); } public function test_extracts_sudy_news_faculty_list(): void { $html = <<<'HTML' 师资力量-南京大学前沿科学学院
  • 功能材料与智能制造研究院

  • HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromSudyNewsFacultyList'); $method->setAccessible(true); $items = $method->invoke($adapter, $html, [], 'https://frontier.nju.edu.cn/zrjs/list.htm'); $this->assertCount(1, $items); $this->assertSame('王保明', $items[0]->title); $this->assertSame('功能材料与智能制造研究院', $items[0]->extra['college_name']); $this->assertSame('faculty_html_sudy_news', $items[0]->extra['platform']); } public function test_extracts_ic_nju_titles_from_news_title1_and_comments(): void { $qyml = <<<'HTML' 全院名录-南京大学集成电路学院 HTML; $overview = <<<'HTML' 师资队伍-南京大学集成电路学院 HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromSudyNewsFacultyList'); $method->setAccessible(true); $qymlItems = $method->invoke($adapter, $qyml, [], 'https://ic.nju.edu.cn/qyml/list.htm'); $this->assertSame('王欣然', $qymlItems[0]->title); $this->assertSame('教授,博导', $qymlItems[0]->extra['academic_title']); $overviewItems = $method->invoke($adapter, $overview, [], 'https://ic.nju.edu.cn/56606/list.htm'); $this->assertSame('王欣然', $overviewItems[0]->title); $this->assertSame('教授,博导', $overviewItems[0]->extra['academic_title']); $profileHtml = '
    职称:教授,博导
    '; $titleMethod = new \ReflectionMethod($adapter, 'extractAcademicTitleFromProfileHtml'); $titleMethod->setAccessible(true); $this->assertSame('教授,博导', $titleMethod->invoke($adapter, $profileHtml)); } public function test_extracts_ra_teacher_cards(): void { $html = <<<'HTML' 专职教师-南京大学机器人与自动化学院 HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromRaTeacherList'); $method->setAccessible(true); $items = $method->invoke($adapter, $html, [], 'https://ra.nju.edu.cn/szll/zzjs/index.html'); $this->assertCount(1, $items); $this->assertSame('周克敏', $items[0]->title); $this->assertSame('教授', $items[0]->extra['academic_title']); $this->assertSame('faculty_html_ra', $items[0]->extra['platform']); } public function test_extracts_vsb_faculty_table(): void { $html = <<<'HTML' 专兼职教师-南京大学智能软件与工程学院

    教授

    杨鲲陶先平

    副教授

    邵栋
    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'extractFromVsbFacultyTable'); $method->setAccessible(true); $items = $method->invoke($adapter, $html, [], 'https://ise.nju.edu.cn/szll/zjzjs.htm'); $this->assertCount(3, $items); $names = array_map(fn ($item) => $item->title, $items); $this->assertContains('杨鲲', $names); $this->assertContains('陶先平', $names); $this->assertContains('邵栋', $names); $titles = array_column(array_map(fn ($item) => $item->extra, $items), 'academic_title', null); $this->assertSame('教授', $items[array_search('杨鲲', $names, true)]->extra['academic_title']); $this->assertSame('副教授', $items[array_search('邵栋', $names, true)]->extra['academic_title']); } public function test_parse_research_direction_names_keeps_valid_utf8_with_chinese_separators(): void { $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'parseResearchDirectionNames'); $method->setAccessible(true); $names = $method->invoke( $adapter, '纳米材料与器件、先进功能涂层、轻质高强金属材料', ); $this->assertSame( ['纳米材料与器件', '先进功能涂层', '轻质高强金属材料'], $names, ); foreach ($names as $name) { $this->assertTrue(mb_check_encoding($name, 'UTF-8')); } $this->assertNotFalse(json_encode(['research_direction_names' => $names], JSON_THROW_ON_ERROR)); } public function test_smse_long_research_li_items_are_kept_as_whole_and_json_safe(): void { $html = <<<'HTML'
    研究方向
    HTML; $adapter = new FacultyListHtmlAdapter; $method = new \ReflectionMethod($adapter, 'applyProfileMetadataToItem'); $method->setAccessible(true); $item = $method->invoke( $adapter, new \App\Services\Crawl\CrawlItemDto( externalId: 'faculty:smse-utf8', title: '赵斌元', canonicalUrl: 'https://smse.sjtu.edu.cn/people/detail_new/1', extra: ['lead_author' => ['name' => '赵斌元']], ), $html, ); $names = $item->extra['research_direction_names']; $this->assertSame( ['高性能陶瓷基复合材料及其构件制备', '金属材料强韧化与表面改性'], $names, ); $this->assertNotFalse(json_encode(['research_direction_names' => $names], JSON_THROW_ON_ERROR)); } }