1. ESP32 Wi-Fi基础与B站粉丝数爬取实战

在嵌入式物联网开发中,Wi-Fi连接能力是ESP32最核心的价值之一。不同于传统MCU需要外挂Wi-Fi模块并处理复杂的AT指令协议栈,ESP32将Wi-Fi射频、基带、MAC层及TCP/IP协议栈深度集成于SoC内部,并原生支持FreeRTOS多任务调度。这意味着开发者无需关心底层驱动细节,可直接通过高层API完成网络初始化、连接管理、HTTP通信等完整链路操作。本实践以爬取Bilibili用户粉丝数为具体目标,系统性地拆解ESP32在Arduino框架下的Wi-Fi应用全流程:从物理层连接建立,到应用层HTTP请求构造与JSON响应解析,最终实现端侧数据采集闭环。所有代码均基于ESP-IDF 4.x兼容的Arduino-ESP32核心库(v2.0.9+),不依赖任何第三方非标库,确保工程可复现性与长期维护性。

1.1 网络环境初始化:Wi-Fi Station模式配置原理

ESP32的Wi-Fi模块工作于Station(STA)模式时,作为客户端接入已有的无线路由器。该模式下需明确配置两个关键参数:SSID(服务集标识符)与密码(PSK)。在Arduino框架中, WiFi.h 头文件封装了完整的Wi-Fi驱动抽象层,其初始化流程严格遵循IEEE 802.11标准状态机:

#include <WiFi.h>

const char* ssid = "YourRouterSSID";
const char* password = "YourRouterPassword";

void wifiConnect() {
    WiFi.mode(WIFI_STA);                    // 强制设置为Station模式,禁用AP功能
    WiFi.begin(ssid, password);             // 触发关联(Association)与认证(Authentication)流程
    Serial.print("Connecting to ");
    Serial.println(ssid);

    // 等待连接完成,超时机制防止无限阻塞
    int connectTimeout = 0;
    while (WiFi.status() != WL_CONNECTED && connectTimeout < 20) {
        delay(500);
        Serial.print(".");
        connectTimeout++;
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi Connected");
        Serial.print("IP Address: ");
        Serial.println(WiFi.localIP());     // 获取DHCP分配的IPv4地址
    } else {
        Serial.println("\nWiFi Connection Failed");
        // 此处应加入故障诊断逻辑,如检查信号强度、密码错误等
    }
}

关键参数解析
- WiFi.mode(WIFI_STA) :显式声明工作模式。ESP32默认启动时处于 WIFI_MODE_NULL ,必须主动切换至 WIFI_STA 才能启用Station功能。若省略此步,在部分固件版本中可能导致 WiFi.begin() 静默失败。
- WiFi.begin(ssid, password) :该函数内部执行完整的802.11连接序列:扫描信道→选择最佳AP→发送Probe Request→接收Probe Response→发起Authentication Request→完成Authentication→发起Association Request→接收Association Response→完成四次握手(WPA/WPA2)。整个过程由ESP-IDF底层Wi-Fi驱动自动管理,上层无需干预。
- WiFi.status() 返回值: WL_CONNECTED 表示已成功获取IP地址并进入运行态; WL_CONNECT_FAILED 表示认证或关联失败; WL_NO_SSID_AVAIL 表示未扫描到指定SSID; WL_DISCONNECTED 表示已断开连接。实际工程中应避免仅依赖 WL_CONNECTED 判断,需结合 WiFi.RSSI() 获取信号强度(单位dBm),低于-80dBm时连接稳定性显著下降。

时钟与电源约束 :Wi-Fi射频模块启动需稳定供电与精确时钟。ESP32的RF模块依赖内部PLL生成2.4GHz载波,该过程对VDD33电压波动敏感。实测表明,当USB供电不足(如使用劣质数据线)或外部LDO压差过小时, WiFi.begin() 可能长时间卡在 WL_NO_SSID_AVAIL 状态。建议在硬件设计阶段为RF模块单独配置低噪声LDO,并在软件中增加电源状态检测。

1.2 TCP/IP协议栈初始化:DHCP与DNS服务协同

Wi-Fi物理层连接成功仅是第一步,上层应用通信依赖完整的TCP/IP协议栈。ESP32的lwIP协议栈在Arduino框架下默认启用以下关键服务:

服务 默认状态 工程意义
DHCP Client 启用 自动从路由器获取IP、子网掩码、网关、DNS服务器地址,避免静态IP冲突风险
DNS Resolver 启用 将域名(如 api.bilibili.com )解析为IPv4地址,是HTTP通信的前提
ARP Cache 启用 缓存局域网内IP-MAC映射关系,减少广播包数量,提升LAN通信效率

WiFi.localIP() 返回有效地址(如 192.168.123.87 )时,表明DHCP流程已完成,且lwIP已将网关MAC地址写入ARP缓存。此时可立即发起DNS查询。但需注意:DNS解析存在异步特性, WiFi.hostByName() 调用后需等待返回结果,而非立即使用。典型错误写法:

// ❌ 错误:未检查DNS解析结果即使用
IPAddress serverIP;
WiFi.hostByName("api.bilibili.com", serverIP); // 此调用非阻塞,serverIP可能仍为0.0.0.0
client.connect(serverIP, 443); // 连接必然失败

正确做法是加入超时等待循环:

IPAddress serverIP;
int dnsTimeout = 0;
while (WiFi.hostByName("api.bilibili.com", serverIP) == 0 && dnsTimeout < 10) {
    delay(1000);
    dnsTimeout++;
}
if (serverIP == INADDR_NONE) {
    Serial.println("DNS Resolution Failed");
    return;
}

MTU与缓冲区配置 :ESP32 lwIP默认MTU为1500字节,但实际Wi-Fi帧包含MAC头、LLC/SNAP头等额外开销,有效载荷约1460字节。HTTP请求头通常小于512字节,但B站API响应JSON可能超过2KB。若未调整 TCP_MSS (最大分段大小)与接收缓冲区,大响应体将被截断。Arduino-ESP32核心库中可通过 #define LWIP_TCP_MSS 536 platformio.ini 中预编译配置,或在 setup() 中动态调用 tcp_mss() 函数。

1.3 HTTP客户端实现:HTTPS通信的安全考量

Bilibili API强制要求HTTPS访问,这引入了TLS/SSL加密层。ESP32内置硬件加速引擎(RSA、AES、SHA)可卸载大部分加解密运算,但Arduino框架的 HTTPClient.h 对此做了高度封装:

#include <HTTPClient.h>
#include <ArduinoJson.h>

String fetchBilibiliFans(const String& uid) {
    HTTPClient http;
    String url = "https://api.bilibili.com/x/relation/stat?vmid=" + uid;

    // 配置HTTPS客户端
    http.begin(url);
    http.setConnectTimeout(5000);    // 连接超时5秒
    http.setTimeout(10000);          // 整体请求超时10秒
    http.setUserAgent("ESP32-Bilibili-Client/1.0"); // 设置User-Agent,部分API需此头

    int httpResponseCode = http.GET();
    if (httpResponseCode > 0) {
        String payload = http.getString(); // 获取完整响应体
        http.end(); // 必须调用,释放TCP连接与内存
        return payload;
    } else {
        http.end();
        return "";
    }
}

安全证书验证陷阱 http.begin(url) 在HTTPS场景下默认启用证书校验(Certificate Verification)。若目标服务器证书由公共CA签发(如Let’s Encrypt),验证通过;但若使用自签名证书或内网测试环境,将返回 HTTP_CODE_UNAUTHORIZED (-1)或 HTTP_CODE_BAD_REQUEST (-2)。生产环境必须保持校验开启,而开发调试时可临时禁用:

// ⚠️ 仅限开发环境!生产环境严禁禁用证书校验
http.setInsecure(); // 跳过SSL证书验证
// 或更安全的方式:加载特定根证书
// http.addHeader("Accept", "application/json");
// http.useHTTP10(true); // 强制HTTP/1.0,避免某些老旧服务器的HTTP/1.1兼容问题

连接复用优化 :HTTP/1.1默认启用 Connection: keep-alive ,但Arduino-ESP32的 HTTPClient 每次调用 http.end() 会关闭TCP连接。频繁请求时应重用 HTTPClient 实例并手动管理连接,避免三次握手与慢启动开销。对于B站粉丝数这种低频查询(分钟级),当前实现已足够。

