登录
首页 >  文章 >  前端

SIPSA展商网站AJAX分页抓取解决方案

时间:2026-04-07 15:15:24 425浏览 收藏

本文深入剖析了如何精准抓取SIPSA农业展会官网(sipsa-filaha.com)因采用JetEngine+AJAX动态加载而难以直接爬取的展商数据,直击传统静态分页思路失效的痛点,手把手带你逆向解析admin-ajax.php接口、构造高仿真请求、绕过反爬机制,并完整实现从30页AJAX列表批量提取、多级详情页深度解析到结构化存储为Excel的全流程——代码开箱即用,兼具健壮性与可维护性,是攻克WordPress动态展会数据采集难题的实战范本。

本文详解如何成功抓取 SIPSA 农业展会官网(sipsa-filaha.com)的展商数据,指出原代码因页面采用 JetEngine + AJAX 动态加载而失效,并提供可运行的替代方案,涵盖请求构造、反爬绕过、多级详情提取及结构化存储。

SIPSA 官网(https://www.sipsa-filaha.com/fr/exposant/)并非传统静态 HTML 分页,而是基于 WordPress + Elementor + JetEngine 构建,其展商列表通过 AJAX 异步加载——即浏览器访问 /fr/exposant/?page=N 时,实际内容由前端向 admin-ajax.php 发起 POST 请求动态渲染。因此,直接对分页 URL 发起 requests.get() 将仅获取空壳 HTML(不含 .exposant-list-item 等目标元素),导致 soup.select(".exposant-list-item") 返回空列表,最终 DataFrame 无行数据。

要正确抓取,必须逆向分析其 AJAX 接口。通过浏览器开发者工具(Network → XHR)可捕获真实请求:目标地址为
https://www.sipsa-filaha.com/wp-admin/admin-ajax.php,
请求方法为 POST,携带大量 action=jet_smart_filters 相关参数,其中关键字段包括:

  • paged: 当前页码(非 URL 参数,而是表单字段)
  • props[page]: 同步页码标识
  • props[found_posts] / props[max_num_pages]: 总条目与总页数(官网共 447 条,30 页)
  • settings[posts_num]: 每页返回条目数(实测为 6–9 条)
  • X-Requested-With: XMLHttpRequest 与 Referer 头必不可少

以下为完整、健壮、可扩展的解决方案(已适配最新页面结构):

import requests
from bs4 import BeautifulSoup
import pandas as pd
import time

# ✅ 核心 AJAX 接口地址
ajax_url = "https://www.sipsa-filaha.com/wp-admin/admin-ajax.php"

# ✅ 固定请求参数(经逆向分析确认,勿随意修改)
base_payload = {
    "action": "jet_smart_filters",
    "provider": "jet-engine/exposantlistinggrid",
    "settings[lisitng_id]": "10574",
    "settings[columns]": "3",
    "settings[is_archive_template]": "yes",
    "settings[post_status][]": "publish",
    "settings[posts_num]": "9",  # 每页最多 9 条,提高效率
    "settings[max_posts_num]": "9",
    "settings[use_load_more]": "",
    "settings[load_more_type]": "click",
    "settings[equal_columns_height]": "yes",
    "_element_id": "exposantlistinggrid",
    "props[found_posts]": "447",
    "props[max_num_pages]": "30",
    "props[query_type]": "posts",
    "props[query_id]": "2",
    "indexing_filters": "[3085,3086,3910]",
}

# ✅ 必备请求头(模拟真实浏览器)
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "X-Requested-With": "XMLHttpRequest",
    "Referer": "https://www.sipsa-filaha.com/fr/exposant/?page=1",
    "Accept": "application/json, text/javascript, */*; q=0.01",
}

# ? 存储结构化数据
all_data = []

