欢迎光临
我们一直在努力

Python动态网页数据采集实战:JS逆向破解与自动化爬虫实现

1. 项目概述与核心价值

最近在做一个社群运营分析的小工具,核心需求是从某个社交平台的群组里,把成员数据给“搬”出来。听起来简单,不就是爬虫嘛?但实际操作起来,发现页面数据是通过JavaScript动态加载的,直接

requests.get()

拿到的HTML里空空如也,关键的用户列表压根不在里面。这就不得不踏入“JS逆向”这个听起来有点技术门槛的领域了。这个项目,本质上就是一次典型的针对动态网页的数据采集实战,核心在于破解前端JavaScript的数据获取逻辑,并用Python将其复现。

这个过程对于数据分析师、运营人员或者任何需要从现代Web应用中获取结构化数据的朋友来说,价值巨大。现在越来越多的网站和应用为了性能和用户体验,都采用了前后端分离的架构,数据通过Ajax接口异步加载。如果你还停留在解析静态HTML的层面,很多数据源对你来说就是“隐形”的。通过这个项目,你将不仅学会如何用Python采集数据,更重要的是掌握一套分析、理解并模拟前端JavaScript行为的通用方法论。这能让你在面对各种加密参数、复杂认证的动态网站时,有路可循,而不是束手无策。

2. 逆向分析的核心思路与工具准备

逆向分析不是盲目地猜测,而是有策略地“侦察”和“推理”。我们的目标很明确:找到前端JavaScript是从哪个接口、以何种方式获取到群成员数据的。整个思路可以概括为“抓包定位 -> 静态分析 -> 动态调试 -> Python复现”。

2.1 核心工具链选择

工欲善其事,必先利其器。以下是经过多次实战检验的工具组合:

  • 浏览器开发者工具 (Chrome DevTools)

    :这是我们的主战场。尤其是

    Network(网络)

    面板和

    Sources(源代码)

    面板。Network面板用于监听所有网络请求,快速定位到目标数据接口;Sources面板用于设置断点,动态跟踪JavaScript的执行流程。

  • Python请求库:Requests 或 httpx

    Requests

    库经典、稳定,社区资源丰富,是大多数情况下的首选。

    httpx

    支持HTTP/2和异步,在处理大量并发请求时更有优势。本项目以

    Requests

    为例进行讲解。

  • 解析与模拟库

    • json

      : 目标接口返回的数据大概率是JSON格式,Python标准库的

      json

      模块足矣。

    • execjs

      PyExecJS

      : 如果逆向发现前端有复杂的本地加密(如用JS生成的签名、Token),需要直接在Python环境中执行JavaScript代码片段时使用。但我们的首要目标是

      避免

      走到这一步,而是通过分析,用Python原生代码模拟出关键逻辑。

    • re

      (正则表达式): 有时密钥或参数会硬编码在JS文件里,用正则快速提取。

  • 辅助分析工具

    • Charles / Fiddler / mitmproxy

      : 高级抓包工具,可以拦截和修改HTTPS流量,对移动端APP或复杂场景的分析尤其有用。在浏览器DevTools搞不定时,它们是强力后备。

    • Pretty Print (美化)

      : Network面板里看到的JS代码通常是压缩混淆过的,一行代码几万字符。点击代码区域左下角的

      {}

      按钮,可以格式化代码,让其变得可读。

2.2 关键分析流程拆解

  • 开启记录

    : 打开目标群组页面,先打开DevTools的Network面板,勾选

    Preserve log

    (保留日志),防止页面跳转时请求记录被清空。然后刷新页面。

  • 筛选与定位

    : 在Network面板的筛选栏,输入

    XHR

    Fetch

    ,过滤出Ajax请求。一边在页面上操作(如滚动加载更多成员),一边观察Network面板中新出现的请求。重点关注那些响应体(Preview)里包含类似

    memberList

    ,

    userInfo

    ,

    data

    等字段的请求。

  • 请求剖析

    : 点击找到的疑似接口,查看其

    Headers

    详情。你需要关注:

    • Request URL

      : 接口地址,注意查询参数(

      ?

      后面的部分)。

    • Request Method

      : 通常是

      GET

      POST

    • Request Headers

      : 重中之重!特别是

      Cookie

      ,

      Authorization

      ,

      User-Agent

      ,

      Referer

      , 以及一些自定义的头部如

      X-Sign

      ,

      X-Timestamp

      等。这些往往是服务端用于验证请求合法性的关键。

    • Query String Parameters / Form Data

      : 请求携带的参数。同样,可能包含加密或动态生成的参数。

  • 溯源与破解

    : 这是逆向的核心。右键点击该请求,选择

    Copy -> Copy as cURL

    ,然后到

    https://curlconverter.com/

    这类网站可以快速转为Python代码,但这只是第一步。你需要弄清楚那些可疑的参数(比如一个叫

    _signature

    的加密字符串)是怎么生成的。这时就要用到

    Sources

    面板进行搜索和断点调试了。

  • 注意

    : 不要一上来就试图理解整个压缩过的JS文件。先用搜索功能(Ctrl+Shift+F),在全站资源中搜索关键参数名,如

    _signature

    token

    或接口URL的一部分,来定位生成这些参数的函数所在的具体JS文件。

    3. 实战:一个简化的逆向案例拆解

    假设我们通过Network面板,找到了一个获取群成员列表的接口:

    GET https://api.example-group.com/member/list?groupId=123456&page=1&size=20&_t=1648886400000&_sign=abc123def456

    3.1 参数分析

    • groupId

      ,

      page

      ,

      size

      : 这些是业务参数,含义明确,我们直接构造即可。

    • _t

      : 看起来是一个时间戳(13位毫秒时间戳)。在Python中可以用

      int(time.time() * 1000)

      轻松生成。

    • _sign

      : 最可疑的参数,看起来是一个签名。签名的目的是防止参数被篡改,通常是使用所有参数(可能加上一个密钥)通过某种算法(如MD5, SHA1, HMAC-SHA256)计算得出的。

    3.2 逆向签名生成逻辑

    我们的目标是找到

    _sign

    的生成方式。

  • 全局搜索

    : 在Sources面板,按

    Ctrl+Shift+F

    ,搜索

    _sign

    sign

    。你可能会在某个JS文件中找到类似这样的代码片段(经过格式化后):
    function generateSign(params) {
    var key = "a_secret_key_here";
    // 1. 参数排序
    var sortedKeys = Object.keys(params).sort();
    var strToSign = "";
    for (var i = 0; i < sortedKeys.length; i++) {
    var k = sortedKeys[i];
    if (params[k] !== null && params[k] !== undefined) {
    strToSign += k + "=" + params[k] + "&";
    }
    }
    // 2. 拼接密钥
    strToSign += "key=" + key;
    // 3. MD5加密
    var sign = md5(strToSign).toUpperCase();
    return sign;
    }

  • 逻辑解读

    : 这段代码揭示了一个常见的签名流程:

    • 将请求参数(

      groupId

      ,

      page

      ,

      size

      ,

      _t

      )按键名

      升序排序

    • 将它们拼接成

      key1=value1&key2=value2&…

      的格式。

    • 在末尾拼接一个固定的

      key=密钥

    • 对整个拼接后的字符串进行

      MD5

      计算,并将结果转为大写。

  • 验证与调试

    : 在Sources面板找到这段代码,在

    return sign;

    这一行打上断点。然后回到页面触发请求(比如翻页)。当代码执行到断点时,查看

    params

    ,

    strToSign

    ,

    sign

    等变量的值,与你通过Network面板看到的实际请求参数和

    _sign

    值进行比对,确认逻辑是否正确。

  • 3.3 Python代码复现

    一旦分析清楚逻辑,用Python复现就非常直接了:

    import hashlib
    import time
    import requests

    def generate_sign(params, secret_key):
    """
    模拟JS端的签名生成算法
    :param params: 参数字典
    :param secret_key: 从JS中分析出的密钥
    :return: 签名字符串
    """
    # 1. 参数排序
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    # 2. 拼接键值对
    str_to_sign = '&'.join([f'{k}={v}' for k, v in sorted_params if v is not None])
    # 3. 拼接密钥
    str_to_sign += f'&key={secret_key}'
    # 4. MD5加密并大写
    m = hashlib.md5()
    m.update(str_to_sign.encode('utf-8'))
    return m.hexdigest().upper()

    # 使用示例
    secret_key = "a_secret_key_here" # 这个密钥需要从JS代码中分析提取
    group_id = "123456"
    page = 1
    size = 20
    timestamp = int(time.time() * 1000)

    # 构造参数字典
    params = {
    "groupId": group_id,
    "page": page,
    "size": size,
    "_t": timestamp
    }
    # 生成签名
    signature = generate_sign(params, secret_key)
    # 将签名加入参数
    params['_sign'] = signature

    # 构造请求头,Cookie需要从登录后的浏览器中获取
    headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Cookie": "你的登录Cookie字符串", # 关键!如何获取见下文
    "Referer": "https://xxx-group.com/group/123456" # 通常需要添加来源页
    }

    # 发送请求
    url = "https://api.example-group.com/member/list"
    response = requests.get(url, params=params, headers=headers)
    if response.status_code == 200:
    member_list = response.json()
    print(f"获取到第{page}页,共{len(member_list.get('data', []))}条成员数据")
    # 后续处理数据…
    else:
    print(f"请求失败,状态码:{response.status_code}")
    print(response.text)

    实操心得

    : 密钥 (

    secret_key

    ) 的查找是逆向中的难点。它可能是一个硬编码的字符串(如上例),也可能是一个经过简单变换(如Base64解码、反转)的字符串,甚至是从另一个接口获取的动态值。耐心地在JS代码中搜索

    key

    ,

    secret

    ,

    salt

    ,

    appSecret

    等关键词,并结合断点查看运行时的值,是找到它的关键。

    4. 核心环节:登录态维持与Cookie处理

    上面代码中有一个关键注释:

    Cookie

    需要从登录后的浏览器中获取。对于需要登录才能访问的群组,如何让Python脚本“保持登录状态”是必须解决的问题。

    4.1 手动获取Cookie(简单场景)

    对于一次性或低频采集,最简单的方法是手动复制:

  • 用浏览器正常登录目标网站。
  • 打开DevTools的

    Application

    面板(或

    Storage

    )。

  • Cookies

    下找到目标网站的域名,右键点击某个Cookie(通常是包含

    session

    ,

    token

    ,

    login

    等字样的),选择

    Copy Value

  • 将复制的一长串字符串粘贴到代码的

    headers['Cookie']

    中。

  • 缺点

    : Cookie会过期(Session Cookie在浏览器关闭后失效,持久Cookie也有有效期)。过期后需要重新手动获取。

    4.2 模拟登录(自动化方案)

    要实现全自动化,就必须用Python模拟登录过程,从登录接口获取有效的Cookie或Token。

  • 分析登录请求

    : 在Network面板,记录你手动输入用户名密码点击登录时发出的请求。它通常是一个

    POST

    请求,表单数据里包含用户名、密码(很可能是加密的),以及一些防CSRF的Token。

  • 逆向密码加密

    : 找到密码在提交前被哪个JS函数加密了。方法和逆向签名一样:在登录页面的JS中搜索

    password

    ,

    encrypt

    ,

    md5

    ,

    rsa

    等关键词,打上断点,查看明文密码如何变成密文。

  • Python复现登录

    : 用

    requests.Session()

    对象来保持会话。先访问登录页,获取必要的初始Token(如

    csrf_token

    ),然后模拟加密过程生成密码密文,最后向登录接口提交数据。

  • 保存会话

    : 登录成功后,

    session

    对象会自动管理Cookies。后续访问群成员接口,直接使用这个

    session

    去请求即可。

  • import requests
    from lxml import html # 用于解析HTML获取csrf token

    def login(username, password):
    session = requests.Session()
    login_page_url = "https://example.com/login"
    login_api_url = "https://example.com/api/login"

    # 1. 获取登录页,提取csrf token (假设它在一个name为_csrf的input里)
    resp = session.get(login_page_url)
    tree = html.fromstring(resp.content)
    csrf_token = tree.xpath('//input[@name="_csrf"]/@value')[0]

    # 2. 模拟JS加密密码 (这里假设是简单的MD5,实际需要根据逆向结果修改)
    # 假设逆向发现密码是: md5(md5(明文密码) + csrf_token)
    import hashlib
    first_md5 = hashlib.md5(password.encode()).hexdigest()
    encrypted_pwd = hashlib.md5((first_md5 + csrf_token).encode()).hexdigest()

    # 3. 构造登录数据
    login_data = {
    "username": username,
    "password": encrypted_pwd, # 提交加密后的密码
    "_csrf": csrf_token
    }

    # 4. 发送登录请求
    login_resp = session.post(login_api_url, data=login_data)
    if login_resp.status_code == 200 and login_resp.json().get('success'):
    print("登录成功!")
    return session # 返回携带登录态Cookie的session对象
    else:
    print("登录失败")
    return None

    # 使用
    my_session = login("your_username", "your_password")
    if my_session:
    # 用这个session去请求需要登录的接口
    # member_list_resp = my_session.get(member_api_url, params=…)

    注意事项

    : 模拟登录是逆向中最复杂的环节之一。网站可能采用更复杂的加密(如RSA非对称加密)、图形验证码、滑块验证等。遇到验证码时,可能需要引入图像识别库(如

    ddddocr

    用于简单验证码)或考虑使用付费打码平台。对于极其复杂的登录,有时“手动获取长期有效的Token”并定期更换,在成本效益上更划算。

    5. 数据采集的工程化与优化

    当核心接口打通后,我们需要考虑如何稳定、高效、友好地采集数据。

    5.1 请求头伪装与反爬策略

    现代网站都有基础的反爬机制。除了必不可少的

    Cookie

    ,以下请求头也建议加上,让你的请求更像一个真实的浏览器:

    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",
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    "Accept-Encoding": "gzip, deflate, br", # 注意requests会自动解压gzip/deflate,但不要写'br'除非你处理brotli
    "Referer": "https://目标群组页面的URL",
    "Sec-Ch-Ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"', # Chrome的UA客户端提示,动态变化
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": '"Windows"',
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin",
    "X-Requested-With": "XMLHttpRequest", # 表明是Ajax请求
    }

    使用

    requests

    时,

    Accept-Encoding

    最好不要包含

    br

    (Brotli),除非你额外安装

    brotli

    库并处理。

    Sec-*

    系列头是较新的浏览器特征,加上能提升伪装度,但并非所有网站都校验。

    5.2 分页与速率控制

    群成员数据通常是分页的。你需要循环请求,直到没有数据为止。

    def fetch_all_members(group_id, session, start_page=1):
    all_members = []
    page = start_page
    size = 50 # 每页数量,根据接口允许的最大值调整
    secret_key = "your_secret_key"

    while True:
    params = {
    "groupId": group_id,
    "page": page,
    "size": size,
    "_t": int(time.time() * 1000)
    }
    params['_sign'] = generate_sign(params, secret_key)

    resp = session.get(member_api_url, params=params, headers=headers)
    if resp.status_code != 200:
    print(f"第{page}页请求失败: {resp.status_code}")
    break

    data = resp.json()
    current_page_members = data.get('data', [])
    total = data.get('total', 0)

    if not current_page_members: # 当前页无数据,结束
    print(f"第{page}页无数据,采集结束。")
    break

    all_members.extend(current_page_members)
    print(f"已采集第{page}页,共{len(current_page_members)}条,累计{len(all_members)}条")

    # 简单判断是否还有下一页
    if len(current_page_members) < size or (total and len(all_members) >= total):
    break

    page += 1
    time.sleep(1.5) # 非常重要的延时,避免请求过快被封IP

    return all_members

    time.sleep()

    是简单有效的速率控制。对于更复杂的场景,可以考虑使用

    random.uniform(1, 3)

    增加随机性,或者使用更高级的限流队列。

    5.3 数据存储

    采集到的数据(通常是JSON列表)可以保存为多种格式:

    • JSON文件

      : 简单直接,

      json.dump(all_members, open('members.json', 'w', encoding='utf-8'), ensure_ascii=False)

    • CSV文件

      : 方便用Excel打开分析。使用

      pandas

      库非常便捷:

      pd.DataFrame(all_members).to_csv('members.csv', index=False, encoding='utf-8-sig')

    • 数据库(SQLite/MySQL)

      : 适合数据量大或需要复杂查询的场景。使用

      sqlite3

      (内置)或

      pymysql

      /

      sqlalchemy

      库。

    6. 常见问题与排查技巧实录

    在实际操作中,你几乎一定会遇到各种问题。下面是一些典型问题及解决思路:

    6.1 请求返回403/412错误

    这通常是签名错误、Cookie失效或请求头不完整导致的。

    • 排查签名

      : 用Python生成的签名和浏览器实际请求的签名对比。确保参数顺序、拼接方式、密钥、编码完全一致。

      特别注意

      :JS中的

      +

      号拼接字符串,如果值不是字符串类型,可能会发生隐式转换。在Python中要确保所有参数值都是字符串后再拼接,或者完全模拟JS的类型处理逻辑。

    • 排查Cookie

      : 检查Cookie是否已过期。重新登录获取新的Cookie。确认Cookie的域(Domain)和路径(Path)是否正确。

    • 排查请求头

      : 使用

      curlconverter

      将浏览器的请求直接转为Python代码,对比和你自己构造的请求头差异,逐个补全。

    6.2 数据接口找不到或参数不明

    • 尝试搜索

      : 在Network面板的搜索框(Filter)里,不要只搜

      member

      ,可以尝试搜接口返回数据中特有的字段名,比如用户名的字段

      nickName

      ,或者群ID

      123456

    • 查看Initator(发起者)

      : 在Network面板点击请求,查看

      Initiator

      标签页,它会显示是哪个JS文件发起了这个请求,点击可以跳转到Sources面板的对应行,这是定位关键JS代码的捷径。

    • 参数可能是全局变量

      : 有些参数(如

      _t

      )可能是在页面加载时生成并存储在全局变量

      window

      对象里,或者来自上一个接口的响应。需要仔细梳理前端代码的数据流。

    6.3 代码被混淆,难以阅读

    这是常态。面对混淆代码(变量名都是a,b,c,d):

  • 使用Pretty Print

    : 先格式化。

  • 搜索常量

    : 搜索接口URL字符串、固定的密钥字符串、加密算法常量(如

    0x67452301

    可能是MD5的初始向量)。

  • 关注函数调用

    : 即使变量名混淆,函数调用如

    Object.keys()

    ,

    encodeURIComponent()

    ,

    md5()

    ,

    CryptoJS.AES.encrypt()

    是清晰的,它们是理解逻辑的锚点。

  • 动态调试

    : 在疑似加密函数入口处打上断点,观察输入和输出,这是理解混淆代码最有效的方法。不要试图通读所有代码。

  • 6.4 遇到WebSocket或SSE传输数据

    有些现代应用使用WebSocket或Server-Sent Events (SSE) 进行实时数据传输。Network面板中会有

    WS

    EventStream

    类型的请求。

    • 对于WebSocket

      : 可以使用

      websockets

      库进行连接和通信。你需要分析建立WebSocket连接时的握手请求(一个带有

      Upgrade: websocket

      头部的HTTP请求),以及后续发送和接收的消息格式。

    • 对于SSE

      : 本质上是一个长连接的HTTP流,可以使用

      requests

      以流模式 (

      stream=True

      ) 读取,并解析

      data:

      开头的事件行。

    6.5 关于封IP与验证码

    如果请求频率过高,可能会触发IP限制或弹出验证码。

    • 降低频率

      : 增加

      time.sleep()

      的间隔,并加入随机延迟。

    • 使用代理IP池

      : 这是应对IP封锁的终极方案。可以使用一些代理服务,并在requests请求中通过

      proxies

      参数设置。

    • 处理简单验证码

      : 如果验证码是图片形式且比较简单,可以考虑使用OCR库(如

      pytesseract

      或更易用的

      ddddocr

      )进行识别。但这属于攻防对抗的范畴,需要谨慎评估法律和道德风险。

    • 根本性解决方案

      : 与网站所有者沟通,看是否有官方API可以提供数据。或者,你的采集行为是否在对方

      robots.txt

      协议允许范围内?始终将合规性放在首位。

    逆向分析是一个需要耐心和细致观察的过程。每一次成功的逆向,都是对前端逻辑和网络协议一次深入的理解。从定位接口到破解参数,从模拟登录到处理反爬,每一步的突破都建立在扎实的基础和有条理的排查之上。掌握这套方法,你就能从大多数动态网站中获取到你需要的宝贵数据。

    赞(0)
    未经允许不得转载:171主机测评 » Python动态网页数据采集实战:JS逆向破解与自动化爬虫实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址