2. Bilibili API接口分析与数据结构建模

Bilibili开放平台提供RESTful风格API,其粉丝统计接口 /x/relation/stat 属于公开免鉴权接口,但需严格遵循请求规范。理解其数据契约是JSON解析正确的前提。

2.1 接口协议规范与请求构造

接口URL结构为: https://api.bilibili.com/x/relation/stat?vmid={uid}
- HTTP方法 :GET
- 路径参数 vmid 为Bilibili用户唯一ID(UID),非用户名。例如UID 12345678 对应URL https://api.bilibili.com/x/relation/stat?vmid=12345678
- 必需请求头 User-Agent (模拟浏览器行为,规避反爬); Referer (部分接口需此头,B站此处非必需但建议添加)
- 响应格式 application/json; charset=utf-8

UID获取方法 :登录Bilibili网页端 → 进入个人主页 → 查看浏览器地址栏URL。例如 https://space.bilibili.com/12345678 中的 12345678 即为UID。切勿使用用户名(如 homepea ),因API仅接受数字UID。

响应体结构分析 (以UID 12345678 为例):

{
  "code": 0,
  "message": "0",
  "ttl": 1,
  "data": {
    "mid": 12345678,
    "following": 123,
    "follower": 45678,
    "whisper": 0,
    "black": 0,
    "silent": 0
  }
}

字段语义说明
- code : 响应状态码, 0 表示成功,非 0 值(如 -400 )表示错误,需查B站文档解码
- message : 状态描述,成功时为 "0" ,错误时为具体提示(如 "请求错误"
- ttl : Time-To-Live(秒),指示客户端可缓存该响应的时间,此处为 1 秒,表明数据实时性极高
- data : 核心数据对象,包含:
- mid : 用户UID,与请求参数一致,用于校验
- following : 关注数
- follower : 粉丝数(本次目标字段)
- whisper : 私密关注数
- black : 黑名单数
- silent : 悄悄关注数

关键设计原则 :该接口无速率限制(Rate Limiting),但高频请求(>1次/秒)可能触发风控。生产环境应加入指数退避(Exponential Backoff)机制。

2.2 JSON解析:ArduinoJson库的内存安全实践

Arduino平台资源受限,JSON解析必须规避动态内存分配陷阱。ArduinoJson 6.x采用静态内存模型,需预先计算所需缓冲区大小。根据B站API响应结构, data 对象含6个键值对,最长字符串( message )约10字符,整数最大位数( follower )约8位,估算总内存需求:

// 计算JSON缓冲区大小:对象开销 + 字符串长度 + 整数存储
// ArduinoJson Assistant工具推荐:256字节足够解析此响应
const size_t JSON_BUFFER_SIZE = 256;
DynamicJsonDocument doc(JSON_BUFFER_SIZE);

解析代码实现

String parseFansCount(const String& jsonPayload) {
    DynamicJsonDocument doc(JSON_BUFFER_SIZE);
    DeserializationError error = deserializeJson(doc, jsonPayload);

    if (error) {
        Serial.print("JSON Parse Error: ");
        Serial.println(error.c_str());
        return "ParseError";
    }

    // 逐层校验JSON结构,避免访问空指针
    if (doc["code"] != 0) {
        Serial.print("API Error Code: ");
        Serial.println(doc["code"].as<int>());
        return "APIError";
    }

    JsonObject dataObj = doc["data"];
    if (!dataObj.containsKey("follower")) {
        Serial.println("Missing 'follower' field in response");
        return "NoFollowerField";
    }

    long followerCount = dataObj["follower"]; // 自动类型转换
    return String(followerCount);
}

// 使用示例
String fans = parseFansCount(payload);
Serial.print("Current Fans: ");
Serial.println(fans);

内存安全要点
- DynamicJsonDocument 在栈上分配固定内存, deserializeJson() 不会调用 malloc() ,避免堆碎片
- doc["data"] 返回 JsonObject 引用,非深拷贝,零内存开销
- dataObj["follower"] 返回 JsonVariant as<long>() 进行安全类型转换,若字段不存在则返回默认值(0)
- 严禁 使用 String 拼接JSON路径(如 doc["data"]["follower"] ),因 JsonObject 不支持链式索引,必须分步获取

调试技巧 :当解析失败时,先打印原始 jsonPayload 确认HTTP响应完整性,再用 ArduinoJson Assistant 在线工具粘贴响应体,自动生成精确缓冲区大小与解析代码。

3. 完整工程实现与鲁棒性增强

将前述模块整合为可运行的ESP32工程,需解决时序协调、错误恢复、资源管理等实际问题。

3.1 主程序架构:状态机驱动的周期性任务

loop() 函数不应包含阻塞操作,需采用非阻塞状态机设计。以下为推荐架构:

enum SystemState {
    STATE_WIFI_INIT,
    STATE_WIFI_WAIT,
    STATE_HTTP_FETCH,
    STATE_PARSE_JSON,
    STATE_DISPLAY_RESULT,
    STATE_ERROR_RECOVER
};

SystemState currentState = STATE_WIFI_INIT;
unsigned long lastWifiCheck = 0;
unsigned long lastFetchTime = 0;
const unsigned long FETCH_INTERVAL_MS = 60000; // 每分钟更新一次

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("ESP32 Bilibili Fans Monitor Starting...");
}

