Python 爬虫
「先观察页面规律,再写选择器」的思路
Web 爬虫的本质,是针对网络中对外公开呈现的内容,梳理其 HTTP 请求链路、DOM 结构与数据展示规律,进而按规则解析并提取目标字段的技术手段。
场景一:信息流分析
- 以小红书首页为例,内容以列表形式渲染,每个卡片对应一条笔记。
- 通过爬虫批量提取标题(
title)、封面图 URL(cover image)等字段,可进一步做统计分析、特征工程,或反推平台推荐逻辑与用户画像。
场景二:无 API 的数据聚合
- 部分站点 UI 体验欠佳,但数据具有不可替代性,且未开放公开 API,无法通过正规接口拉取。
- 此时可在合规前提下,抓取前端页面中结构稳定、规律清晰的数据,落盘至本地(文件、SQLite 等)。
- 后续可接入自研 Web 项目:Python + Jinja2 服务端渲染,或 Spring Boot 封装 RESTful API 再对接 Vue/React 前端——技术栈可按需选型。
- 配合自定义展示层,能显著提升数据浏览与分析效率。
下文讨论的,正是这类场景下的爬虫实现思路。

为什么用 Python
- 爬虫程序通过自动化方式模拟客户端访问 Web / 移动端资源,再按预设规则提取目标内容。更前沿的方案会结合深度学习做自适应解析,本文暂不展开。
- 从技术实现看,Java、JavaScript、Go 等语言均具备 HTTP 客户端与 HTML 解析能力。
- 实际差异在于生态:Python 拥有 Beautiful Soup、Scrapy、aiohttp 等成熟库,封装了请求、解析、管道等常见环节,开发成本较低。
- 语法层面同样占优:同等功能,Java 往往需要更多样板代码;而 Python 调用第三方库通常数行即可。爬虫任务多为脚本级需求,无需庞大服务集群,一段 50 行以内的代码往往就能跑通「请求 → 解析 → 落盘」全流程。
- 综合生态与效率,Python 是业界爬虫开发的主流选择之一。
- 个人技术栈方面,Python 是我的主力脚本语言(详见 Profile),因此本文示例均以 Python 实现。
Python 爬虫经常使用的几个核心工具
| 工具 | 作用 | 适用场景 |
|---|---|---|
| requests | 发起 HTTP/HTTPS 请求 | 静态网页、REST 接口 |
| BeautifulSoup4 | 解析 HTML/XML,CSS 选择器提取节点 | 静态页面结构化解析 |
| lxml | 高性能 HTML/XML 解析后端 | 复杂页面、大规模解析 |
| Scrapy | 爬虫框架(请求 / 解析 / 管道一体化) | 大规模、可扩展爬取 |
| aiohttp | 异步 HTTP 客户端 | 高并发 I/O 密集型任务 |
| Selenium/Playwright | 驱动真实浏览器 | JS 动态渲染、需登录交互的页面 |
基础流程:requests 发起 HTTP 请求 → 获取响应体 → BeautifulSoup 解析 → 提取字段 → 落盘。
请求阶段需注意服务端校验:缺少 Cookie、Content-Type、Authorization 等 Header 时,可能返回 4xx 错误。建议在 Chrome / Edge 中打开目标页,F12 → Network 面板查看成功请求的 Header,再在 Python 中复现。
响应体类型决定解析方式:
- JSON(RESTful API):
response.json()按字段路径提取;前端 SPA 与移动端(如 Retrofit2)通常走这条链路。 - HTML(服务端渲染页面):分析 DOM 结构,用 CSS 选择器定位重复节点,提取核心字段后持久化存储。
实际案例
- 尤雨溪曾回应框架之争时提到:Pornhub 的前端技术栈是 Vue。这引发了我对部分站点前端实现的关注。
- 顺着这条线索,我查阅了若干同类站点的页面结构与渲染方式。
- 结论是:不少站点前端实现较为简单,URL 规律与 DOM 结构清晰,适合用
requests+BeautifulSoup做自动化采集,再接入自定义展示层。
注意:实操此案例需能访问目标站点,网络环境可参考 路由器翻墙方案(Shadowsocks、V2RayNG、ClashX 等)。
目标站点:JavBus
- 警告⚠️:请勿在公开场合打开上述链接。
其 URL 构造规律如下:
link = "https://www.javbus.com/KBI-001"
# 规律如下
link = host_name + "/" + series + "-" + code-
即
域名 + "/" + 系列番号 + "-" + 编号,例如KBI-001。 -
封面图(Cover Image)对应 DOM 节点:
<div class="col-md-9 screencap">
<a class="bigImage" href="/pics/cover/6qah_b.jpg">
<img src="/pics/cover/6qah_b.jpg" title="KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ">
</a>
</div>- 样品图(Sample Images)对应
#sample-waterfall容器,子节点为a.sample-box:
<div id="sample-waterfall">
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-1.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_1.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 1">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-2.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_2.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 2">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-3.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_3.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 3">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-4.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_4.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 4">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-5.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_5.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 5">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-6.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_6.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 6">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-7.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_7.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 7">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-8.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_8.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 8">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-9.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_9.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 9">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-10.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_10.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 10">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-11.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_11.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 11">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-12.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_12.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 12">
</div>
</a>
<a class="sample-box" href="https://pics.dmm.co.jp/digital/video/118kbi00002/118kbi00002jp-13.jpg">
<div class="photo-frame">
<img src="/pics/sample/6qah_13.jpg" title="KBI-002 KANBi専属出演第1弾!旦那を忘れる程 汗だく汁だくで絡み合う 濃厚接吻性交3本番 舌を絡ませ抱き合いながら絶頂に達する密着性交! 織笠るみ - 樣品圖像 - 13">
</div>
</a>
</div>页面为静态 HTML,无需 Selenium/Playwright 等浏览器驱动,requests + BeautifulSoup 即可完成解析。
实现代码如下。
配置文件 series_image_cover_config.py
series = "KBI"
start_code = 1
end_code = 100主程序 series_image_cover_download.py(解析 + 异步下载 + 入口)
import os
import asyncio
import aiohttp
from bs4 import BeautifulSoup
import urllib3
from series_image_cover_config import *
# 禁用 InsecureRequestWarning 警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
host = "https://www.javbus.com"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
}
async def download_image_cover(session: aiohttp.ClientSession, name: str, series: str):
URL = f"{host}/{name}"
SAVE_DIR = f"./{series}"
os.makedirs(SAVE_DIR, exist_ok=True)
try:
async with session.get(URL, timeout=aiohttp.ClientTimeout(total=10), ssl=False) as response:
if response.status != 200:
print(f"[{name}] 页面不存在,跳过")
return
html = await response.text()
soup = BeautifulSoup(html, "html.parser")
title = soup.find("title")
cover_tag = soup.select_one("a.bigImage img")
if not cover_tag:
print(f"[{name}] 无封面,跳过")
return
cover_url = cover_tag["src"]
if cover_url.startswith("/"):
cover_url = host + cover_url
ext = os.path.splitext(cover_url)[1]
cover_path = os.path.join(SAVE_DIR, f"{name}{ext}")
async with session.get(
cover_url,
headers={"Referer": URL, **HEADERS},
ssl=False
) as img_response:
if img_response.status == 200:
with open(cover_path, "wb") as f:
f.write(await img_response.read())
print(f"[{name}] 下载成功 → {cover_path}")
print(f"[{name}] 标题:{title.text.strip() if title else '无标题'}")
else:
print(f"[{name}] 图片下载失败,状态码:{img_response.status}")
except asyncio.TimeoutError:
print(f"[{name}] 请求超时,跳过")
except Exception as e:
print(f"[{name}] 发生错误:{e}")
return
async def start_to_download():
names = [
f"{SERIES}-{code:03d}" for code in range(max(1, START_CODE), END_CODE)
]
connector = aiohttp.TCPConnector(limit=10)
async with aiohttp.ClientSession(
connector=connector,
headers=HEADERS
) as session:
tasks = [download_image_cover(session, name, SERIES) for name in names]
batch_size = 10
for i in range(0, len(tasks), batch_size):
batch = tasks[i:i+batch_size]
await asyncio.gather(*batch)
await asyncio.sleep(0.5) # 防止请求太快,被他们知道这是来自脚本的下载解析行为
if __name__ == "__main__":
asyncio.run(start_to_download())运行:
python ./series_image_cover_download.py上述脚本按系列番号批量遍历:给定 series(如 KBI)与 start_code / end_code 范围,自动生成 KBI-001、KBI-002 … 等 URL 并下载封面。
番号可类比产品型号体系:厂商(品牌)→ 系列(番号,如 KBI)→ 具体编号(如 KBI-002)。三星的 Galaxy Z-Fold / Z-Flip / S 系列即是类似的分层结构。
若需下载单片的全部样品图,解析 #sample-waterfall 区域即可,代码如下。
配置文件 special_image_detail_config.py
videos = [
{
"name" : "KBI-001",
},
{
"name": "KBI-002",
}
]主程序 special_image_detail_download.py
import os
import requests
from bs4 import BeautifulSoup
host = "https://www.javbus.com"
def image_download(names, onlyFolderImage = False):
for name in names:
URL = host + "/" + name
SAVE_DIR = "./{}".format(name)
HEADERS = {
"authority": "www.javbus.com",
"method": "GET",
"path": f"/{name}",
"scheme": "https",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"cookie": "PHPSESSID=nnelcojatbmsjh2s02i4m0fno4; existmag=mag; _tea_utm_cache_10000007=undefined",
"dnt": "1",
"priority": "u=0, i",
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
}
os.makedirs(SAVE_DIR, exist_ok=True)
session = requests.Session()
response = session.get(URL, headers=HEADERS)
soup = BeautifulSoup(response.text, "html.parser")
title_tag = soup.find("title")
title = title_tag.text.strip() if title_tag else "unknown_title"
print("页面标题:", title)
cover_tag = soup.select_one("a.bigImage img")
if cover_tag:
cover_url = cover_tag["src"]
if cover_url.startswith("/"):
cover_url = host + cover_url
ext = os.path.splitext(cover_url)[1]
cover_path = os.path.join(SAVE_DIR, f"cover{ext}")
cover_path = cover_path.replace("cover.jpg", "folder.jpg")
cover_headers = HEADERS.copy()
cover_headers["referer"] = URL
r = session.get(cover_url, headers=cover_headers, stream=True)
with open(cover_path, "wb") as f:
for chunk in r.iter_content(1024):
f.write(chunk)
print("封面图已下载:", cover_path)
if not onlyFolderImage:
sample_tags = soup.select("a.sample-box")
if not sample_tags:
print("没有样本图")
else:
for idx, a_tag in enumerate(sample_tags, 1):
img_url = a_tag.get("href")
if not img_url:
continue
ext = os.path.splitext(img_url)[1]
img_path = os.path.join(SAVE_DIR, f"sample_{idx}{ext}")
r = session.get(img_url, headers=HEADERS, stream=True)
with open(img_path, "wb") as f:
for chunk in r.iter_content(1024):
f.write(chunk)
print(f"样本图 {idx} 已下载:", img_path)
if __name__ == "__main__":
from special_image_detail_config import videos
image_download([video["name"] for video in videos])运行:
python ./special_image_detail_download.py视频资源下载
JavBus 等站点通常提供磁力链接,可用 Motrix 等客户端拉取;但对部分资源,直连 P2P 速度可能不理想。常见替代方案:
- 网盘离线:夸克、迅雷会员版等,将磁力转存后下载至本地。
- M3U8 流媒体:部分聚合平台以 HLS 协议输出,可借助自研脚本抓取分片并合并,详见 M3U8 下载。
更多玩法
- 若具备基础前端能力,无需 React/Vue,仅用 HTML + CSS + JavaScript 即可将爬取结果做成瀑布流图库:

基于 Python Flask + HTML/CSS/JS 实现。
后续将核心逻辑迁移至 Go + Gin + Bootstrap,在 Web 层以表格驱动抓取,并新增图库展示页(Vibe Coding 方式迭代)。

说明
- 本文仅讨论页面结构与爬虫技术实现,案例站点内容仅供学习参考,不构成任何商业用途或价值导向声明。本人不对案例站点内容负责。技术是无罪的
- 移动端扩展:Android 逆向与 HTTPS 抓包(Charles / mitmproxy)是另一条常见路线,可定位 App 内 XHR 接口,往往比解析 HTML 更稳定。竞品分析时,可复现数据对比,用可验证的结果替代主观争论。
- 其他方向:
- 3.1 采集公众号、小红书等平台的公开图文,落盘至 SQLite / TXT 等本地存储。
- 3.2 将归档内容作为语料,接入 RAG 或微调流程,用于检索增强与信息匹配。