不写爬虫也能批量提取商品ID和图片:本地HTML解析与9:16截图实战 拼多多的商品链接和素材整理做过的同学应该都有体会手动打开一个页面、复制商品 ID、另存主图、再裁一张竖版封面单个商品还好一旦数量到几十上百整个人都会麻。更麻烦的是很多人第一反应是直接写 Python 爬虫去批量抓取结果要么被平台风控拦下来要么把网络请求频率拉满协议和合规上都容易踩坑。这篇文章我打算换一个更稳妥的思路用“本地解析”代替“远程爬取”。具体来说就是把商品页面通过浏览器正常访问并“另存为”到本地再用 Python 脚本从本地 HTML 文件里批量提取商品 ID、图片地址最后用 Pillow 或 Playwright 生成 9:16 精准截图。整个过程对目标服务器零压力脚本只读本地文件不主动发起大量请求适合做选品素材整理、商品信息归档、内容创作配图等场景。这套流程不需要你从零写复杂框架只需要会基础的 Python 文件操作跟着实操一步步就能跑通。下面我从概念边界、环境准备、核心代码到常见报错排查完整拆解一遍。1. 网页解析与爬虫的区别1.1 为什么“本地解析”不等于“爬虫”很多同学一听“批量提取商品 ID、批量提取图片”下意识就认为这是爬虫。其实从技术实现和请求行为上看二者有明显的区别。典型的爬虫工作方式是程序主动模拟浏览器向服务器发起 HTTP 请求循环访问商品列表页、详情页或接口然后从响应内容中提取数据。这个过程只要写好循环确实可以短时间内拿到大量数据但问题也很现实高频请求会给对方服务器造成压力也可能违反平台的服务条款更别说遇到签名校验、字体反爬、IP 限制时维护成本会直线上升。而本文说的“本地解析”核心特征是“先有本地文件再解析本地文件”。操作步骤是在浏览器中正常打开商品页面。页面内容完整加载后使用浏览器的“另存为”功能保存到本地。用 Python 脚本读取本地 HTML 文件从 HTML 结构中提取商品 ID、标题、图片地址等信息。脚本不主动请求目标站点解析的只是用户自己访问过的页面副本。本质上这个操作和用编辑器打开一份网页源码、用 CtrlF 查找字符没有区别只是把查找动作自动化了。对平台方来说除了用户正常访问那一次之外没有额外的流量压力。1.2 这套方案能做什么把思路定成“本地解析”之后整个工具链条就非常清晰了功能说明批量提取商品 ID从多个本地 HTML 文件中自动识别商品 ID并输出到表格批量提取原图地址从页面源码的图片标签中提取 URL并尝试还原高清原图链接9:16 精准截图对本地商品图做 9:16 中心裁剪或用浏览器内核按 9:16 视口截图数据落盘商品 ID、标题、描述、图片地址统一输出为 CSV/Excel适合的场景包括做商品选品记录时把收藏的爆款链接结构化整理成表格。做内容创作时批量准备 9:16 竖版封面图。做商品素材归档时把图片地址和商品 ID 一一对应保存。学习前端页面结构与数据提取时在一个完全可控的本地环境里练习解析思路。1.3 工具的边界与原则这里要提前说清楚几条底线避免后续操作跑偏只处理自己有权限访问、合法获取的页面内容。保存页面和解析仅用于个人学习、自有素材整理、选品参考等合法用途。如果后续确实需要下载图片建议手动或低频处理并遵守目标平台的服务条款不要批量高频请求。不尝试绕过平台限制、不破解验证码、不攻击接口。明确边界之后再进入技术实操部分。2. 环境准备与项目结构2.1 运行环境说明本文示例以 Python 3.8 为基础建议使用虚拟环境隔离依赖。操作系统上Windows、macOS、Linux 都可以代码没有依赖特定系统命令。版本不需要完全一致重点是保证以下核心库能正常导入依赖库用途beautifulsoup4解析 HTML 结构提取标签属性lxmlBeautifulSoup 的解析引擎速度快pandas数据清洗并输出 CSV/ExcelPillow对图片做 9:16 裁剪处理playwright使用 Chromium 内核按 9:16 视口截图需要注意playwright 负责驱动浏览器首次使用还需要安装浏览器内核。如果只想做纯图片裁剪Pillow 就够用playwright 可以按需安装。创建虚拟环境并安装依赖mkdir duoduo_parser cd duoduo_parser python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install beautifulsoup4 lxml pandas Pillow playwright如果使用 Playwright 截图方案还需要额外安装 Chromiumplaywright install chromium2.2 项目目录结构我建议把页面文件、脚本、输出目录分开管理避免后面文件一多就乱。duoduo_parser/ ├── pages/ │ ├── 商品A.html │ ├── 商品A_files/ │ ├── 商品B.html │ └── 商品B_files/ ├── output/ │ ├── goods_info.csv │ ├── goods_images.csv │ ├── images/ │ └── screenshots/ ├── parse_goods.py ├── extract_images.py └── make_screenshot.py其中pages目录存放浏览器“另存为”得到的 HTML 文件和同名资源目录output目录存放脚本生成的 CSV、裁剪图、截图三个 Python 脚本分别负责“解析商品信息”“提取图片地址”“生成 9:16 截图”。2.3 页面保存操作建议在浏览器中打开商品页面后不要立刻另存为。因为很多页面采用懒加载图片和部分信息要滚动到可视区域才会加载出来。比较稳妥的做法是打开商品页后按 F12 打开开发者工具切换到 Network 面板并勾选 Disable cache。从页面顶部慢慢向下滚动确保商品图、详情图、相似推荐等区域都触发加载。点击浏览器右上角菜单选择“另存为”保存类型选择“网页全部”。保存后会生成一个 HTML 文件和同名的资源文件夹。这样保存下来的 HTML 内容更完整后续解析时漏数据的概率会小很多。3. 核心原理拆解从本地 HTML 中找商品 ID3.1 页面数据藏在哪里一个商品页面的 HTML 里商品 ID 可能出现在多个位置。常见的包括浏览器地址栏 URL 参数例如goods_id12345678901234567。页面源码里的 JS 变量例如window.__goods_id 12345678901234567。某些节点属性的标识例如按钮、收藏按钮、加入购物车按钮上的># -*- coding: utf-8 -*- import re with open(pages/商品A.html, r, encodingutf-8, errorsignore) as f: html f.read() # 搜索常见的 ID 字段关键词 for keyword in [goods_id, goodsId, goods_id_str, goodsID]: matches re.findall(rf{keyword}[\]?\s*[:]\s*[\]?(\d{{8,20}}), html) if matches: print(keyword, matches[:5])这段代码会帮你快速定位页面中出现了哪些 ID 字段以及 ID 的格式。确认之后再针对性地写批量解析规则。3.2 正则提取 vs DOM 提取提取商品 ID 主要有两种方式第一种是正则表达式适合匹配带明显规律的字符串。例如商品 ID 是连续的数字且前面带有固定字段名正则写起来很直接。优点是不依赖 HTML 结构缺点是如果页面里的无关内容也出现类似数字容易误匹配。第二种是 BeautifulSoup 解析 DOM适合目标信息位于某个标签的># -*- coding: utf-8 -*- # 文件路径parse_goods.py import re import glob import pandas as pd from bs4 import BeautifulSoup TARGET_DIR pages OUTPUT_CSV output/goods_info.csv # 收集常见的商品 ID 写法按优先级顺序匹配 ID_PATTERNS [ re.compile(rgoods_id[:]\?(\d{8,20})\?), re.compile(rgoodsId[:]\?(\d{8,20})\?), re.compile(rgoods_id_str[\]?\s*[:]\s*[\](\d{8,20})[\]), re.compile(rdata-goods-id[\]?\s*\s*[\](\d{8,20})[\]), ] def extract_goods_info(html_path): with open(html_path, r, encodingutf-8, errorsignore) as f: html f.read() result {file: html_path} # 提取标题 title_tag re.search(rtitle[^]*(.*?)/title, html, re.S) result[title] title_tag.group(1).strip() if title_tag else # 提取商品 ID goods_id for pattern in ID_PATTERNS: m pattern.search(html) if m: goods_id m.group(1) break result[goods_id] goods_id # 提取 meta 描述 soup BeautifulSoup(html, lxml) meta soup.find(meta, attrs{name: description}) result[description] meta.get(content, ) if meta else return result def main(): rows [] for html_path in glob.glob(f{TARGET_DIR}/*.html): rows.append(extract_goods_info(html_path)) if not rows: print(f在 {TARGET_DIR} 目录下没有找到 HTML 文件) return df pd.DataFrame(rows) df.to_csv(OUTPUT_CSV, indexFalse, encodingutf-8-sig) print(df) if __name__ __main__: main()运行方式python parse_goods.py预期输出是控制台打印一个表格同时在output/goods_info.csv生成 UTF-8 编码的 CSV 文件。这里用utf-8-sig编码是为了让 Excel 直接打开时中文不乱码。需要注意ID_PATTERNS里的规则只是通用思路具体页面中可能字段名完全不同。如果你的页面里商品 ID 出现在 URL 参数中可以单独把 URL 存下来再用urlparse解析参数思路是一样的。4. 批量提取商品 ID 与数据落盘4.1 从文件名到数据的映射在批量场景中往往需要把“某个页面”和“某个商品 ID”对应起来。最简单的方式是在解析结果中额外保存文件名但更好的做法是直接把商品 ID 作为后续处理的主键。商品 ID 稳定、唯一便于和图片记录、截图记录做关联。运行上面的parse_goods.py之后output/goods_info.csv里已经包含了商品 ID。接下来做数据清洗时需要注意几个细节如果页面是动态渲染的文章详情页title可能带有网站后缀比如“商品标题_平台名称”需要统一去掉。有些页面会在 URL 参数里带goods_id在 JS 变量里带另一套goodsId两者可能一致也可能一个是推广 ID一个才是真实商品 ID。建议以更完整的 JSON 变量或页面内部字段为准。description 为空时不用强行补数据保留空字符串即可。4.2 清洗和扩展字段我们可以把解析脚本稍微扩展一下增加一个清洗函数。def clean_title(title, suffix_listNone): if not title: return suffix_list suffix_list or [_拼多多, -拼多多, _手机版, _移动端] for suffix in suffix_list: if title.endswith(suffix): title title[: -len(suffix)] return title.strip()实际使用中你只需要把自己页面里出现的后缀加进列表即可。这里想表达的核心思想是解析脚本是一个“提取器”而清洗逻辑是一个独立的“过滤器”两者分开写后续页面结构变化时维护成本更低。4.3 避免重复解析如果pages目录下已经解析过一次再次运行会产生重复记录。一种简单策略是记录已处理文件的mtime或者直接让脚本生成一个有去重逻辑的结果表。为了不过度设计这里用 pandas 在读取已有 CSV 后做一次drop_duplicatesdef merge_output(new_df, csv_path): try: old_df pd.read_csv(csv_path, dtype{goods_id: str}) merged pd.concat([old_df, new_df], ignore_indexTrue) merged merged.drop_duplicates(subset[goods_id]) except FileNotFoundError: merged new_df merged.to_csv(csv_path, indexFalse, encodingutf-8-sig)这样即使重复运行脚本也不会堆积重复的商品记录。5. 批量提取原图5.1 图片 URL 与缩略图参数商品图片的 URL 一般保存在img标签的src属性中。不过很多平台为了加速展示会优先使用压缩过的缩略图地址。缩略图 URL 和原图 URL 之间通常只差几个尺寸参数常见规律有缩略图 URL 示例特征https://example.com/img/abc.jpg!400x400.jpgURL 尾部带!宽度x高度https://example.com/img/abc_400x400.jpg文件名后插入_400x400https://example.com/img/abc-300x300.jpg文件名后插入-300x300https://example.com/img/abc.jpg?imageMogr2/thumbnail/!300x300用查询参数控制尺寸不同平台的规则不同甚至同一平台不同批次的图片 URL 也不完全一致。所以正确的做法是先保存一两个页面把图片 URL 粘贴出来对比找出其中的规律再写还原函数。5.2 图片地址提取与还原下面这个脚本会从本地 HTML 中提取图片地址并尝试去掉常见的缩略图参数还原成高清原图链接。# -*- coding: utf-8 -*- # 文件路径extract_images.py import re import glob import os import pandas as pd from bs4 import BeautifulSoup from parse_goods import extract_goods_info TARGET_DIR pages OUTPUT_CSV output/goods_images.csv # 根据自己保存页面的实际情况调整 NORMALIZE_RULES [ (r!(\d)x(\d)(\.(jpg|jpeg|png|webp))$, r\3), (r_(\d)x(\d)(\.(jpg|jpeg|png|webp))$, r\3), (r-(\d)x(\d)(\.(jpg|jpeg|png|webp))$, r\3), ] def normalize_big_image(url): for pattern, repl in NORMALIZE_RULES: url re.sub(pattern, repl, url) return url def extract_image_urls(html_path): with open(html_path, r, encodingutf-8, errorsignore) as f: html f.read() soup BeautifulSoup(html, lxml) urls set() for img in soup.find_all(img): src img.get(src) or img.get(data-src) or if src: urls.add(src) for tag in soup.find_all(attrs{background: True}): bg tag.get(background) if bg: urls.add(bg) return [normalize_big_image(u) for u in urls] def main(): rows [] for html_path in glob.glob(f{TARGET_DIR}/*.html): info extract_goods_info(html_path) goods_id info.get(goods_id) for url in extract_image_urls(html_path): rows.append({goods_id: goods_id, image_url: url}) if not rows: print(没有提取到图片地址) return df pd.DataFrame(rows) df.to_csv(OUTPUT_CSV, indexFalse, encodingutf-8-sig) print(f共提取 {len(df)} 张图片地址已保存到 {OUTPUT_CSV}) if __name__ __main__: main()运行python extract_images.py生成的结果里每一行代表一个商品的一张图片地址并通过goods_id与商品信息表关联。这里要注意去重时使用了set()同一个页面中重复出现的图片地址不会计入多次。5.3 懒加载属性补充很多商品详情页会使用懒加载图片src属性可能是空白占位图真正的图片地址放在>for img in soup.find_all(img): src (img.get(data-src) or img.get(data-original) or img.get(data-lazy-src) or img.get(src)) if src: urls.add(src)哪个属性里存了真实图片以你保存的 HTML 实际结构为准上面列的是最常碰到的几个属性名。6. 9:16 精准截图实战6.1 什么是 9:16 精准截图9:16 是竖版比例宽高比约等于 0.5625常见于手机端主图、短视频封面、图文笔记配图。所谓“精准截图”是指最终输出的图片严格满足这个比例而不是随手把浏览器窗口拉一下直接截图。实现方式有两条路如果已经有商品图使用 Pillow 对图片做中心裁剪得到严格 9:16 比例的图片。如果需要对本地 HTML 页面进行视口截图使用 Playwright 设置 540x960 或 1080x1920 的浏览器视口截取视口内容。下面分别给出完整代码。6.2 Pillow 中心裁剪方案中心裁剪的思路是先判断原图比例与目标比例的关系再决定以宽度还是高度为基准来裁。如果原图偏宽就以高度为基准裁掉左右两边多余的宽度如果原图偏高就以宽度为基准裁掉上下两边多余的高度。# -*- coding: utf-8 -*- # 文件路径make_screenshot.py import os from PIL import Image INPUT_DIR output/images OUTPUT_DIR output/screenshots TARGET_RATIO 9 / 16 # 0.5625 def crop_to_9_16(image_path, output_path): img Image.open(image_path) w, h img.size current_ratio w / h if current_ratio TARGET_RATIO: # 图片偏宽按高度裁剪宽度 new_w int(h * TARGET_RATIO) left (w - new_w) // 2 box (left, 0, left new_w, h) elif current_ratio TARGET_RATIO: # 图片偏高按宽度裁剪高度 new_h int(w / TARGET_RATIO) top (h - new_h) // 2 box (0, top, w, top new_h) else: box (0, 0, w, h) cropped img.crop(box) cropped.save(output_path, quality95) print(f已生成: {output_path} 尺寸: {cropped.size}) def main(): if not os.path.exists(INPUT_DIR): print(f目录不存在: {INPUT_DIR}) return os.makedirs(OUTPUT_DIR, exist_okTrue) for name in os.listdir(INPUT_DIR): if name.lower().endswith((.jpg, .jpeg, .png, .webp)): src os.path.join(INPUT_DIR, name) base os.path.splitext(name)[0] dst os.path.join(OUTPUT_DIR, base _9x16.jpg) try: crop_to_9_16(src, dst) except Exception as e: print(f处理失败: {src}, 错误: {e}) if __name__ __main__: main()这段代码会把output/images下的所有图片逐一处理生成output/screenshots下的 9:16 版本。需要注意几点Pillow 对 WebP 的支持取决于安装时的构建选项如果提示不能打开 WebP 图片可以先转成 PNG 再处理。中心裁剪会损失边缘内容。如果商品主体不在中心建议裁剪前先用看图工具确认构图。如果希望“保留完整图片 背景填充”而不是裁剪可以改成ImageOps.pad将多余部分用白色或模糊背景填充。6.3 Playwright 视口截图方案如果你需要把本地 HTML 页面整体截成 9:16 的图可以用 Playwright 启动 Chromium 浏览器把视口设置为 540x960然后加载本地 HTML 文件并截图。# -*- coding: utf-8 -*- # 文件路径make_screenshot_browser.py import asyncio import glob import os from pathlib import Path from playwright.async_api import async_playwright TARGET_DIR pages OUTPUT_DIR output/screenshots VIEWPORT_WIDTH 540 VIEWPORT_HEIGHT 960 async def screenshot_html(html_path, output_path): async with async_playwright() as p: browser await p.chromium.launch() page await browser.new_page( viewport{width: VIEWPORT_WIDTH, height: VIEWPORT_HEIGHT} ) url Path(html_path).resolve().as_uri() await page.goto(url, wait_untilnetworkidle) await page.wait_for_timeout(1000) await page.screenshot(pathoutput_path, full_pageFalse) await browser.close() print(f已生成: {output_path}) async def main(): os.makedirs(OUTPUT_DIR, exist_okTrue) html_files glob.glob(f{TARGET_DIR}/*.html) if not html_files: print(f在 {TARGET_DIR} 目录下没有找到 HTML 文件) return for html_path in html_files: base os.path.splitext(os.path.basename(html_path))[0] out os.path.join(OUTPUT_DIR, base _9x16.png) await screenshot_html(html_path, out) if __name__ __main__: asyncio.run(main())这里关键点是full_pageFalse。Playwright 截图的默认行为是截取当前视口区域配合 9:16 的 viewport得到的就是严格 9:16 的页面截图。如果设置full_pageTrue会截取整个可滚动页面的高度比例就不再是 9:16。运行前先确认 Chromium 已安装playwright install chromium然后执行python make_screenshot_browser.py如果页面加载很慢可以把wait_until改为load并把wait_for_timeout调大一些。如果页面有懒加载内容也可以在截图前用page.mouse.wheel模拟滚动。6.4 两种方案如何选择场景推荐方案已有商品图片想快速生成统一比例的封面图Pillow 中心裁剪想保留页面原始排版按手机屏幕尺寸截图Playwright 视口截图需要批量处理几百张本地图片Pillow 更快不需要浏览器需要模拟移动端浏览效果Playwright可额外设置user_agent和device_scale_factor如果你的目标是把截图做成一个个正方形或 9:16 封面Pillow 方案更轻量如果你的目标是还原真实浏览器的展示效果Playwright 方案更贴近。7. 常见问题与排查思路7.1 解析结果为空可能原因很多最常见的是保存的 HTML 不完整动态内容没有加载出来。排查时可以先用文本编辑器打开 HTML 文件按 CtrlF 搜索你期望出现的关键词比如goods_id。如果在源码里都搜不到说明你要的数据不在静态 HTML 中需要回到浏览器中把页面滚动到对应位置后再重新另存为。如果 HTML 源码里确实有数据但脚本仍然提取为空可能是字段名不同比如页面里用的是itemId而不是goodsId。解决办法是把正则规则补充完整或者先跑一遍关键词搜索脚本确认实际字段名。问题现象常见原因解决思路goods_id 为空保存时页面未加载完整滚动加载后重新另存为goods_id 为空字段名与正则不匹配用搜索脚本确认页面实际字段名图片地址全是占位图src 是懒加载占位地址读取>img Image.open(src) print(img.format, img.size)如果确实是 WebP 但 Pillow 无法识别可以尝试先安装pillow最新版或者用ffmpeg把 WebP 批量转成 PNG 再做裁剪。还有一类问题是大图内存占用高裁剪前可以考虑先img.thumbnail((2000, 4000))缩小处理范围。7.4 Playwright 截图白屏白屏常见原因有三个本地 HTML 引用的 CSS、JS 是绝对路径或 CDN 地址离线加载时部分样式丢失。wait_untilnetworkidle等待时间过长或过早页面还没有渲染完成。页面在启动时检测到非正常环境跳出阻断逻辑。解决思路是打印更多运行时信息先确认页面是否正常打开title await page.title() content await page.content() print(title, len(content))如果页面整体空白可以增加page.wait_for_selector(img, .content, body)等待关键元素出现。对于引用 CDN 的页面如果只是想截内容区域可以只截图中部区域不要求完整样式。8. 最佳实践与合规建议8.1 工程化组织脚本三个脚本如果直接在命令行里单独执行也能完成工作。但为了让流程更顺建议把公共函数抽成一个common.py比如extract_goods_info、normalize_big_image都放进去各脚本统一 import。以后页面结构变化只需要改一个地方。脚本文件命名尽量按照职责划分parse_goods.py只做商品信息解析extract_images.py只做图片地址提取make_screenshot.py只做截图。这样即使项目变大也不会出现一个文件里堆了所有逻辑的情况。8.2 数据管理经验输出到 CSV 时建议统一用dtype{goods_id: str}读回避免长数字被 Excel 当成科学计数法。CSV 文件用utf-8-sig编码保存兼容 Excel。如果数据量继续变大再考虑迁移到 SQLiteimport sqlite3 conn sqlite3.connect(goods.db) df.to_sql(goods_info, conn, if_existsreplace, indexFalse) conn.close()SQLite 单文件、免安装适合本地小工具存储结构化结果。8.3 合规红线再强调本地解析只是个数据处理方式真正需要注意的反而是后续动作不要把解析出的数据用于商业用途除非你获得了明确的授权。不要批量下载原图到本地重新发布版权归属需要尊重。不要编写自动遍历商品 ID 的脚本去“扫”全站商品。不要绕过平台的接口签名、验证码等机制。如果只是个人选品、学习前端解析、管理自己的素材这套本地方案是稳妥且够用的。8.4 扩展方向懂一点前端开发的同学可以把这套流程做成一个带界面的小工具用 Flask 或 FastAPI 做本地 Web 服务上传 HTML 文件后自动解析前端展示商品 ID、图片列表和 9:16 预览图。后续还能加“批量导出压缩包”“复制 Markdown 格式商品文档”之类的功能本质上都是在本文这套解析和截图逻辑之上做封装。把上面三个脚本串成一个总的main.py入口整个“多多爆款网页解析工具”的闭环就基本成型了。临时处理几十个商品手动执行三个脚本完全够用如果处理量更大再考虑加界面、加数据库、加自动化流程。这里最有价值的不是某一个脚本而是“本地保存页面 离线解析 批量出图”这套思路——它让你在不需要高频请求目标服务器的情况下也能完成大量素材整理工作合规压力小维护起来也轻松。