void loop() {
    switch (currentState) {
        case STATE_WIFI_INIT:
            wifiConnect();
            currentState = STATE_WIFI_WAIT;
            break;

        case STATE_WIFI_WAIT:
            if (WiFi.status() == WL_CONNECTED) {
                Serial.println("WiFi Ready. Starting fetch cycle.");
                lastFetchTime = millis();
                currentState = STATE_HTTP_FETCH;
            } else if (millis() - lastWifiCheck > 5000) {
                Serial.print("Still connecting... RSSI: ");
                Serial.println(WiFi.RSSI());
                lastWifiCheck = millis();
            }
            break;

        case STATE_HTTP_FETCH:
            if (millis() - lastFetchTime >= FETCH_INTERVAL_MS) {
                String payload = fetchBilibiliFans("12345678"); // 替换为你的UID
                if (payload.length() > 0) {
                    currentState = STATE_PARSE_JSON;
                    // 将payload传递给解析函数(通过全局变量或队列)
                    lastFetchTime = millis();
                } else {
                    currentState = STATE_ERROR_RECOVER;
                }
            }
            break;

        case STATE_PARSE_JSON:
            String fans = parseFansCount(payload);
            Serial.print("Follower Count: ");
            Serial.println(fans);
            currentState = STATE_DISPLAY_RESULT;
            break;

        case STATE_DISPLAY_RESULT:
            // 此处可驱动OLED/LCD显示粉丝数,或通过串口发送至PC
            delay(1000); // 简单延时,实际应替换为状态保持
            currentState = STATE_HTTP_FETCH;
            break;

        case STATE_ERROR_RECOVER:
            Serial.println("Error occurred. Resetting system in 10s...");
            delay(10000);
            ESP.restart(); // 硬件复位,确保状态清零
            break;
    }
}

状态机优势
- 避免 delay() 阻塞导致Wi-Fi心跳丢失(ESP32需定期处理Wi-Fi事件)
- 明确各阶段职责,便于插入日志、监控、告警逻辑
- 错误状态( STATE_ERROR_RECOVER )强制复位,防止状态腐化

3.2 关键鲁棒性增强措施

3.2.1 Wi-Fi连接韧性提升
  • 自动重连机制 :监听 WiFi.onEvent() 事件,捕获 SYSTEM_EVENT_STA_DISCONNECTED 后自动调用 WiFi.begin()
  • 信号质量监控 :当 WiFi.RSSI() 持续低于-70dBm时,触发信道扫描并尝试切换至更强信号AP(需路由器支持多SSID)
  • 内存泄漏防护 :每次 HTTPClient::end() 后,调用 WiFiClient::flush() 确保TCP缓冲区清空
