|
|
|
|
@ -318,7 +318,12 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$configured = (int) ($params['profile_enrich_max'] ?? config('crawl.faculty.profile_enrich_max', 200));
|
|
|
|
|
$configured = (int) ($params['profile_enrich_max'] ?? config('crawl.faculty.profile_enrich_max', 500));
|
|
|
|
|
// 与本次抓取上限对齐,避免名单很长时详情补全被截断
|
|
|
|
|
$maxResults = (int) ($params['max_results'] ?? 0);
|
|
|
|
|
if ($maxResults > 0) {
|
|
|
|
|
$configured = max($configured, min(500, $maxResults));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return max(0, min($itemCount, min(500, $configured)));
|
|
|
|
|
}
|
|
|
|
|
@ -421,27 +426,21 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
{
|
|
|
|
|
$scoped = $this->profileContentHtml($html);
|
|
|
|
|
|
|
|
|
|
$labeledPatterns = [
|
|
|
|
|
'/电子邮箱[::]\s*<\/(?:strong|span|b)>\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子邮箱[::]\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子信箱[::]\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子邮件[::]\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/E-?mail[::]\s*(?:<[^>]+>\s*)*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/iu',
|
|
|
|
|
'/details-tag">\s*电子邮件\s*<\/span>\s*<span class="details-con">\s*(?:<a[^>]*>)?\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子邮箱[::]\s*<\/span>\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/邮箱[::]\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/mailto:([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($labeledPatterns as $pattern) {
|
|
|
|
|
if (preg_match($pattern, $scoped, $match)) {
|
|
|
|
|
$email = CrawlAuthorParser::normalizeEmail($match[1]);
|
|
|
|
|
if ($email && ! $this->isNoiseEmail($email)) {
|
|
|
|
|
// 计算机学院等:优先从联系卡片 .dt 取「邮箱」
|
|
|
|
|
if (preg_match_all('#<div class="dt"[^>]*>(.*?)</div>#su', $scoped, $cards)) {
|
|
|
|
|
foreach ($cards[1] as $cardHtml) {
|
|
|
|
|
$email = $this->extractEmailFromLabeledChunk((string) $cardHtml);
|
|
|
|
|
if ($email !== null) {
|
|
|
|
|
return $email;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$email = $this->extractEmailFromLabeledChunk($scoped);
|
|
|
|
|
if ($email !== null) {
|
|
|
|
|
return $email;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$candidates = [];
|
|
|
|
|
if (preg_match_all(
|
|
|
|
|
'#([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})#',
|
|
|
|
|
@ -449,9 +448,9 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
$emailMatches,
|
|
|
|
|
)) {
|
|
|
|
|
foreach ($emailMatches[1] as $raw) {
|
|
|
|
|
$email = CrawlAuthorParser::normalizeEmail($raw);
|
|
|
|
|
if ($email && ! $this->isNoiseEmail($email)) {
|
|
|
|
|
$candidates[] = $email;
|
|
|
|
|
$normalized = CrawlAuthorParser::normalizeEmail($raw);
|
|
|
|
|
if ($normalized && ! $this->isNoiseEmail($normalized)) {
|
|
|
|
|
$candidates[] = $normalized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -462,15 +461,51 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
|
|
|
|
|
$candidates = array_values(array_unique($candidates));
|
|
|
|
|
|
|
|
|
|
foreach ($candidates as $email) {
|
|
|
|
|
if (str_ends_with($email, '.edu.cn') || str_ends_with($email, '.edu')) {
|
|
|
|
|
return $email;
|
|
|
|
|
foreach ($candidates as $candidate) {
|
|
|
|
|
if (str_ends_with($candidate, '.edu.cn') || str_ends_with($candidate, '.edu')) {
|
|
|
|
|
return $candidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $candidates[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function extractEmailFromLabeledChunk(string $html): ?string
|
|
|
|
|
{
|
|
|
|
|
$labeledPatterns = [
|
|
|
|
|
'/电子邮箱[::]\s*<\/(?:strong|span|b)>\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子邮箱[::]\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子信箱[::]\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子邮件[::]\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/E-?mail[::]\s*(?:<[^>]+>\s*)*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/iu',
|
|
|
|
|
'/details-tag">\s*电子邮件\s*<\/span>\s*<span class="details-con">\s*(?:<a[^>]*>)?\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/电子邮箱[::]\s*<\/span>\s*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/u',
|
|
|
|
|
'/邮箱[::]\s*(?:<a[^>]*href=["\']mailto:([^"\']+)["\'][^>]*>\s*)?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})?/u',
|
|
|
|
|
'/mailto:([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($labeledPatterns as $pattern) {
|
|
|
|
|
if (! preg_match($pattern, $html, $match)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$raw = '';
|
|
|
|
|
foreach (array_slice($match, 1) as $group) {
|
|
|
|
|
if (is_string($group) && trim($group) !== '') {
|
|
|
|
|
$raw = trim($group);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$email = CrawlAuthorParser::normalizeEmail($raw);
|
|
|
|
|
if ($email && ! $this->isNoiseEmail($email)) {
|
|
|
|
|
return $email;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function isNoiseEmail(string $email): bool
|
|
|
|
|
{
|
|
|
|
|
$local = strtolower((string) strstr($email, '@', true));
|
|
|
|
|
@ -2067,6 +2102,14 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
|
|
|
|
|
protected function extractAcademicTitleFromProfileHtml(string $html): ?string
|
|
|
|
|
{
|
|
|
|
|
// 计算机学院等:<div class="zw">长聘教轨副教授</div>
|
|
|
|
|
if (preg_match('#<div class="zw"[^>]*>\s*([^<]+?)\s*</div>#su', $html, $match)) {
|
|
|
|
|
$title = CrawlAuthorParser::cleanText($match[1]);
|
|
|
|
|
if (CrawlAuthorParser::looksLikeAcademicTitle((string) $title)) {
|
|
|
|
|
return $title;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 电院等:<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]);
|
|
|
|
|
@ -2137,6 +2180,9 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
protected function extractBioFromProfileHtml(string $html): ?string
|
|
|
|
|
{
|
|
|
|
|
$patterns = [
|
|
|
|
|
// 计算机学院:js-dt 下「个人简介」整块
|
|
|
|
|
'#<div class="js-dt"[^>]*>.*?<div class="name">\s*(?:<p>)?\s*个人简介\s*(?:</p>)?\s*</div>\s*<div class="txt">(.*?)</div>#su',
|
|
|
|
|
'#<div class="item[^"]*"[^>]*>\s*<div class="name">\s*(?:<p>)?\s*个人简介\s*(?:</p>)?\s*</div>\s*<div class="txt">(.*?)</div>#su',
|
|
|
|
|
'#个人简介</(?:p|div|span)>\s*</div>\s*<div class="(?:txt|p|detail)"[^>]*>(.*?)</div>#su',
|
|
|
|
|
'#class="h3">\s*个人简介\s*</div>\s*<div class="p">(.*?)</div>#su',
|
|
|
|
|
'#class="name"><p>个人简介</p></div>\s*<div class="txt">(.*?)</div>#su',
|
|
|
|
|
@ -2151,7 +2197,7 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
if (preg_match($pattern, $html, $match)) {
|
|
|
|
|
$bio = $this->htmlToPlain($match[1]);
|
|
|
|
|
$bio = CrawlAuthorParser::cleanText($bio);
|
|
|
|
|
if ($bio !== null && mb_strlen($bio) >= 20) {
|
|
|
|
|
if ($bio !== null && mb_strlen($bio) >= 20 && ! $this->isContactLikeBio($bio)) {
|
|
|
|
|
return Str::limit($bio, 2000, '');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -2165,7 +2211,7 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$plain = CrawlAuthorParser::cleanText($plain);
|
|
|
|
|
if ($plain !== null && mb_strlen($plain) >= 40) {
|
|
|
|
|
if ($plain !== null && mb_strlen($plain) >= 40 && ! $this->isContactLikeBio($plain)) {
|
|
|
|
|
return Str::limit($plain, 2000, '');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -2174,6 +2220,23 @@ class FacultyListHtmlAdapter implements CrawlerAdapterInterface
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function isContactLikeBio(string $bio): bool
|
|
|
|
|
{
|
|
|
|
|
// 避免把联系卡片(邮箱/地址/研究所)误当成简介
|
|
|
|
|
if (preg_match('/^(?:邮箱|地址|所在研究所|个人主页|办公电话|联系电话)/u', $bio)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$contactHits = 0;
|
|
|
|
|
foreach (['邮箱', '地址', '所在研究所', '个人主页'] as $label) {
|
|
|
|
|
if (str_contains($bio, $label)) {
|
|
|
|
|
$contactHits++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $contactHits >= 2 && mb_strlen($bio) < 120;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function extractResearchDirectionTextFromProfileHtml(string $html): ?string
|
|
|
|
|
{
|
|
|
|
|
$patterns = [
|
|
|
|
|
|