简介:这是一份在线扒站与网页源码抓取技术的实操资源包,面向想学习HTML爬站、整站下载与源码分析的初中级开发者。资源以静态网页项目形式呈现,包含16个文件、压缩包体积约129KB,主要涵盖jQuery、Bootstrap、localforage等前端JS库,以及HTML、CSS、SVG、PNG和ICO等页面资源,适合用来理解网页结构、镜像站点工具逻辑与前端资源组织方式。已有5128人学习下载,说明该主题在网站备份、竞品页面分析和数据提取场景中具有较高参考价值。通过学习包内文件,读者可以直观看到在线扒站工具常见的页面入口、脚本调用方式与静态资源引用结构,并能结合Wget、HTTrack等命令行或镜像工具,掌握发送HTTP请求、解析HTML、处理动态内容及遵守robots.txt等关键环节,为后续构建轻量级爬虫或网站离线备份方案打下基础。
1. 在线扒站源码到底在扒什么:不只是 HTML,而是一份能离线打开的站点副本
第一次搜“在线扒站源码”的人,通常手头有一个眼馋的站点:排版好看、交互简洁,但对方没开源。所谓“在线扒源码”,是把线上站点正常返回的 HTML、CSS、JavaScript、图片、字体完整抓回本地,重组出一份能离线打开、能继续改的站点副本。市面上不少挂着“在线扒站”名字的工具站,比如 js11227.com 这类,多数只是给 wget、HTTrack 套了层网页壳。下面按判断站点类型、选工具、跑命令、排坑、验证的顺序,把我实际用过的命令和翻车点整理出来。做站点离线备份、前端参考、快速攒源码库的人都用得上。
2. 动手前先分清静态站和动态站:选错工具,扒回来就是一堆残废 html
很多人一上来就wget -r甩出去,跑了一晚上才发现首页是个 React 空壳。扒站先花三分钟判断目标站是静态还是动态,能省下后面一晚上返工。
2.1 三分钟判断目标站是纯 HTML 还是 JS 渲染:用 curl 看返回体
在浏览器里右键“查看网页源代码”,如果能直接看到正文文本、img标签、业务 class 名,说明内容是服务端渲染好的,静态镜像工具能用。如果源代码里只有一个<div id="app"></div>加一堆<script>,数据全靠 JS 拉接口再填充,那 wget 只能扒到壳。
不放心的话,用 curl 看服务端实际返回了什么:
curl -s -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \ "https://example.com/" | head -c 2000-s是静默模式,不打印下载进度;-A把 User-Agent 改成真实浏览器,避免被默认 UA 拒绝;head -c 2000只截取前 2000 字节看内容。返回里能看到正文关键词,基本可以判断是静态站;如果输出全是空标签和 script 引用,就走后面第 4 章的接口抓取方案。
页面需要登录才能看到内容的情况也归入动态方案,因为 wget 直接跑拿不到登录后的数据,除非你已经准备好带 Cookie 的请求头。这一步判断不超过三分钟,值得在每次扒站之前都做一次。
2.2 常见扒站工具选型:wget、HTTrack、自写脚本的取舍
没有万能的扒站工具,只有适合当前站点类型的工具。我做整站镜像时的选型标准大致是这样:
| 工具 | 最适合的场景 | 优点 | 常见缺点 |
|---|---|---|---|
| wget | 服务端渲染的静态站 | 参数稳定、适合脚本化、断点续传友好 | 对动态渲染和带签名的接口无能为力 |
| HTTrack | 中小型整站镜像 | 图形化队列、自动重写链接、支持排除规则 | 抓取策略偏激进,队列细节不容易控制 |
| requests + BeautifulSoup | 有明确接口、需要二次加工的站 | 可以自由处理 JSON、翻页、文件整理 | 所有边界条件自己写 |
| Playwright | JS 渲染且接口带签名的站 | 能拿到真实执行完 JS 的 DOM | 慢、吃内存、资源文件还要二次下载 |
选型核心原则是:能用简单工具解决的不要上浏览器。一个纯静态营销页,wget 一条命令解决;一个内容站带分页和文章详情,HTTrack 更稳;一个管理后台或列表接口,脚本最灵活;页面渲染强依赖 JS 时才考虑无头浏览器。所谓“在线扒站源码”的网页壳,本质就是把下面这些命令行参数换成表单,参数理解得越清楚,越不会被工具站的黑匣子卡住。
2.3 自己写一个在线扒站源码的骨架:requests + BeautifulSoup 的最小实现
理解扒站原理最直接的方式,就是写一个最小可用的源码骨架。下面这段代码抓首页,并把页面里的图片、CSS、JS 下载到本地目录,同时把 HTML 里的资源路径改成相对路径,保证离线能开:
import os import requests from bs4 import BeautifulSoup from urllib.parse import urljoin, urlparse start_url = "https://example.com/" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", } resp = requests.get(start_url, headers=headers, timeout=15) # 优先按页面实际编码解码,避免后面写文件时中文乱码 resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "html.parser") os.makedirs("mysite", exist_ok=True) # 遍历页面里常见的静态资源标签 for tag, attr in [("img", "src"), ("link", "href"), ("script", "src")]: for node in soup.find_all(tag): value = node.get(attr) if not value or value.startswith("data:"): continue full_url = urljoin(start_url, value) # 把相对路径拼成完整 URL path = urlparse(full_url).path.lstrip("/") if not path: continue local_path = os.path.join("mysite", path) os.makedirs(os.path.dirname(local_path), exist_ok=True) with open(local_path, "wb") as f: f.write(requests.get(full_url, headers=headers, timeout=15).content) # 把页面里的资源路径改成相对路径,离线才能直接打开 node[attr] = "../" + path with open(os.path.join("mysite", "index.html"), "w", encoding="utf-8") as f: f.write(str(soup))这是我最常用来封装在线扒站逻辑的骨架。流程很简单:先取首页 HTML,BeautifulSoup 找出img、link、script三类标签的地址,urljoin处理相对路径,urlparse只取 path 部分作为本地保存路径,下载完成后把页面里的src、href改成相对路径。
三个关键点:第一,headers里必须有接近真实浏览器的 User-Agent,很多站点对 requests 默认 UA 直接返回 403;第二,resp.encoding = resp.apparent_encoding不能省,否则 GBK 编码的站写出来就是乱码;第三,urljoin必须用,因为页面里大量写的是images/logo.png这种相对地址,不拼完整 URL 没法下载。
局限也要说清楚:这套骨架只覆盖单页的img/link/script,CSS 里的background url()、懒加载的>wget \ --recursive \ --level=5 \ --page-requisites \ --convert-links \ --adjust-extension \ --no-parent \ --restrict-file-names=windows \ --directory-prefix=mysite \ --timeout=20 \ --tries=2 \ --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \ "https://example.com/"
--recursive:开启递归抓取,没有它 wget 只下载单个页面。--level=5:递归深度限制为 5 层。默认无限深度,碰上无限滚动或日历参数会跑到失控。--page-requisites:把页面渲染需要的不只是 HTML,还有 CSS、JS、图片、字体全部拿下来。不加这个参数,扒回来大概率是只有文字没有样式的残废页面。--convert-links:下载完成后重写 HTML 里的链接,把指向线上的绝对路径改成本地相对路径。这是离线能直接打开的关键。--adjust-extension:遇到/article?id=1这类不带.html的 URL,保存时补上.html后缀,方便浏览器识别。--no-parent:禁止往上抓取上级目录,避免把链接指向的上级栏目也一起拖进来。--restrict-file-names=windows:把 URL 里的特殊字符转成 Windows 兼容文件名。如果只在 Linux 上用,可以不加;但目录要拷给同事,加上能省很多乱码文件名的麻烦。--directory-prefix=mysite:指定保存目录。--timeout=20 --tries=2:防止某个资源卡死导致整个任务挂住。--user-agent:必须写。不少站点对 wget 默认的 UA 直接拒绝,这个头就是你的第一层“入场券”。
如果只想要首页和它依赖的资源,先跑这条窄命令:
wget --page-requisites --convert-links \ --directory-prefix=mysite \ "https://example.com/"没有--recursive,wget 不会展开整站,只会把首页及相关资源拉回来。我习惯先跑一遍看页面效果,确认样式、图片都在,再上完整递归命令,相当于花一分钟先看效果,避免一次性把错误目录结构也抓到本地。
3.2 带登录态和 Referer 的扒法:Cookie、UA、请求头写进 wget
部分站点需要登录才能看到正文,或者图片目录做了防盗链。wget 可以用--header逐个加请求头:
wget \ --header="User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \ --header="Referer: https://example.com/" \ --header="Cookie: sessionid=abc123" \ --recursive --level=3 \ --page-requisites --convert-links \ "https://example.com/dashboard"--header会在每个请求里带上对应头。Cookie 从浏览器 DevTools 的 Network 面板里复制,只需要复制像sessionid=abc123这样的必要字段,不用整段复制。整段复制容易把一些浏览器扩展写进 Cookie 的无效字段也带进去,无端增加被服务端拒绝的概率。
如果 Cookie 很长,建议保存成 Netscape 格式的 cookies.txt 再用--load-cookies加载:
wget --load-cookies cookies.txt \ --recursive --level=3 \ --page-requisites --convert-links \ "https://example.com/"cookies.txt 每行格式是:域名、是否包含子域、路径、是否 HTTPS、过期时间、Cookie 名、Cookie 值。手工维护比较烦,我一般直接从浏览器插件导出。
遇到 403 不要立刻怀疑是 IP 被拉黑,先检查 Referer。图片防盗链的常见做法是校验 Referer 必须来自同站点,wget 的递归请求默认不带 Referer,这一个头漏掉,图片就会全部 403。经验做法是 Referer 统一指向站点首页,不要指向具体图片页。
3.3 断点续传和后台跑长任务:nohup + wget -c 的组合
内容多的站点递归镜像跑半小时很正常。终端一旦断开,wget 默认跟着退出,前面就白跑了。我一般用 nohup 把任务放到后台,日志写进文件:
nohup wget -c \ --recursive --level=5 \ --page-requisites --convert-links \ --directory-prefix=mysite \ "https://example.com/" > wget.log 2>&1 &nohup让进程忽略挂断信号;-c是断点续传,中断后再次执行会从已下载的字节继续;> wget.log 2>&1把标准输出和错误都写进日志;最后的&让命令在后台执行。
看进度用:
tail -f wget.log关注日志里反复出现的某个 URL,那往往是循环链接或无法下载的异常资源。注意-c在递归模式下并不是每个文件都能精确断点续传,HTML 类小文件经常会被重新拉取,这不影响最终完整性,不用太纠结日志里的重复记录。
任务结束后做一次统计:
du -sh mysite && find mysite -type f | wc -ldu -sh看总共占了多少空间,find mysite -type f | wc -l统计文件数量,拿这个数和线上站点的资源规模做对比,能快速判断是不是漏抓了大目录。
4. 动态站点和接口型站点怎么扒:解析 XHR、落 JSON、再渲染成静态 html
静态镜像工具面对动态站基本失效。动态站的内容不是藏在 HTML 里,而是藏在 XHR 接口里。扒这类站的核心思路也变成:找到接口,拿到 JSON,自己渲染成静态 html。
4.1 在 DevTools 里定位 XHR 接口:过滤条件与翻页规律
打开 DevTools 的 Network 面板,勾选 Fetch/XHR,刷新页面。列表里凡是返回 JSON 的请求,就是数据接口。常见的特征包括/api/list、/api/articles?page=1这类路径。
在 Preview 或 Response 里看返回的 JSON,找到标题、链接、时间这些字段名,后续抓取脚本就按这些字段解析。需要翻页时,切换页面上的下一页,观察 XHR 请求的 query 参数变化,通常是page=2或offset=20,这个规律看两页就能确认。
如果接口带 token 或签名,先别急着逆向 JS。打开 Console 刷新页面,很多站点会把 token 存在 Cookie 里,或者挂在某个全局变量上。带 Cookie 能解决就直接带;如果签名是动态计算的,就用 4.3 的无头浏览器方案,而不是去分析压缩混淆过的 JS 文件。无限滚动的站点会持续刷 XHR,这类接口抓取一定要加 sleep,把对方接口打挂了,你的 IP 可能直接被防火墙拦掉,得不偿失。
4.2 用一个 Python 脚本把 JSON 接口变成整页 html
找到接口后,用脚本抓取并渲染成本地 html。这里给一份可直接改的模板:
import html import time import requests api = "https://example.com/api/articles?page=1&size=20" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://example.com/articles", "X-Requested-With": "XMLHttpRequest", "Accept": "application/json", } resp = requests.get(api, headers=headers, timeout=10) resp.raise_for_status() data = resp.json() cards = [] for page in range(1, 6): # 抓前 5 页 resp = requests.get( f"https://example.com/api/articles?page={page}&size=20", headers=headers, timeout=10 ) for item in resp.json()["data"]["list"]: title = html.escape(item["title"]) link = html.escape(item["url"]) cards.append(f'<li><a href="{link}" target="_blank">{title}</a></li>') time.sleep(1) # 控制请求频率,避免触发风控 page = f"""<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>文章列表</title> </head> <body> <ul> {''.join(cards)} </ul> </body> </html>""" with open("articles.html", "w", encoding="utf-8") as f: f.write(page) print("saved", len(cards), "items")headers里的Referer和X-Requested-With很关键,很多后端靠这两个头判断请求是不是从页面内部发起的,缺了容易出现空数据或 403。html.escape用来转义标题里的尖括号和引号,避免抓回来的内容里藏了<script>之类的注入代码。time.sleep(1)是给分页循环做的限速,频率太低容易触发反爬。
最后的 HTML 用 f-string 拼出来,注意''.join(cards)的写法,直接str(cards)会把逗号也带进去。这个方案的价值在于:你不用管原始站点是 Vue 还是 React,只要接口还在,就能用自己的模板生成稳定、干净、离线可读的页面。
4.3 实在要跑浏览器:用 Playwright 无头模式抓 JS 渲染后的 html
有些站点数据完全在 JS 里计算,接口也带签名,选择直接渲染浏览器是最后的后悔药。Playwright 无头模式适合这种场景:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", viewport={"width": 1280, "height": 800}, ) page.goto("https://example.com/", timeout=30000) page.wait_for_selector(".list-item") # 滚动到底部,触发懒加载 page.evaluate("window.scrollTo(0, document.body.scrollHeight)") page.wait_for_timeout(2000) html_text = page.content() with open("rendered.html", "w", encoding="utf-8") as f: f.write(html_text) browser.close()headless=True表示不显示浏览器窗口,适合在服务器上跑。wait_for_selector等待首屏数据渲染完成,比固定sleep可靠。滚动到底部是为了触发懒加载,wait_for_timeout给后续接口留出返回时间。page.content()拿到的是 JS 执行完之后的完整 DOM,而不是初始的壳。
注意 Playwright 拿到的 HTML 里资源路径可能是浏览器展开后的完整 URL,要转成离线副本还得继续下载资源并重写路径。另外,如果页面强依赖 WebSocket 或 Canvas 实时绘制,无头浏览器也只能拿到某一帧的快照,不要执着于完全复刻,把 DOM 当静态快照存档就够了。
5. 扒站避坑:五处必踩的坑,附现象、原因和解决
扒站翻车基本都翻在这五个地方。每一条我都给出现象、原因和解决,照着排查比重新扒一遍省时间。
5.1 扒完双击打开却是空白页:相对路径、绝对路径和 base 标签
现象:本地打开 index.html,文字能出来,但样式全丢,图片全裂。
原因:三个可能位置。第一,页面里写了<base href="https://example.com/">,浏览器会把所有相对路径拼到线上域名,本地自然拿不到资源。第二,HTML 本身用的是/assets/style.css这种根路径绝对地址,没有--convert-links就不会被重写。第三,自写脚本时只下载了资源,忘了把标签里的地址改成相对路径。
解决:wget 镜像必须保留--convert-links,它会批量重写 HTML 里的链接。自写脚本则要像 2.3 的骨架那样,每下载一个文件就把src、href改成../开头的相对路径。遇到<base>标签,离线后直接删掉,或改成<base href="./">,避免本地文件被强行指回线上。
5.2 图片全裂、字体不显示:防盗链和 Referer 的拉扯
现象:HTML 结构完整,但图片全部显示裂图,字体文件也加载不出来。
原因:站点或 CDN 配置了防盗链,只有带正确 Referer 的请求才返回图片内容。wget 递归抓取子资源时,默认不会带上 Referer,所以图片请求被源站拒绝。
解决:镜像命令里加一行固定 Referer 头:
wget \ --header="Referer: https://example.com/" \ --recursive --level=5 \ --page-requisites --convert-links \ "https://example.com/"如果图片已经抓完但内容不对,可以在脚本里单独对图片域名重试请求,把 Referer 改成图片所在页面。如果原始域名已经不可用,那图片是拿不回来的,不用在这上面死磕,优先保证 HTML 和样式表完整。
5.3 HTML 源码里夹带统计脚本和第三方外链:怎么看、怎么清
现象:扒下来的页面在本地打开,后台还在请求第三方统计域名,甚至有弹窗。
原因:原始 HTML 里直接写了统计脚本、广告脚本或其它第三方外链,这些脚本在离线环境下也会尝试连接外网,拖慢打开速度,还可能加载出一堆无关内容。
解决:镜像完成后做一次全局清理,搜出所有http开头的 script、link、iframe,按域名过滤掉。用 Python 批量清:
import re from pathlib import Path for f in Path("mysite").rglob("*.html"): text = f.read_text(encoding="utf-8", errors="ignore") text = re.sub(r'<script[^>]+src="https?://[^"]*"[^>]*></script>', "", text) f.write_text(text, encoding="utf-8")这段正则只删除带外部 src 的 script 标签,内联脚本保留。清理之后再用本地静态服务跑一遍,确认不再有外网请求。这个步骤也是我每次扒站之后必做的卫生工作,既减少本地卡顿,也避免离线页面暗藏第三方追踪。
5.4 磁盘被无限递归塞满:失控的层级抓取和循环链接
现象:wget 跑了一会儿,目录已经几个 G,日志里全是同一个页面加了不同 query 参数的 URL。
原因:日历控件、排序按钮、无限滚动、验证码跳转,都会生成新的 URL,wget 把它们当成新页面一路抓下去。带参数的 URL 尤其危险,?page=1、?page=2可以无限组合。
解决:限制递归深度,再给总量封顶:
wget \ --recursive --level=2 \ --quota=200m \ --reject-regex='\?|page=' \ --page-requisites --convert-links \ "https://example.com/"--quota=200m是总下载量上限,超过就停,这是最有效的“后悔药”。--reject-regex按正则排除带问号和 page 的 URL,避免陷入查询参数漩涡。拿不准的时候先用--level=1跑一遍,看抓了多少文件,再决定要不要加深。
5.5 页面打开全是乱码:编码声明与实际字节不一致
现象:浏览器打开本地 HTML,中文变乱码;但同一文件在线上是正常的。
原因:服务器实际返回的是 GBK 字节,HTML 里却声明<meta charset="utf-8">,或者反过来。wget 把原始字节存下来,本地浏览器按 meta 声明解码,自然乱码。
解决:写脚本时用resp.encoding = resp.apparent_encoding,让 requests 自动猜编码。已经抓完的文件用 iconv 批量转:
iconv -f GBK -t UTF-8 index.html > index_utf8.html文件多时用 Python 统一处理:
from pathlib import Path for f in Path("mysite").rglob("*.html"): raw = f.read_bytes() try: text = raw.decode("gbk") except UnicodeDecodeError: continue f.write_bytes(text.encode("utf-8"))先按 GBK 解,解不开说明本来就是 UTF-8,跳过。转完之后记得把 HTML 头部的 charset 声明也统一改成 utf-8,不然浏览器还是会按旧声明解码。
6. 扒完之后怎么办:验证、把多个 html 打包成 zip、再转 markdown
6.1 本地起一个静态服务,核对文件数
扒完先别急着开 file:// 协议直接双击,file 协议下很多相对路径行为和服务环境不一致。我习惯先起一个本地静态服务:
cd mysite python -m http.server 8080浏览器访问http://localhost:8080,逐个点击主要栏目,看控制台有没有 404。然后用两条命令做量化核对:
find mysite -type f | wc -l du -sh mysite文件数和总体积对不上预期,说明有资源没抓下来,优先排查懒加载和防盗链。
6.2 把扒下来的 html 批量转成 markdown:html2text 与 pandoc 的选择
如果扒站是为了研究内容而不是还原页面,把 html 转成 markdown 更好检索。我常用 html2text:
python -m pip install html2text python - <<'PY' import html2text from pathlib import Path for f in Path("mysite").rglob("*.html"): md = html2text.html2text(f.read_text(encoding="utf-8", errors="ignore")) f.with_suffix(".md").write_text(md, encoding="utf-8") PYhtml2text 对标题、列表、链接的支持够用;遇到复杂表格,改用 pandoc 的 GFM 格式更稳:
pandoc index.html -t gfm -o index.md6.3 我现在的习惯:扒前记清单、扒后清外链,维护一个本地索引
我现在扒站的固定流程是:动工前先列资源清单,把栏目、图片目录、接口路径写进 README,避免抓完不知道抓了什么;扒完统一清统计脚本;再按栏目建目录,把 index.html、assets、api_json 分开存,攒成一个私人版“免费 python 源码大全”式的本地索引。如果只是为了收集页面素材,我会把多个 html 连同 assets 打包成 zip 归档,比散目录省心得多。做 html 网页制作参考时,也从这些归档目录里翻同类型站点对照。希望这些命令和踩坑记录能帮到你。
本文还有配套的精品资源,点击获取