3.2.2 HTTP通信容错
  • 重试策略 http.GET() 失败时,最多重试3次,每次间隔呈指数增长(1s, 2s, 4s)
  • 超时分级 :连接超时(5s)< 请求超时(10s)< 整体周期超时(60s),形成防御纵深
  • 响应校验 :除 code 字段外,校验 data.mid 是否匹配请求UID,防止API返回缓存脏数据
3.2.3 JSON解析防御式编程
  • 字段存在性检查 :使用 doc.containsKey("code") && doc.containsKey("data") 双校验
  • 类型安全访问 doc["data"]["follower"].is<long>() 先判断类型,再 as<long>() 转换
  • 边界值处理 :粉丝数为 long 类型,但API可能返回负数(异常情况),解析后需 max(0, followerCount)

3.3 硬件与调试接口集成

为便于现场调试,建议在工程中预留硬件交互接口:

// 硬件定义
#define LED_PIN 2   // 板载LED,连接WiFi时闪烁,获取数据时长亮
#define BUTTON_PIN 0 // GPIO0按钮,短按触发手动刷新,长按(3s)进入AP配网模式

void setupHardware() {
    pinMode(LED_PIN, OUTPUT);
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    digitalWrite(LED_PIN, HIGH); // 初始熄灭(共阳极)
}

// 在loop()中添加按钮扫描
void checkButton() {
    static unsigned long buttonPressStart = 0;
    static bool buttonPressed = false;

    if (digitalRead(BUTTON_PIN) == LOW) {
        if (!buttonPressed) {
            buttonPressStart = millis();
            buttonPressed = true;
        }
        if (millis() - buttonPressStart > 3000) {
            // 长按进入配网模式
            startSmartConfig();
        }
    } else {
        if (buttonPressed && (millis() - buttonPressStart < 1000)) {
            // 短按手动刷新
            lastFetchTime = 0;
        }
        buttonPressed = false;
    }
}

调试技巧 :使用 Serial.setDebugOutput(true) 启用ESP-IDF底层日志,可看到Wi-Fi连接详细状态(如 wifi: state: init -> auth )、TLS握手过程( ssl: SSL connection established ),这对诊断连接失败至关重要。

4. 生产环境部署注意事项

将实验室原型转化为可靠产品,需关注以下工程实践:

4.1 电源管理与功耗优化

  • Wi-Fi模块功耗 :ESP32 STA模式平均电流约70mA,峰值(传输时)达240mA。若使用电池供电,必须启用Modem Sleep模式: WiFi.setSleep(true) ,可降低待机电流至20mA。
  • 深度睡眠唤醒 :若只需每小时更新一次,可配置RTC定时器唤醒: esp_sleep_enable_timer_wakeup(3600 * 1000000) ,唤醒后执行完整流程,完成后再次进入深度睡眠,续航可达数月。

4.2 固件升级与配置管理

  • OTA升级 :利用 ArduinoOTA 库实现无线固件更新,避免物理接触。需在 setup() 中初始化:
    cpp ArduinoOTA.setHostname("bilibili-monitor"); ArduinoOTA.setPassword("admin123"); // 强制设置密码 ArduinoOTA.begin();
  • 配置持久化 :SSID/Password/UID等敏感信息不应硬编码。使用 Preferences 库存储于Flash:
    cpp Preferences prefs; prefs.begin("wifi", false); String savedSSID = prefs.getString("ssid", ""); prefs.end();

4.3 安全合规性

  • 凭证安全 :绝不将Wi-Fi密码以明文形式存储于代码或Flash。生产固件应通过安全启动(Secure Boot)与Flash加密(Flash Encryption)保护。
  • API调用合规 :遵守Bilibili《开放平台开发者协议》,禁止高频刷量、数据爬取用于商业目的。应在设备端添加 X-Device-ID 请求头标识唯一设备。

我在实际项目中曾遇到一个典型问题:某批次ESP32-WROVER模块在连接特定品牌路由器时, WiFi.status() 始终返回 WL_NO_SSID_AVAIL 。抓包发现路由器禁用了2.4GHz信标帧中的 Extended Capabilities 元素,导致ESP32驱动无法完成信道切换。解决方案是在 wifiConnect() 前添加:

esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N);

强制兼容旧协议。这类硬件兼容性问题只能通过实测积累经验,没有银弹。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