本文详解如何在不依赖 selenium 的前提下,用纯 scrapy 高效抓取 mdpi 等动态加载、含双重混淆邮箱的学术页面,通过模拟 ajax 分页请求获取全部 50 条/页结果,并正确解密 cloudflare 保护的邮箱地址。
本文详解如何在不依赖 selenium 的前提下,用纯 scrapy 高效抓取 mdpi 等动态加载、含双重混淆邮箱的学术页面,通过模拟 ajax 分页请求获取全部 50 条/页结果,并正确解密 cloudflare 保护的邮箱地址。
MDPI 官网(如 mdpi.com/search)默认仅渲染前 15 篇文章,剩余 35 篇通过 AJAX 异步加载——这是导致你原 Scrapy 爬虫只返回 15 条链接的根本原因。页面未触发后续请求,Scrapy 作为无头静态解析器自然无法获取隐藏内容。关键不是换工具,而是理解并复现前端分页逻辑。
✅ 正确方案:Scrapy + 模拟 AJAX 分页(零 Selenium)
MDPI 使用 /search/set/default/pagination 接口按需加载更多结果。该接口需满足三个条件:
- 请求头包含 X-Requested-With: XMLHttpRequest
- Referer 必须为当前搜索页 URL(否则返回 403)
- User-Agent 需与主页面一致(建议复用 Scrapy 默认 UA 或显式设置)
以下是优化后的完整 Spider 示例(已验证可稳定获取每页 50 篇文章):
import scrapy
import logging
def decode_email_protection(encoded_string):
"""解密 Cloudflare 双重 XOR 编码邮箱(如 mailto:xx#2d786a...)"""
if not encoded_string or '#' not in encoded_string:
return None
try:
# 第一层解码
encoded_data = encoded_string.split('#')[-1]
r1 = int(encoded_data[:2], 16)
email = ''.join([
chr(int(encoded_data[i:i+2], 16) ^ r1)
for i in range(2, len(encoded_data), 2)
])
# 第二层解码(嵌套在第一层结果中)
encoded_data = email.split('#')[-1]
r2 = int(encoded_data[4:6], 16)
encoded_data = encoded_data[:4] + encoded_data[6:] # 跳过干扰字节
email = ''.join([
chr(int(encoded_data[i:i+2], 16) ^ r2)
for i in range(0, len(encoded_data), 2)
])
return email
except (ValueError, IndexError, TypeError):
return None
class MDPIBiomaterialsSpider(scrapy.Spider):
name = "mdpi_biomaterials"
allowed_domains = ["mdpi.com"]
# 关键:启用分页参数 page_count=50,并指定年份范围提升稳定性
base_url = "https://www.mdpi.com/search?sort=pubdate&page_no={}&page_count=50&year_from=1996&year_to=2024&q=biomaterials&view=default"
def start_requests(self):
# 示例:抓取第 218 页(可扩展为循环 range(1, N+1))
yield scrapy.Request(
url=self.base_url.format(218),
cb_kwargs={"page_no": 218, "load_index": 0}
)
def parse(self, response, page_no, load_index):
self.logger.info(f"✅ 解析搜索页 {page_no}(第 {load_index + 1} 批)")
# 提取当前批次可见文章链接
article_hrefs = response.xpath("//a[@class='title-link']/@href").getall()
for href in article_hrefs:
yield response.follow(url=href, callback=self.parse_page)
# 判断是否需加载下一批(每批约 15 条,50 条共需 4 批)
if load_index < 3: # 0→15, 1→30, 2→45, 3→50
headers = {
"X-Requested-With": "XMLHttpRequest",
"Referer": self.base_url.format(page_no),
"User-Agent": response.request.headers.get("User-Agent").decode("utf-8"),
}
yield scrapy.Request(
url="https://www.mdpi.com/search/set/default/pagination",
headers=headers,
cb_kwargs={"page_no": page_no, "load_index": load_index + 1},
dont_filter=True # 避免因相同 URL 被去重
)
def parse_page(self, response):
self.logger.debug(f"? 解析文章页: {response.url}")
title = "".join(
t.strip() for t in response.xpath("//h1[contains(@class,'title')]//text()").getall()
)
authors = [
a.strip() for a in response.xpath("//a[@class='profile-card-drop']//text()").getall()
if a.strip()
]
email_href = response.xpath("//a[contains(@class,'email')]/@href").get()
email = decode_email_protection(email_href)
yield {
"Title": title,
"Link": response.url,
"Authors": authors,
"Email": email
}⚠️ 注意事项与最佳实践
- 分页逻辑验证:使用 scrapy shell 'https://www.mdpi.com/search?q=biomaterials' 进入交互环境,手动执行 fetch('https://www.mdpi.com/search/set/default/pagination') 并检查响应体,确认返回的是 HTML 片段而非重定向。
- UA 一致性:务必复用主请求的 User-Agent,否则 AJAX 请求易被拦截(示例中已通过 response.request.headers 提取)。
- 邮箱解密鲁棒性:添加 try-except 捕获解码异常,避免单条失败导致整个解析中断。
- 反爬应对:若遭遇频率限制,可在 settings.py 中启用 DOWNLOAD_DELAY = 1 和 RANDOMIZE_DOWNLOAD_DELAY = True。
- 扩展性建议:将 page_no 改为 range(1, 220) 即可批量抓取全部页面;结合 CrawlSpider 规则可进一步自动化翻页。
✅ 结论:Selenium 并非必要选择。Scrapy 完全胜任现代动态网站抓取——核心在于分析网络请求,而非渲染页面。本方案将耗时从 Selenium 的秒级降至毫秒级(纯 HTTP 请求),同时保持代码简洁、可维护性强、资源占用低。


















