GrabImage.py:一个用Python批量抓取网页图片的命令行工具
简介GrabImage.py是一份面向工业相机图像采集场景的Python脚本适合质量检测、自动化产线监控、精密测量等方向的开发者用于快速掌握OpenCV与工业相机硬件之间的交互方法。脚本以VideoCapture模块为核心完整覆盖相机设备初始化、逐帧读取与ret状态判断、曝光/增益/白平衡等参数调优、图像实时显示与本地保存以及结束时的资源释放等关键环节既可作为理解工业相机编程的入门模板也能作为后续图像处理与算法开发的起点。资源以rar压缩包提供内部仅1个py文件体积约4KB结构清晰、便于直接阅读或修改复用。目前已有2763人学习浏览适合具备基础Python语法、正打算将视觉采集能力接入具体项目的开发者参考使用。GrabImage.py一个把“手动存图”变成“一行命令”的Python小工具我最早写这个脚本纯粹是烦了——运营同事隔三差五找我帮忙下载某个页面上的一堆图片一张张右键另存为一次两次忍了十次八次实在顶不住。后来我花了不到一个下午写了GrabImage.py把“输入网址、批量抓取、自动命名、按需过滤”全塞进一个命令行工具里。现在团队里谁要批量取图自己跑一句python GrabImage.py -u https://xxx -o ./images就行再也没人来找我点鼠标了。这篇博文就分享一下这个脚本的设计思路和完整实现。虽然名字叫GrabImage.py但它解决的问题很通用网页里有大量图片需要批量下载时怎样用一个轻量脚本稳定、安全、可控地抓下来。适用场景包括产品图片素材收集、竞品页面视觉参考、公众号配图归档、以及任何“我知道图在哪但我懒得一张张点”的情况。适合有Python基础、想快速写一个实用爬虫小工具的读者也适合完全没写过爬虫、但想从零做个能用的脚本的新手。1. 整体设计与思路拆解1.1 为什么选Python做批量抓图工具选择Python不是因为它“简单”而是因为它在爬虫和文本处理这个领域积累太厚了。requests做HTTP请求、BeautifulSoup解析HTML、Pillow处理验证图片有效性这三个库足够覆盖一个抓图工具的全部需求。更重要的是Python的标准库concurrent.futures自带线程池几行代码就能实现多线程下载不用额外引第三方框架。再配合argparse做命令行参数整个脚本可以不依赖任何GUI环境服务器上、Windows里、Linux终端都能跑甚至打好包扔给不懂代码的同事用也没问题。我见过不少人一上来就上Scrapy、Selenium但对“批量下载某个页面里的图片”这种轻量任务来说完全是杀鸡用牛刀。Scrapy适合大规模分布式采集Selenium适合对付动态渲染页面但它们的安装成本、学习成本、运行稳定性都会反噬原本很简单的小任务。正确的做法是先判定任务边界再选工具而不是先选工具再强行套任务。GrabImage.py的定位就是“小而可靠”所以它只解决静态页面或API返回内容里的图片抓取遇到复杂的动态页面再考虑升级方案。1.2 脚本的功能边界与架构分层设计的时候我给自己定了三条铁律一是只能输入URL即可运行参数越少越好二是抓取过程必须能随时中断、可重试不会因为某张图片失败就整体崩溃三是输出目录清晰文件名可读不能出现一堆“看不到名字的乱码图”。基于这三条铁律脚本拆成了四层参数解析层接收URL、输出目录、图片类型过滤、递归深度、线程数、延迟时间等参数页面获取层用requests请求目标页面处理headers、超时、编码问题图片提取层从HTML中解析出所有候选图片URL去掉重复项、外链排除项下载执行层多线程下载、文件名清洗、错误重试、日志输出每一层之间的依赖只有一个函数调用没有隐藏耦合。这样做的原因是后期维护和排查问题的时候能直接定位到某一行而不是在乱成一团的代码里翻找。写完到现在我改了四五版每次都是只动一个层其他层完全不受影响这个分层设计帮了大忙。2. 核心细节解析与实操要点2.1 图片链接提取的两个关键细节抓图脚本的核心不是“下载”而是“从HTML里准确定位图片地址”。我发现很多初学者栽在这里所以专门展开说一下。第一个细节是图片URL的类型远比你想的多。常规的img src...只是一个起点现在的页面里至少还有以下几种情况懒加载图片img>import argparse import concurrent.futures import os import re import time from urllib.parse import urljoin, urlparse, unquote import requests from bs4 import BeautifulSoup DEFAULT_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, } IMG_ATTRS [src, data-src, data-original, data-lazy] def parse_args(): parser argparse.ArgumentParser(description批量抓取页面图片的小工具) parser.add_argument(-u, --url, requiredTrue, help目标页面URL) parser.add_argument(-o, --output, default./images, help图片保存目录) parser.add_argument(-t, --threads, typeint, default5, help下载线程数) parser.add_argument(-d, --delay, typefloat, default0.3, help每张图下载后的延迟秒数) parser.add_argument(--ext, defaultjpg,png,jpeg,gif,webp, help允许的图片扩展名逗号分隔) parser.add_argument(--timeout, typeint, default10, help请求超时时间) return parser.parse_args() def fetch_html(url, timeout10): resp requests.get(url, headersDEFAULT_HEADERS, timeouttimeout) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def extract_image_urls(html, base_url): soup BeautifulSoup(html, html.parser) candidates set() def add_url(url): if not url: return url unquote(url.strip()) if url.startswith(data:) or url.startswith(blob:): return full_url urljoin(base_url, url) candidates.add(full_url) for img in soup.find_all(img): for attr in IMG_ATTRS: add_url(img.get(attr)) srcset img.get(srcset) if srcset: for part in srcset.split(,): add_url(part.strip().split( )[0]) for tag in soup.find_all(attrs{style: True}): bg_matches re.findall(rurl\([\]?(.*?)[\]?\), tag[style]) for bg in bg_matches: add_url(bg) return candidates def is_valid_ext(url, allowed_set): path urlparse(url).path.lower() return any(path.endswith(ext) for ext in allowed_set) def sanitize_filename(url): path urlparse(url).path filename os.path.basename(path) filename re.sub(r[^A-Za-z0-9._-], _, filename) if not filename: filename fimage_{hash(url) 0xffffffff}.jpg if not os.path.splitext(filename)[1]: filename .jpg return filename在extract_image_urls函数里我同时检查了src、>def download_one(url, output_dir, allowed_ext, delay, timeout): if not is_valid_ext(url, allowed_ext): return False, None filename sanitize_filename(url) filepath os.path.join(output_dir, filename) if os.path.exists(filepath): return True, filename for attempt in range(3): try: resp requests.get(url, headersDEFAULT_HEADERS, timeouttimeout, streamTrue) resp.raise_for_status() with open(filepath, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk) time.sleep(delay) return True, filename except requests.RequestException as e: if attempt 2: time.sleep(1) else: return False, filename return False, filename def download_all(url, output_dir, threads, delay, allowed_ext, timeout): os.makedirs(output_dir, exist_okTrue) html fetch_html(url, timeout) image_urls extract_image_urls(html, url) print(f[*] 页面解析完成共找到 {len(image_urls)} 个候选图片链接) success 0 failed [] with concurrent.futures.ThreadPoolExecutor(max_workersthreads) as executor: future_map { executor.submit(download_one, img_url, output_dir, allowed_ext, delay, timeout): img_url for img_url in image_urls } for future in concurrent.futures.as_completed(future_map): ok, filename future.result() img_url future_map[future] if ok: success 1 print(f[] 成功: {filename}) else: failed.append(img_url) print(f[-] 失败: {img_url}) print(f[*] 下载完成成功 {success} 张失败 {len(failed)} 张) if failed: with open(os.path.join(output_dir, failed_urls.txt), w, encodingutf-8) as f: f.write(\n.join(failed)) print(f[*] 失败链接已保存到 {os.path.join(output_dir, failed_urls.txt)}) if __name__ __main__: args parse_args() allowed_ext {item.strip().lower() for item in args.ext.split(,)} download_all(args.url, args.output, args.threads, args.delay, allowed_ext, args.timeout)这里有两个容易踩的点我在注释之外多提两句。第一resp.iter_content(chunk_size8192)是流式下载对于大图非常关键如果把整个响应读进内存一不留神就能把内存吃满。第二失败链接列表保存到独立文件里这样下载完如果发现失败了几张直接再跑一遍脚本针对失败名单补下就行不用重新扫描页面。运行方式很简单# 基础用法 python GrabImage.py -u https://example.com # 指定输出目录、线程数并限制只抓png和jpg python GrabImage.py -u https://example.com -o ./photos -t 8 --ext png,jpg # 加大延迟降低请求频率适合对请求敏感的站点 python GrabImage.py -u https://example.com -d 1.03.2 图片类型过滤的参数计算--ext参数默认是jpg,png,jpeg,gif,webp这五种是网页图片的绝对主流。你可能会问为什么还要特别设置这个参数因为有些页面会引用一堆图标、按钮素材、甚至svg这些图片抓下来对大多数场景毫无价值反而占据下载时间和存储空间。所以我默认允许的类型就五种如果你在抓一个全是webp图集的网站可以单独指定--ext webp其他格式一律跳过。另外需要留意一个参数细节--timeout我一般推荐设成10秒。设太短比如3秒稍微大一点的图就容易超时设太长比如30秒一旦遇到一个迟迟不响应的连接会拖住整个下载队列。测试下来10秒是一个比较稳的平衡点。3.3 多线程与延迟的配合逻辑线程数和延迟这两个参数是配合使用的。并发数高、延迟小总下载速度快但请求频率会高并发数低、延迟大总下载速度慢但几乎不给服务器造成压力。我给的默认组合是-t 5 -d 0.3实测针对大多数内容网站都没有问题下载几百张图在可接受的等待时间内完成也不会触发反爬。如果你的目标是下载自家服务器上的图片或者你明确知道对方网站对爬虫宽容就把线程调高到10、延迟降到0.1速度会快很多。反之如果遇到下载过程中开始频繁出现403、503错误大概率是请求频率过高被限流了这时候先把线程降下来延迟涨上去别硬试。4. 常见问题与排查技巧实录4.1 遇到403 Forbidden怎么办403是抓图最常遇到的错误原因通常是请求头不完整或者被服务器识别出非浏览器请求。排查顺序我一般是这样确认请求头里带了User-Agent和Referer。Referer直接设成目标页面的URL很多图床只认这一条的。检查Cookie是否需要登录才能访问图片。如果是登录态的图片资源脚本里需要增加一个--cookie参数把浏览器里复制的Cookie字符串带上否则即使请求头伪装得再像也拿不到数据。如果以上都试过了仍然403换个User-Agent试试。有些站点针对特定UA做了拦截换一个主流浏览器的最新UA字符串往往能解决。实际过程中我发现大部分403问题出在Referer上其次才是CookieUA反而是最后才需要调整的。这是因为很多CDN和对象存储服务比如各种图床的防盗链机制主要检查的就是Referer是否来自允许的域名。4.2 抓下来的图片打不开或全是空文件首先检查下载的文件大小用os.path.getsize()或者直接在文件管理器里看属性。如果大量文件是0字节或者只有几KB基本可以确定是下载逻辑有问题。常见的可能性有两个一个是resp.iter_content被错误地用了多次。如果你在流式下载前先访问过resp.content再进行流式读取后面就读不到数据了因为响应流已经被消费掉了。我这个脚本里如果只检查resp.status_code而不读resp.content就能避开这个坑。另一个是服务器对不支持Range请求的文件返回了完整响应或者某些图片URL跳转到了错误页面。比如服务器把404请求重定向到了一个自定义HTML页面而你没有检查Content-Type是image/*还是text/html就按图片保存了这样保存下来的文件当然打不开。在download_one函数里加一个响应头的Content-Type判断如果不是image/开头就跳过能有效挡掉这种无效下载。4.3 页面图片是动态加载的脚本抓不到如果你用requests直接抓HTML发现提取的图片远少于浏览器里看到的图片十有八九是页面用了JavaScript动态渲染图片URL。请求到的源代码里图片地址是在JS执行之后才生成的requests拿到的还是渲染前的结果。这种情况有两条路我建议按成本从低到高尝试查看页面是否有内置JSON数据。很多现代页面会把数据以script typeapplication/json的形式内嵌图片URL就藏在里面用正则或JSON解析可以直接提取。如果确实找不到再考虑升级为Selenium或Playwright这类无头浏览器工具。但要注意引入它们意味着脚本要依赖浏览器环境打包体积和部署复杂度都会上去。对于GrabImage.py这个定位来说我更倾向于控制任务边界它能处理静态页面和嵌入式数据动态渲染的页面就交给更专业的方案没有必要把所有人都拖进重型依赖里。4.4 多线程下载时日志交错看不清楚默认情况下多线程的print输出会混在一起容易看着乱。我在脚本里用的是最朴素的print方式但每行日志都带有明确的文件名或URL开头靠这个前缀来辨识是哪张图的状态。如果你对日志可读性要求更高可以用threading.Lock包一下print或者引入logging库按线程打日志排错体验会更好。对于这种小工具我不会建议过度设计但至少print里的信息要足够定位问题和任务进度。5. 实战案例从页面批量抓取商品主图纸上谈兵没意思我用一个真实场景走一遍完整流程。假设我们要从某个商品详情页里把所有主图包括轮播图里的隐藏图全部抓下来。第一步确认页面地址和输出目录python GrabImage.py -u https://example.com/product/10086 -o ./product_imgs -t 8 --ext jpg,png第二步观察脚本输出。正常情况下会看到类似下面的内容[*] 页面解析完成共找到 34 个候选图片链接 [] 成功: 10086_1.jpg [] 成功: 10086_2.jpg ... [-] 失败: https://img.example.com/watermark.jpg [*] 下载完成成功 33 张失败 1 张 [*] 失败链接已保存到 ./product_imgs/failed_urls.txt第三步检查失败列表。发现失败的是水印图本来就不需要跳过即可。如果失败的是主图打开failed_urls.txt里的URL在浏览器里访问一下看是链接失效还是被防盗链拦了由此确定下一步的应对方案。这里有个小技巧下载完成的图片我用Pillow批量做了一次尺寸和格式校验把所有非图片文件或损坏文件统一删掉重下。这个校验可以写成一段几行的循环from PIL import Image import os for f in os.listdir(./product_imgs): filepath os.path.join(./product_imgs, f) try: with Image.open(filepath) as img: print(f, img.size, img.format) except Exception: os.remove(filepath) print(f, 损坏已删除)这样就能保证最终拿到的素材都是有效的。虽然在GrabImage.py脚本主体里没有内置这个校验但把它作为后续处理步骤和抓图脚本搭在一起整个流程就很完整了。6. 后期扩展与经验沉淀GrabImage.py目前已经在我电脑上服役了小半年除了解决同事的问题我自己也在这个基础上做了几处扩展分享出来供你参考。一个扩展是增量下载。已经下载过的图片URL会记录在本地的download_history.json里再次运行同一URL时会跳过已存在的文件只下载新增的图片。对于每天更新的网站专题页来说这个功能非常实用把脚本甩进Crontab定时任务里就能每天自动把新图同步到本地。另一个扩展是修改输出文件名规则。默认的文件名取自URL但有些场景下希望按“页面标题序号”来命名。只要在download_one之前把filename用页面标题前缀加上索引就能改得很灵活。这个改动大概只涉及两三行代码但对后续人工整理素材的效率提升是实打实的。关于打包成exe我已经测试过用pyinstaller -F GrabImage.py把脚本打包成单个可执行文件在没有Python环境的同事电脑上也能直接运行。这里有一个坑要先提醒你打包含第三方库requests和bs4时生成的exe体积会在10MB到20MB之间启动也会慢一些但胜在分发方便不用让对方折腾Python环境。根据我的个人经验像GrabImage.py这种小工具最重要的是“做完就用、用起来顺手”。不要一上来就想做成一个完美的框架因为很多所谓的“完美功能”只有在真实使用中才会知道值不值得做。比如我加的重试机制、失败链接记录、增量下载全是在实际被人催着补图的过程中一点点加出来的。最后分享一个从这次开发里沉淀下来的习惯写爬虫工具一定要保留运行日志和失败清单。这看起来是个很小的细节但等你下一次运行同一个脚本去抓新数据、或者隔了两个月再回来调试的时候当时的输出和失败信息是你最可靠的debug入口。没有这几行输出你面对的可能就是一整目录的未知文件和一屏幕的报错重新定位问题的时间比当时写这个脚本还长。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →