首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >图片懒加载怎么破:爬去哪儿图集实战

图片懒加载怎么破:爬去哪儿图集实战

原创
作者头像
小白学大数据
发布2026-07-27 16:57:35
发布2026-07-27 16:57:35
1520
举报

在构建离线旅行攻略时,需要把去哪儿网游记中的图集批量抓取并整理为本地 Markdown 电子书。技术上的主要障碍并非反爬,而是图片懒加载:页面中数十张图片在初始 HTML 里仅以占位图形式存在,真实地址延迟到滚动进入视口时才加载。懒加载本身是标准的 Web 性能优化手段,但对只获取静态 HTML 的爬虫而言,拿到的永远是未赋值的占位标记。本文记录从原理分析到工程落地的完整方案,目标读者已具备 Python 与 requests 基础。懒加载的两种实现去哪儿图集主要采用两种实现形态。第一种是属性占位。图片标签的 src 指向占位图,真实地址存放在 data-original、data-src 或 data-lazy-src 等自定义属性中,滚动进入视口时由脚本将属性值赋给 src:

代码语言:txt
复制
<img class="lazy" src="https://source.qunar.com/placeholder.gif"
     data-original="https://travel.qunar.com/imgs/xxxxx.jpg"
     loading="lazy">

第二种是接口分页注入。真实地址并不出现在初始 HTML 中,而是由 IntersectionObserver 监听滚动事件,触发后调用接口拉取图片列表再写入 DOM:

代码语言:txt
复制
const io = new IntersectionObserver((entries) => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      const url = e.target.dataset.original;
      e.target.src = url;
    }
  });
});
document.querySelectorAll("img.lazy").forEach(el => io.observe(el));

判定方式:查看网页源代码(view-source),若 img 的 src 为占位图而存在 data-* 属性,属于第一种;若 Network 面板中图片请求随滚动分批出现且伴随 XHR 接口,属于第二种。判定结果直接决定后续技术路线。提取方案与权衡针对属性占位,直接用 BeautifulSoup 解析即可,按优先级回退读取 data-original、data-src、data-lazy-src、src:

代码语言:txt
复制
import requests
from bs4 import BeautifulSoup

url = "https://travel.qunar.com/youji/123456"
html = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}).text
soup = BeautifulSoup(html, "lxml")

imgs = soup.select("img.lazy")
for img in imgs:
    real = (img.get("data-original")
            or img.get("data-src")
            or img.get("data-lazy-src")
            or img.get("src"))
    if real and real.startswith("http"):
        print(real)

该方案零渲染开销,能覆盖约八成图集,应作为首选。针对接口注入,在 Network 面板筛选 Img 与 XHR,滚动页面捕获返回图片列表的 JSON 接口(通常带游记 id 与分页参数)。响应多为 [{"url":"...","width":...}, ...] 结构,按页累加直至无新数据:

代码语言:txt
复制
import requests

def fetch_album(session, youji_id):
    page, urls = 1, []
    while True:
        api = f"https://travel.qunar.com/api/album/{youji_id}?page={page}"
        data = session.get(api, timeout=10).json()
        items = data.get("list", [])
        if not items:
            break
        urls += [it["url"] for it in items if it.get("url")]
        page += 1
    return urls

直接请求接口比渲染整页更高效稳定。若接口含签名参数,可优先从页面中提取初始 token;无法绕过时再考虑渲染。渲染方案使用 Playwright 触发懒加载:

代码语言:txt
复制
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    b = p.chromium.launch(headless=True)
    page = b.new_page()
    page.goto(url)
    page.eval_on_selector_all(
        "img", "els => els.forEach(e => e.scrollIntoView())")
    page.wait_for_timeout(2000)
    html = page.content()
    b.close()

渲染能解决任意懒加载形态,但单页耗时数秒且占用较多内存,仅建议在接口不可达的少量页面使用,不适合大规模批处理。会话复用与请求头批量抓取应复用 TCP 连接并统一请求头,避免每次请求重建连接带来的开销与异常的指纹波动。使用 requests.Session 固定 User-Agent 与 Referer,并对瞬时错误配置自动重试与退避:

代码语言:txt
复制
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import requests

session = requests.Session()
retry = Retry(total=3, backoff_factor=0.5,
              status_forcelist=[403, 429, 500, 502, 503])
adapter = HTTPAdapter(max_retries=retry, pool_connections=10, pool_maxsize=10)
session.mount("http://", adapter)
session.mount("https://", adapter)
session.headers.update({
    "User-Agent": "Mozilla/5.0",
    "Referer": "https://travel.qunar.com",
})

Referer 用于应对去哪儿部分图片的防盗链校验,缺失时会返回 403。出口 IP 与代理架构批处理规模上升后,单 IP 高频请求会触发去哪儿的频率阈值,表现为 403 或图片被替换为验证内容。解决思路是引入可轮换的出口 IP。本文采用亿牛云隧道代理(官网 16yun.cn)。隧道代理属于动态转发代理:客户端只需配置一个固定入口域名,平台在后端自动完成出口 IP 的轮换与连接管理,对业务代码近似透明。相比短效代理需要自建存活检测与节点调度,或自行维护代理池,隧道代理将调度复杂性下沉到服务端,更适合一波式批量抓取。亿牛云提供短效、长效与隧道三类产品,其中隧道代理对批量场景最省运维成本。在请求中接入代理:

代码语言:txt
复制
# 亿牛云 隧道代理(动态转发)
# host、port、user、pass 以亿牛云控制台实际下发为准
proxy_host = "http-proxy-t3.16yun.cn"
proxy_port = "31028"
proxy_user = "你的订单号"
proxy_pass = "你的密码"

proxy = f"http://{proxy_user}:{proxy_pass}@{proxy_host}:{proxy_port}"
session.proxies.update({"http": proxy, "https": proxy})

resp = session.get(url, timeout=10)
print(resp.status_code)

同一 Session 可在提取与下载阶段复用,请求不会长期绑定单一出口 IP。平台控制台提供成功率与耗时监控,可据此调整并发。需注意代理并非规避限制的万能手段。应保留合理的请求间隔与超时重试,避免对代理通道与目标站点造成过大压力。工程化落地与目录约定完整流程为:配置代理与会话,请求页面并提取图片地址,下载至本地目录,以相对路径写入 Markdown,最后由 Pandoc 转为 PDF。建议按以下结构组织产物:

代码语言:txt
复制
guides/
  youji_123456/
    guide.md
    imgs/
      a1b2c3.jpg
  youji_789012/
    guide.md
    imgs/
  README.md

下载逻辑以 md5 作为文件名,天然实现同地址去重;跨游记去重可维护已下载 md5 集合,命中即跳过:

代码语言:txt
复制
import os, hashlib
def save_img(src, folder, session):
 name = hashlib.md5(src.encode()).hexdigest() + ".jpg"
 path = os.path.join(folder, "imgs", name)
 if not os.path.exists(path):
 r = session.get(src, timeout=10)
 os.makedirs(os.path.dirname(path), exist_ok=True)
 open(path, "wb").write(r.content)
 return name
with open(f"{folder}/guide.md", "w", encoding="utf-8") as f:
 f.write(f"# {title}\n\n")
 for src in img_list:
 name = save_img(src, folder, session)
 f.write(f"![图](./imgs/{name})\n")

断点续传依赖 os.path.exists 判断与地址清单持久化:将每篇地址列表存为 JSON 清单,重启后仅补抓缺失项。并发方面,建议将图片下载放入有界线程池(例如 8 线程),并配合固定间隔的请求节流,避免代理成功率与本地带宽同时劣化。单篇间隔可设为 1 秒左右,超时设为 3 秒并最多重试 3 次。成书阶段使用 pandoc guide.md -o guide.pdf --pdf-engine=xelatex 即可生成便携文档。吞吐与连接池调优实际吞吐受三重因素约束:代理通道的并发上限、目标站点的频率阈值、本地出口带宽。连接池大小应与线程数对齐,避免线程在等待空闲连接时空转;当代理成功率随并发上升而下滑时,应优先降并发而非加重试,否则会形成错误放大。经验上,在 8 线程、单任务间隔 1 秒的配置下,单日可稳定抓取数千张图,代理通道成功率保持在可接受区间。常见问题排查返回 403:依次核对 User-Agent、Referer、Cookie 是否齐全,确认是否触发了频率阈值,必要时降低并发或切换出口 IP。下载为空文件或小于 2 KB:多为占位图或防盗链拦截,检查 Referer 与代理可达性,并标记补抓。中文路径或文件名乱码:统一以 md5 命名图片、以 UTF-8 编码写入 Markdown,规避文件系统编码差异。接口签名变化:接口改版时旧逻辑会整批失效,应优先从页面动态提取参数,而非将签名写死。监控与日志规模化运行时应为每篇任务记录状态码、耗时与失败原因,对 403、超时、空文件分类统计。借助日志可快速区分是代理通道抖动还是目标接口变更,降低运维成本。小结懒加载的本质是真实地址被延迟赋值,定位到赋值来源即可破解。属性占位走解析,接口注入走接口,极端情况走渲染。出口 IP 问题通过亿牛云隧道代理以极低接入成本解决,会话复用与重试退避则保障批量任务的稳定性。由此构建的脚本可稳定产出离线攻略电子书,后续可扩展正文抽取与封面生成,进一步提升成书质量。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档