使用 chromedp 高效爬取 Bing 搜索结果

在数据采集领域,搜索引擎结果是重要的信息来源。但传统爬虫面对现代浏览器渲染的页面时,常因 JavaScript 动态加载、跳转链接加密等问题束手无策。本文将详细介绍如何使用 Go 语言的chromedp库,模拟真实浏览器行为爬取 Bing 搜索结果,并破解其跳转链接加密,最终获取真实目标地址。

一、需求背景与技术选型

1.1 爬取搜索引擎结果的痛点

在尝试获取 Bing 搜索结果时,我们会遇到两个核心问题:

  • 动态渲染障碍 :Bing 搜索结果页通过 JavaScript 动态加载内容,传统基于http.Client的爬虫无法获取完整 DOM 结构
  • 跳转链接加密 :搜索结果中的链接并非真实地址,而是经过 Base64 编码的 Bing 跳转链接(如https://www.bing.com/ck/a?!...&u=...

1.2 为何选择 chromedp?

chromedp是一个基于 Chrome DevTools Protocol(CDP)的 Go 语言库,相比其他方案有明显优势:

  • 真实浏览器环境:直接控制 Chrome/Chromium 浏览器,完美处理 JS 动态渲染
  • 无需额外依赖:无需安装 Selenium 或 ChromeDriver,简化部署流程
  • 强类型 API:基于 Go 语言的类型安全特性,减少运行时错误
  • 灵活的上下文控制:支持页面导航、元素等待、JS 执行等完整浏览器操作

二、环境准备

2.1 基础依赖

  • Go 1.18+(推荐使用最新稳定版)

  • Chrome/Chromium 浏览器(确保版本与 chromedp 兼容)

  • 依赖库安装:

    go get github.com/chromedp/chromedp

2.2 核心配置说明

在代码初始化阶段,我们需要配置浏览器运行参数:

复制代码
opts := append(chromedp.DefaultExecAllocatorOptions[:],
    chromedp.Flag("ignore-certificate-errors", true), // 忽略证书错误
    chromedp.Flag("headless", true),                  // 无头模式(生产环境推荐)
)
  • 无头模式(headless) :不显示浏览器窗口,适合服务器环境运行,设为false可用于调试
  • 证书错误忽略:避免因 HTTPS 证书问题导致的爬取失败

三、核心功能实现

3.1 破解 Bing 跳转链接:unwrapBingURL 函数

Bing 搜索结果中的链接格式通常为:
https://www.bing.com/ck/a?!...&u=a1aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbQ==

其中u参数即为加密后的真实地址,解密步骤如下:

  1. 解析 URL 参数 :提取u参数值
  2. 去除前缀标识 :Bing 会在 Base64 字符串前添加a1前缀,需先移除
  3. Base64 URL 解码:使用 URL 安全的 Base64 解码算法还原真实地址

实现代码:

复制代码
func unwrapBingURL(bing string) (real string, err error) {
    u, err := url.Parse(bing)
    if err != nil {
        return "", err
    }
    // 提取u参数(加密的真实地址)
    enc := u.Query().Get("u")
    if enc == "" {
        return bing, nil // 非跳转链接,直接返回
    }
    // 移除Bing添加的a1前缀
    if strings.HasPrefix(enc, "a1") {
        enc = enc[2:]
    }
    // Base64 URL解码
    dst := make([]byte, base64.URLEncoding.DecodedLen(len(enc)))
    n, err := base64.URLEncoding.Decode(dst, []byte(enc))
    if err != nil {
        return "", err
    }
    return string(dst[:n]), nil
}

3.2 分页爬取与去重机制

为获取更多搜索结果并避免重复,我们设计了分页爬取与去重逻辑:

3.2.1 分页控制

Bing 通过first参数控制分页(first=1为第 1 页,first=11为第 2 页,以此类推),实现代码:

复制代码
pageSize := 10 // 每页预期结果数
for pageIndex := 0; len(unique) < maxResults; pageIndex++ {
    start := pageIndex*pageSize + 1
    searchURL := fmt.Sprintf("https://www.bing.com/search?q=%s&first=%d",
        url.QueryEscape(keyword), start)
    // 爬取当前页...
}
3.2.2 结果去重

使用map存储已获取的真实链接,确保最终结果唯一:

复制代码
seen := make(map[string]bool)   // 记录已发现的链接
var unique []string             // 存储去重后的真实链接

// 去重逻辑
if !seen[real] {
    seen[real] = true
    unique = append(unique, real)
    newCount++
}

3.3 页面元素提取

通过chromedp.Evaluate执行 JavaScript 代码,提取搜索结果中的链接:

复制代码
var rawLinks []string
if err := chromedp.Run(ctx,
    chromedp.Navigate(searchURL),                  // 导航到搜索页面
    chromedp.WaitVisible(`#b_content`, chromedp.ByID), // 等待结果区域加载完成
    chromedp.Sleep(2*time.Second),                 // 额外等待,确保JS渲染完成
    // 提取h2标题下的链接
    chromedp.Evaluate(`Array.from(document.querySelectorAll('#b_content h2 a'))
        .map(a => a.href)`, &rawLinks),
); err != nil {
    log.Printf("第 %d 页加载失败: %v", pageIndex+1, err)
    break
}
  • WaitVisible :等待结果容器#b_content可见,避免过早提取导致数据缺失
  • Sleep 延迟:应对 Bing 的动态加载机制,确保结果完全渲染
  • JS 选择器 :通过#b_content h2 a精准定位搜索结果链接

四、完整代码解析

4.1 代码结构总览

整个程序分为三个核心部分:

  1. unwrapBingURL:解密 Bing 跳转链接
  2. main 函数初始化:配置 chromedp 上下文、初始化去重容器
  3. 分页爬取循环:控制分页、提取链接、去重存储

4.2 关键细节说明

  • 上下文管理 :使用defer cancel()确保资源释放,避免内存泄漏

    allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
    defer cancel()

    ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()

  • 异常处理

    • 捕获页面加载错误,避免程序崩溃
    • 过滤无效链接(空链接、Microsoft 官方链接)
    • 处理 Base64 解码失败的情况
  • 终止条件

    • 已获取足够数量的结果(达到maxResults
    • 当前页无新结果(newCount == 0),说明已爬取所有结果

五、完整代码

Go 复制代码
package main

import (
	"context"
	"encoding/base64"
	"fmt"
	"log"
	"net/url"
	"strings"
	"time"

	"github.com/chromedp/chromedp"
)

// 从 Bing 跳转链中提取真实地址
func unwrapBingURL(bing string) (real string, err error) {
	u, err := url.Parse(bing)
	if err != nil {
		return "", err
	}
	// 取 u= 参数
	enc := u.Query().Get("u")
	if enc == "" {
		return bing, nil // 不是跳转链,原样返回
	}
	// 去掉前缀
	if strings.HasPrefix(enc, "a1") {
		enc = enc[2:]
	}
	// base64 解码
	dst := make([]byte, base64.URLEncoding.DecodedLen(len(enc)))
	n, err := base64.URLEncoding.Decode(dst, []byte(enc))
	if err != nil {
		return "", err
	}
	return string(dst[:n]), nil
}

func main() {
	keyword := "印度大幅下调消费税应对经济压力"
	maxResults := 100 // 你想拿多少条

	opts := append(chromedp.DefaultExecAllocatorOptions[:],
		chromedp.Flag("ignore-certificate-errors", true),
		chromedp.Flag("headless", true), // 调试可改 false
	)

	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
	defer cancel()

	ctx, cancel := chromedp.NewContext(allocCtx)
	defer cancel()

	seen := make(map[string]bool)
	var unique []string

	pageSize := 10
	for pageIndex := 0; len(unique) < maxResults; pageIndex++ {
		start := pageIndex*pageSize + 1
		searchURL := fmt.Sprintf("https://www.bing.com/search?q=%s&first=%d",
			url.QueryEscape(keyword), start)

		var rawLinks []string
		if err := chromedp.Run(ctx,
			chromedp.Navigate(searchURL),
			chromedp.WaitVisible(`#b_content`, chromedp.ByID),
			chromedp.Sleep(2*time.Second),
			chromedp.Evaluate(`Array.from(document.querySelectorAll('#b_content h2 a'))
				.map(a => a.href)`, &rawLinks),
		); err != nil {
			log.Printf("第 %d 页加载失败: %v", pageIndex+1, err)
			break
		}

		newCount := 0
		for _, l := range rawLinks {
			if l == "" || strings.Contains(l, "go.microsoft.com") {
				continue
			}
			real, err := unwrapBingURL(l)
			if err != nil || real == "" {
				continue
			}
			if !seen[real] {
				seen[real] = true
				unique = append(unique, real)
				newCount++
			}
			if len(unique) >= maxResults {
				break
			}
		}
		if newCount == 0 { // 没新结果就停
			break
		}
	}

	fmt.Printf("共拿到 %d 条真实链接:\n", len(unique))
	for i, u := range unique {
		fmt.Printf("%2d. %s\n", i+1, u)
	}
}

六、优化建议与注意事项

6.1 性能优化

  1. 调整 Sleep 时间:2 秒等待可能过长,可根据网络情况调整为 1-1.5 秒
  2. 并发爬取 :在合规前提下,可使用chromedp的多上下文特性实现并发爬取
  3. 结果缓存:将已爬取的链接存储到本地文件,避免重复爬取

6.2 反爬应对

  1. 添加随机延迟:在分页请求之间添加随机延迟(1-3 秒),模拟人类操作

  2. 设置 User-Agent:在 chromedp 选项中添加真实的 User-Agent,避免被识别为爬虫

    chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36")

  3. IP 轮换:若爬取量大,建议使用代理 IP 轮换,避免 IP 被封禁

6.3 合规性提醒

  • 遵守 Robots 协议 :查看 Bing 的/robots.txt文件,了解爬取限制
  • 控制爬取频率:避免给服务器造成过大压力
  • 尊重版权:爬取的结果仅用于合法用途,不得侵犯他人权益
相关推荐
猿究院-陆昱泽17 小时前
Redis 五大核心数据结构知识点梳理
redis·后端·中间件
yuriy.wang17 小时前
Spring IOC源码篇五 核心方法obtainFreshBeanFactory.doLoadBeanDefinitions
java·后端·spring
咖啡教室19 小时前
程序员应该掌握的网络命令telnet、ping和curl
运维·后端
你的人类朋友20 小时前
Let‘s Encrypt 免费获取 SSL、TLS 证书的原理
后端
老葱头蒸鸡20 小时前
(14)ASP.NET Core2.2 中的日志记录
后端·asp.net
李昊哲小课20 小时前
Spring Boot 基础教程
java·大数据·spring boot·后端
码事漫谈21 小时前
C++内存越界的幽灵:为什么代码运行正常,free时却崩溃了?
后端
Swift社区21 小时前
Spring Boot 3.x + Security + OpenFeign:如何避免内部服务调用被重复拦截?
java·spring boot·后端
90后的晨仔21 小时前
Mac 上配置多个 Gitee 账号的完整教程
前端·后端
码事漫谈21 小时前
AI智能体平台选型指南:从技术架构到商业落地的全景洞察
后端