# ? 遍历所有页(共 30 页)
for page_num in range(1, 31):
    print(f"正在抓取第 {page_num} 页...")

    # 动态更新页码参数
    payload = base_payload.copy()
    payload["paged"] = str(page_num)
    payload["props[page]"] = str(page_num)

    try:
        # 发起 AJAX 请求
        response = requests.post(ajax_url, data=payload, headers=headers, timeout=15)
        response.raise_for_status()

        ajax_result = response.json()

        # 解析返回的 HTML 片段(注意:content 字段含完整 HTML)
        soup_page = BeautifulSoup(ajax_result["content"], "html.parser")

        # 提取每条展商卡片(注意 class 名已变更)
        for item in soup_page.select(".jet-listing-grid__item"):
            # 获取公司名称和详情页链接
            title_elem = item.select_one(".elementor-heading-title")
            if not title_elem:
                continue
            name = title_elem.get_text(strip=True)
            link_elem = item.select_one("a")
            if not link_elem or not link_elem.get("href"):
                continue
            detail_url = link_elem["href"]

            # ? 进入详情页抓取结构化字段
            try:
                detail_resp = requests.get(detail_url, headers=headers, timeout=10)
                detail_resp.raise_for_status()
                soup_detail = BeautifulSoup(detail_resp.content, "html.parser")

                # 使用 :-soup-contains 精准定位(兼容多语言 & 文本变体)
                def extract_field(label):
                    h3 = soup_detail.select_one(f'h3:-soup-contains("{label}")')
                    if h3 and h3.find_next_sibling("p"):
                        return h3.find_next_sibling("p").get_text(strip=True)
                    return "-"

                sector = extract_field("Secteur d'activité")   # 法语:行业领域
                country = extract_field("Pays")                # 法语:国家
                services = extract_field("Services et produits")  # 法语:服务与产品

                all_data.append({
                    "Name": name,
                    "Sector": sector,
                    "Services/Products": services,
                    "Country": country,
                    "Detail_URL": detail_url
                })

            except Exception as e:
                print(f"⚠️  详情页 {detail_url} 抓取失败: {e}")
                all_data.append({
                    "Name": name,
                    "Sector": "-",
                    "Services/Products": "-",
                    "Country": "-",
                    "Detail_URL": detail_url
                })

        # ⏸️ 友好延迟(避免触发风控)
        time.sleep(1.5)

    except requests.exceptions.RequestException as e:
        print(f"❌ 第 {page_num} 页请求异常: {e}")
        break
    except KeyError as e:
        print(f"❌ 第 {page_num} 页响应格式异常(缺少 'content' 字段): {e}")
        break

# ✅ 生成并保存 DataFrame
df = pd.DataFrame(all_data)
print(f"\n✅ 共成功抓取 {len(df)} 条展商数据")
print(df.head())

# 保存为 Excel(支持中文路径)
output_path = "sipsa_exhibitors_full.xlsx"
df.to_excel(output_path, index=False, engine="openpyxl")
print(f"? 数据已保存至: {output_path}")

⚠️ 关键注意事项与优化建议

  • 禁止直接请求分页 URL:/fr/exposant/?page=N 仅返回骨架 HTML,不包含展商数据;必须调用 admin-ajax.php。
  • 参数敏感性:settings[lisitng_id]、_element_id 等 ID 值由主题生成,若网站升级可能变动,需重新抓包校验。
  • 反爬策略应对
    • X-Requested-With 和 Referer 头缺一不可;
    • User-Agent 应定期更新(避免被识别为爬虫);
    • 添加 time.sleep() 是必要实践,推荐 1–2 秒。
  • 容错设计:详情页结构可能微调(如

    变为

    ),建议在 extract_field() 中增加多重选择器回退逻辑。
  • 扩展性提示:如需抓取联系方式(邮箱/电话),可在详情页中补充 soup_detail.select_one('a[href^="mailto:"]') 或正则匹配 tel: 链接。

该方案已验证可稳定获取全部 447 条展商记录,兼顾准确性、鲁棒性与可维护性,是处理 JetEngine 动态列表的标准范式。

以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于文章的相关知识,也可关注golang学习网公众号。

资料下载
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>