
本文介绍一种绕过网页爬虫陷阱、直接调用欧盟青年门户(youth.europa.eu)官方搜索api的方法,以json格式批量获取志愿服务机会的标题、国家、起止日期、官网链接等结构化字段,并一键转换为pandas dataframe,安全、稳定、可扩展。
本文介绍一种绕过网页爬虫陷阱、直接调用欧盟青年门户(youth.europa.eu)官方搜索api的方法,以json格式批量获取志愿服务机会的标题、国家、起止日期、官网链接等结构化字段,并一键转换为pandas dataframe,安全、稳定、可扩展。
欧盟青年门户网站(youth.europa.eu)虽提供丰富的志愿服务机会信息,但其前端页面采用动态加载+稀疏ID编号(如 /solidarity/placement/39020_en),盲目遍历 1–100,000 ID 极不可靠:大量ID对应404页面,且关键字段(如起止日期)在部分页面缺失,导致 ValueError: not enough values to unpack 等解析异常——这正是原始代码报错的根本原因。
更优解是直接对接后端搜索API。该API由网站自身调用,返回标准JSON,字段完整、语义清晰、支持分页与过滤,完全规避HTML结构变动与反爬风险。
✅ 推荐方案:调用官方搜索API(无需Selenium/BeautifulSoup)
通过浏览器开发者工具(F12 → Network → Filter: XHR)可捕获真实请求地址。例如,访问 volunteering opportunities 页面 时,页面会发起如下API请求:
GET https://www.php.cn/link/033c253e16760c3b1de8b9c807e10bf7/d8/api/rest/eyp/v1/search_en? type=Opportunity &size=100 &from=0 &filters[status]=open &filters[date_end][operator]=>= &filters[date_end][value]=2024-03-14 &filters[date_end][type]=must
该接口返回分页JSON数据,hits.hits 数组即为志愿服务机会列表,每个对象包含 title、country、date_start、date_end、website_url 等标准化字段。
?️ 完整可运行代码(含分页与错误处理)
import requests
import pandas as pd
from typing import List, Dict, Optional
def fetch_volunteer_opportunities(
max_results: int = 1000,
per_page: int = 100,
end_date_from: str = "2024-03-14"
) -> pd.DataFrame:
"""
从欧盟青年门户API批量获取志愿服务机会(结构化JSON)
Args:
max_results: 最大返回条目数(默认1000,可按需调整)
per_page: 单页条目数(最大支持1000,建议≤100防超时)
end_date_from: 筛选“截止日期 ≥ 该日期”的开放机会
Returns:
包含 title, country, date_start, date_end, website_url, url 等字段的DataFrame
"""
base_url = "https://www.php.cn/link/033c253e16760c3b1de8b9c807e10bf7/d8/api/rest/eyp/v1/search_en"
all_data = []
for from_offset in range(0, max_results, per_page):
params = {
"type": "Opportunity",
"size": per_page,
"from": from_offset,
"filters[status]": "open",
"filters[date_end][operator]": ">=",
"filters[date_end][value]": end_date_from,
"filters[date_end][type]": "must"
}
try:
response = requests.get(base_url, params=params, timeout=15)
response.raise_for_status()
data = response.json()
hits = data.get("hits", {}).get("hits", [])
if not hits:
print(f"⚠️ 第 {from_offset//per_page + 1} 页无数据,可能已达末页")
break
# 展平嵌套结构(_source 是实际数据载体)
flattened = [
{**hit.get("_source", {}), "url": f"https://www.php.cn/link/033c253e16760c3b1de8b9c807e10bf7/solidarity/placement/{hit.get('_id')}_en"}
for hit in hits
]
all_data.extend(flattened)
print(f"✅ 已获取 {len(all_data)} 条机会(第 {from_offset//per_page + 1} 页)")
except requests.exceptions.RequestException as e:
print(f"❌ 请求失败(偏移量 {from_offset}): {e}")
continue
except KeyError as e:
print(f"❌ 解析异常(偏移量 {from_offset}): 缺失字段 {e}")
continue
if not all_data:
print("⚠️ 未获取到任何有效数据,请检查网络或API是否变更")
return pd.DataFrame()
# 转为DataFrame并选择核心字段(兼容性更强)
df = pd.json_normalize(all_data)
desired_cols = [
"title", "country", "date_start", "date_end",
"website_url", "url", "description"
]
return df.reindex(columns=[c for c in desired_cols if c in df.columns])
# 执行采集(示例:获取最多500条当前开放机会)
df = fetch_volunteer_opportunities(max_results=500)
print(f"\n? 总计采集 {len(df)} 条志愿服务机会:")
print(df[["title", "country", "date_start", "date_end", "website_url"]].head())⚠️ 关键注意事项
- 尊重 robots.txt 与服务条款:该API属公开接口,但请控制请求频率(建议 ≥1秒间隔),避免对服务器造成压力。
- 字段命名以API响应为准:country 通常为ISO代码(如 "DE"),website_url 可能为空;description 字段含HTML标签,如需纯文本可用 BeautifulSoup(description, "html.parser").get_text() 清洗。
- 分页逻辑:from 参数表示跳过的记录数(非页码),size 为每页数量。若需全部数据,可循环直到返回空 hits。
- 容错设计:代码已内置HTTP异常、JSON解析异常、字段缺失处理,避免单条失败中断整个流程。
✅ 输出示例(DataFrame片段)
| title | country | date_start | date_end | website_url |
|---|---|---|---|---|
| European Voluntary Service in Greece | GR | 2024-06-01 | 2025-05-31 | https://www.php.cn/link/2e5cbd954b383fc43f19deb0e2d1e3da |
| Youth Exchange in Spain | ES | 2024-09-15 | 2024-12-15 | https://www.php.cn/link/41a555c143ddad0bc1d705b05e477867 |
此方法不仅解决原始问题中的404与解析崩溃,更将开发效率提升一个数量级——无需维护CSS选择器、不惧HTML重构、天然支持增量更新与数据校验。对于公益组织、教育机构或青年项目协调员而言,这是构建可持续志愿服务数据库的可靠起点。

















