
本文介绍一种合规、高效的方式,通过调用欧盟青年门户网站(youth.europa.eu)的官方rest api,直接获取结构化json格式的志愿服务机会数据,并使用pandas快速转换为dataframe,避免盲目遍历url带来的404错误与解析异常。
本文介绍一种合规、高效的方式,通过调用欧盟青年门户网站(youth.europa.eu)的官方rest api,直接获取结构化json格式的志愿服务机会数据,并使用pandas快速转换为dataframe,避免盲目遍历url带来的404错误与解析异常。
欧盟青年门户网站(youth.europa.eu)虽未公开完整数据集,但其前端实际依赖后台REST API动态加载内容。与其暴力枚举数万个ID(如 /solidarity/placement/39020_en)导致大量404和解析失败(如 ValueError: not enough values to unpack),不如直接对接其真实数据源——这既符合robots.txt规范,也大幅提升稳定性与效率。
✅ 推荐方案:使用官方搜索API(无需爬虫,无HTML解析风险)
该站点使用Elasticsearch驱动的API接口,路径为:
https://www.php.cn/link/033c253e16760c3b1de8b9c807e10bf7/d8/api/rest/eyp/v1/search_{lang}其中 {lang} 可替换为 en、fr、de 等语言代码。请求参数支持分页、过滤与排序,例如:
| 参数 | 示例值 | 说明 |
|---|---|---|
| type | Opportunity | 固定类型,表示志愿服务机会 |
| size | 1000 | 单次返回最大条目数(上限通常为1000) |
| from | 0 | 起始偏移量,用于分页(from=0 → 第1–1000条;from=1000 → 第1001–2000条) |
| filters[status] | open | 仅获取状态为“开放申请”的机会 |
| filters[date_end][value] | 2024-03-14 | 过滤截止日期晚于该日期的机会 |
? 完整可运行代码(含分页与错误处理)
import requests
import pandas as pd
from time import sleep
def fetch_all_opportunities(
lang="en",
max_results=10000,
batch_size=1000,
delay=0.5 # 避免请求过频,建议 ≥0.5s
):
base_url = f"https://www.php.cn/link/033c253e16760c3b1de8b9c807e10bf7/d8/api/rest/eyp/v1/search_{lang}"
all_data = []
for from_offset in range(0, max_results, batch_size):
params = {
"type": "Opportunity",
"size": batch_size,
"from": from_offset,
"filters[status]": "open",
"filters[date_end][operator]": ">=",
"filters[date_end][value]": "2024-01-01", # 可按需调整起始日期
"filters[date_end][type]": "must"
}
try:
response = requests.get(base_url, params=params, timeout=10)
response.raise_for_status()
hits = response.json().get("hits", {}).get("hits", [])
if not hits:
print(f"⚠️ No more results at 'from={from_offset}'. Stopping.")
break
all_data.extend(hits)
print(f"✅ Fetched {len(hits)} records (offset {from_offset})")
except requests.exceptions.RequestException as e:
print(f"❌ Request failed at offset {from_offset}: {e}")
break
except KeyError as e:
print(f"❌ Unexpected JSON structure at offset {from_offset}: missing key {e}")
break
sleep(delay) # 尊重服务端,避免触发限流
return all_data
# 执行采集(示例:最多获取5000条)
raw_data = fetch_all_opportunities(max_results=5000, batch_size=500)
# 解析为结构化DataFrame(自动展开嵌套字段)
df = pd.json_normalize(
raw_data,
sep="_",
record_path=None,
meta=[
"_source.title",
"_source.country",
"_source.date_start",
"_source.date_end",
"_source.website",
"_source.location",
"_source.description_short"
],
errors="ignore"
).rename(columns={
"_source_title": "Title",
"_source_country": "Country",
"_source_date_start": "Start_Date",
"_source_date_end": "End_Date",
"_source_website": "Website",
"_source_location": "Location",
"_source_description_short": "Description_Short"
})[["Title", "Country", "Location", "Start_Date", "End_Date", "Website"]]
print(f"\n? Total opportunities collected: {len(df)}")
print(df.head())⚠️ 注意事项与最佳实践
- 禁止暴力遍历ID:/solidarity/placement/{id}_en 并非连续编号,大量无效ID将引发高频404,可能被临时封禁IP;
- 始终检查响应结构:API返回为嵌套JSON({"hits": {"total": N, "hits": [...]}}),务必用 .get() 安全访问,避免 KeyError;
- 合理设置请求间隔:添加 time.sleep() 防止被识别为爬虫;生产环境建议配合 requests.Session() 和重试机制;
- 字段映射参考:核心字段位于 _source 对象内,常见键包括 title, country, location, date_start, date_end, website, description_short;
- 导出为CSV/Excel:df.to_csv("eu_volunteering_opportunities.csv", index=False, encoding="utf-8-sig")。
该方法已验证稳定可用,单次请求即可获取千条结构化数据,无需HTML解析、CSS选择器调试或异常捕获冗余逻辑——真正实现「小而准」的数据采集目标。

















