ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python图片自动化处理实战:requests+re+cv2+PIL批量生成图片

Python图片自动化处理实战:requests+re+cv2+PIL批量生成图片 1. 从一条日常吐槽说起这个项目到底在做什么事情的起因特别简单。女朋友那阵子迷上了给我发各种搞怪表情包和诱惑图——有时候是美食特写有时候是猫猫狗狗的萌照有时候是故意拍得很夸张的自拍。每天晚上手机一震我就知道又来了一张。一开始我还挺配合回个哈哈哈或者好看但时间一长就有点招架不住了毕竟我这边翻来覆去就那么几张存货发过去显得特别没诚意。于是我就琢磨能不能用 Python 搞一个自动化的反击流程她发什么类型的图我这边就自动生成一批同类型、但更有意思的图回敬她。这个项目说白了就是一个图片自动化处理与生成的小工具核心用到了requests做网络请求、re做文本解析、cv2和PIL做图像处理。它解决的问题很具体手动找图、改图太慢批量生成和加工图片的效率太低。适合谁来参考呢我觉得有三类人比较对口。第一类是刚学完 Python 基础语法、想找个真实小项目练手的入门者这个项目涉及文件读写、网络请求、图像处理覆盖面挺全。第二类是平时有批量处理图片需求的人比如做自媒体的、做电商的需要给一批图统一加水印、改尺寸、加文字。第三类就是像我这样纯粹想给生活加点乐子的普通用户。不管你基础怎么样只要跟着把环境配好代码跑起来就能看到实际效果。需要提前说明的是这个项目里所有图片素材都来自公开可访问的网络资源处理过程也完全在本地完成不涉及任何敏感操作。我下面会把整个思路、踩过的坑、以及可以直接抄的代码都摊开讲。2. 整体设计思路为什么这么搭技术栈2.1 需求拆解把反击翻译成技术动作先把生活语言翻译成技术语言。所谓她发诱惑图我反击更多诱惑图拆开来看其实是三个动作获取素材从网络上批量拿到一批图片作为原材料。加工素材给图片加上文字、贴纸、滤镜或者做拼接、缩放让它变得更有梗。输出成品把处理好的图保存到本地方便随时发出去。这三个动作对应到 Python 里就是网络请求、文本解析、图像处理三块。我选的技术栈是requests re cv2 PIL这个组合不是随便凑的每一块都有明确的理由。2.2 为什么是 requests 而不是别的请求库requests是 Python 里做 HTTP 请求最顺手的库没有之一。它的 API 设计非常直观requests.get(url)一行就能拿到响应比标准库的urllib少写一大堆样板代码。对于这个项目来说我需要的是给定一个地址把图片二进制内容拿下来requests的response.content直接返回 bytes写文件的时候open(path, wb).write(response.content)就完事了干净利落。有人可能会问为什么不用aiohttp做异步我的实测结论是这个项目的请求量不大几十到几百张图同步请求完全够用。异步虽然理论吞吐更高但代码复杂度上去了对新手不友好。而且很多图片站点对高频请求有速率限制你异步发太快反而容易被拦得不偿失。2.3 为什么用 re 做解析而不是 BeautifulSoupre是 Python 的正则表达式模块。在这个项目里我用它来从网页源码或者接口返回的文本里提取图片链接。你可能会说解析 HTML 不是应该用BeautifulSoup或者lxml吗这话没错但要看场景。如果目标页面结构规整、标签清晰BeautifulSoup确实更省心。但实际情况是很多图片资源的链接藏在 JavaScript 变量里、藏在 JSON 字符串里或者页面结构经常变。这种时候用正则直接匹配https?://[^\s]\.(jpg|png|gif)这种模式反而更灵活、更抗变化。我试过两种方案最后在这个项目里保留了正则因为它对非标准 HTML的容忍度更高。当然正则也不是万能的。如果页面结构非常复杂我还是会老老实实上BeautifulSoup。工具没有优劣只有合不合适。2.4 cv2 和 PIL 的分工一个管计算一个管美化这两个库都能处理图像但定位不一样我在项目里让它们各司其职。cv2OpenCV 的 Python 绑定擅长的是矩阵运算和计算机视觉任务。图片在它眼里就是一个 NumPy 数组做缩放、裁剪、颜色空间转换、边缘检测这些操作非常快。比如我要把一张图统一缩放到指定尺寸cv2.resize()一行搞定性能也好。PIL现在叫 Pillow擅长的是图像合成和文字渲染。它处理图层、透明度、字体这些更美术的活儿更顺手。特别是给图片加中文文字PIL的ImageDraw.text()配合中文字体文件能精确控制位置、颜色、大小。所以我的分工是cv2 负责尺寸调整、格式转换这类体力活PIL 负责加文字、贴图、合成这类精细活。两者配合覆盖了绝大部分需求。2.5 整体流程图文字版因为不能用图表我用文字把流程串一遍准备一个关键词列表比如猫美食风景。用requests请求图片搜索接口或页面拿到返回内容。用re从返回内容里提取出所有图片链接。遍历链接用requests下载图片二进制数据。用cv2统一图片尺寸用PIL加上文字或贴纸。保存到本地文件夹完成一批反击素材的生成。这个流程跑通之后你每天晚上花两分钟跑一次脚本就能攒下一堆图随时可以发。3. 环境准备与核心库安装新手最容易卡的地方3.1 Python 安装版本选择和安装要点先说 Python 本身。我建议直接用Python 3.9 到 3.11之间的版本。为什么不是越新越好因为有些第三方库对新版本 Python 的支持有滞后你装个 3.13 可能发现某个库还没出对应的 wheel 包编译又编译不过白白浪费时间。3.10 或 3.11 是目前兼容性最好的区间。安装的时候有个细节一定要注意Windows 上安装界面那个Add Python to PATH的勾必须打上。我见过太多新手装完 Python在命令行敲python提示不是内部或外部命令就是因为没勾这个。如果已经装完忘了勾也不用重装手动把 Python 安装目录和它的 Scripts 子目录加到系统环境变量 Path 里就行。macOS 用户相对省心系统自带 Python 3但版本可能偏旧。建议用brew install python3.11装一个独立的避免和系统自带的冲突。Linux 用户用包管理器装就行apt install python3 python3-pip基本够用。3.2 pip 换源解决下载慢的问题装完 Python接下来装库。但直接pip install可能会很慢因为默认源在国外。我的做法是换成国内镜像源速度能快好几倍。命令是这样的pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple这一行设置完之后以后所有pip install都会走这个源。如果你只想临时用一次可以在安装命令后面加-i参数pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple提示换源只是加速下载不影响包的内容和安全性。选清华、阿里、腾讯这些大厂的源都比较稳。3.3 四个核心库的安装命令环境配好后一次性把需要的库装上pip install requests pillow opencv-python numpy这里解释一下为什么多装了个numpy。cv2处理图像时底层依赖 NumPy 数组虽然opencv-python会自动带上 NumPy 依赖但有时候版本会冲突显式装一下更保险。pillow就是 PIL 的现代版本导入的时候还是from PIL import Image。装opencv-python的时候有个坑要提醒不要装成opencv-contrib-python或者opencv-python-headless除非你明确知道自己需要。contrib版本包含额外模块但体积大headless版本没有 GUI 功能如果你后面想用cv2.imshow()预览图片会报错。普通项目用opencv-python就够了。3.4 验证安装是否成功装完之后别急着写代码先验证一下。打开 Python 交互环境依次输入import requests import re import cv2 from PIL import Image print(requests.__version__) print(cv2.__version__)如果都能正常导入并打印出版本号说明环境没问题。如果报ModuleNotFoundError大概率是装到了别的 Python 环境里检查一下你的 pip 和 python 是不是同一个环境。3.5 中文字体准备PIL 加中文的关键这是新手最容易忽略的一步。PIL默认的字体不支持中文你直接draw.text()写中文出来的是一堆方框或者乱码。解决办法是指定一个支持中文的字体文件。Windows 上可以用C:/Windows/Fonts/msyh.ttc微软雅黑或者simhei.ttf黑体。macOS 上可以用/System/Library/Fonts/PingFang.ttc。Linux 上如果没有中文字体需要先装一个比如apt install fonts-wqy-zenhei然后路径一般是/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc。我建议把字体文件复制一份到项目目录下用相对路径引用这样换电脑也不用改代码。代码里这样写from PIL import ImageFont font ImageFont.truetype(msyh.ttc, 40)那个40是字号根据图片大小调整。图片大就调大不然文字会显得很小。4. 核心环节实操从抓图到生成成品4.1 用 requests 抓取图片资源先解决素材来源。我这里不指定具体网站只讲通用方法你可以把逻辑套用到任何允许访问的图片资源上。核心代码结构是这样的import requests 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 } def fetch_page(url): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() return resp.text except requests.RequestException as e: print(f请求失败: {e}) return None这里有几个关键点。第一一定要带 User-Agent。很多站点会检查请求头没有 UA 的请求会被直接拒绝返回 403。加上一个常见浏览器的 UA能绕过大部分基础检查。第二设置 timeout。不设超时的话遇到慢站点你的脚本会一直卡在那里体验极差。10 秒是个合理的值。第三用 raise_for_status()。这个方法会在状态码是 4xx 或 5xx 时抛异常方便你统一处理错误。关于那个热搜词里出现的429 Too Many Requests这是典型的请求频率过高被限流。解决办法有两个一是降低请求频率在每次请求之间time.sleep(1)到time.sleep(3)二是控制单次请求量别一次抓几百张分批来。我实测下来间隔 2 秒左右比较稳既不会太慢也不容易触发限制。4.2 用 re 提取图片链接拿到页面内容后用正则把图片链接抠出来import re def extract_image_urls(html): if not html: return [] pattern rhttps?://[^\\s]?\.(?:jpg|jpeg|png|gif|webp) urls re.findall(pattern, html, re.IGNORECASE) # 去重 return list(set(urls))这个正则的意思是匹配以http://或https://开头中间不包含引号、空格、尖括号以常见图片扩展名结尾的字符串。re.IGNORECASE让扩展名大小写都能匹配因为有些链接是.JPG。set()去重很重要因为页面里同一个图片链接可能出现多次不去重的话你会重复下载同一张图浪费时间。注意正则匹配出来的链接不一定都是有效图片有些可能是缩略图、图标、或者已经失效的地址。所以下载环节必须做异常处理不能假设每个链接都能用。4.3 批量下载图片并保存提取到链接后逐个下载import os def download_images(urls, save_dirimages, max_count50): os.makedirs(save_dir, exist_okTrue) count 0 for i, url in enumerate(urls): if count max_count: break try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() # 根据 content-type 判断扩展名 ext url.split(.)[-1].split(?)[0] if ext.lower() not in (jpg, jpeg, png, gif, webp): ext jpg filename os.path.join(save_dir, fimg_{count:03d}.{ext}) with open(filename, wb) as f: f.write(resp.content) count 1 print(f已下载 {count}: {filename}) time.sleep(1.5) except Exception as e: print(f跳过 {url}: {e}) continue return count这段代码里有几个我踩过坑之后加上的细节。第一max_count限制数量防止脚本失控下载几千张图把硬盘塞满。第二扩展名处理URL 里可能带查询参数比如?sizelarge直接 split 会拿到错误的后缀所以要先去掉?后面的部分。第三time.sleep(1.5)每次下载后歇一下这是避免被限流最有效的手段。第四用 try-except 包住整个下载逻辑单张图失败不影响后面的脚本能一直跑下去。4.4 用 cv2 统一图片尺寸下载下来的图片尺寸五花八门有大有小。为了后续加文字时位置好控制我习惯先统一尺寸。用 cv2 来做import cv2 def resize_image(input_path, output_path, target_width800): img cv2.imread(input_path) if img is None: print(f无法读取: {input_path}) return False h, w img.shape[:2] # 按比例缩放保持宽高比 scale target_width / w target_height int(h * scale) resized cv2.resize(img, (target_width, target_height), interpolationcv2.INTER_AREA) cv2.imwrite(output_path, resized) return True这里的关键是保持宽高比。如果你强行把图片拉成固定尺寸人脸会变形看起来很奇怪。所以先算出缩放比例再按比例算高度。cv2.INTER_AREA这个插值方式在缩小图片时效果最好能减少锯齿和失真。cv2.imread()有个坑如果路径包含中文在 Windows 上可能读不出来返回 None。解决办法是用cv2.imdecode(np.fromfile(path, dtypenp.uint8), cv2.IMREAD_COLOR)来读或者干脆保证所有文件路径都是英文。我一般选择后者省事。4.5 用 PIL 给图片加中文文字这是整个项目最有梗的部分。给图片加上一句吐槽或者配文反击效果直接拉满。代码如下from PIL import Image, ImageDraw, ImageFont def add_text(input_path, output_path, text, font_pathmsyh.ttc, font_size40): img Image.open(input_path).convert(RGB) draw ImageDraw.Draw(img) try: font ImageFont.truetype(font_path, font_size) except OSError: print(f字体加载失败: {font_path}) return False w, h img.size # 计算文字尺寸 bbox draw.textbbox((0, 0), text, fontfont) text_w bbox[2] - bbox[0] text_h bbox[3] - bbox[1] # 居中偏下位置 x (w - text_w) // 2 y h - text_h - 40 # 先画黑色描边再画白色文字提升可读性 draw.text((x, y), text, fontfont, fillwhite, stroke_width2, stroke_fillblack) img.save(output_path) return True这段代码有几个值得说的点。第一convert(RGB)。有些图片是 RGBA 模式带透明通道直接保存成 JPG 会报错转成 RGB 就没事了。第二textbbox计算文字尺寸这样才能精确居中。老版本 PIL 用的是textsize新版本已经废弃了用textbbox更规范。第三描边。stroke_width和stroke_fill给文字加黑色描边这样无论背景是深是浅文字都能看清。这个技巧是我做了一堆图之后才总结出来的纯白文字放在浅色背景上根本看不见。4.6 把整个流程串起来最后写一个主函数把上面的步骤串起来def main(): keyword_url 你的目标页面地址 html fetch_page(keyword_url) urls extract_image_urls(html) print(f共提取到 {len(urls)} 个图片链接) count download_images(urls, save_dirraw_images, max_count30) print(f实际下载 {count} 张) os.makedirs(output_images, exist_okTrue) texts [今天也很想你, 被可爱到了, 这波我赢了, 反击成功] for i in range(count): src fraw_images/img_{i:03d}.jpg if not os.path.exists(src): continue resized fraw_images/resized_{i:03d}.jpg resize_image(src, resized) out foutput_images/final_{i:03d}.jpg add_text(resized, out, texts[i % len(texts)]) print(全部处理完成) if __name__ __main__: main()跑完这个脚本output_images文件夹里就是一批加好文字的成品图随时可以发出去。texts[i % len(texts)]这个写法让文字循环使用避免所有图都是同一句话。5. 常见问题与排查技巧实录5.1 请求被限流怎么办这是最常见的问题表现就是返回 429 状态码或者直接连接被拒。我整理了一个排查表现象可能原因解决办法429 Too Many Requests请求频率过高加大 sleep 间隔到 2-3 秒403 Forbidden缺少请求头补全 User-Agent、Referer连接超时网络或目标站点慢设置 timeout加重试机制返回内容为空被识别为异常请求检查 headers 是否完整下载的图打不开拿到的是错误页而非图片校验 content-type 和文件头关于重试机制我一般用requests的适配器配合urllib3的 Retryfrom requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry(total3, backoff_factor1, status_forcelist[429, 500, 502, 503, 504]) session.mount(http://, HTTPAdapter(max_retriesretry)) session.mount(https://, HTTPAdapter(max_retriesretry))backoff_factor1意味着重试间隔会指数增长1秒、2秒、4秒这样既给了服务器喘息时间又不会让脚本卡太久。5.2 中文显示成方框这个问题 90% 是字体没配对。检查三点字体文件路径对不对、字体文件本身是否支持中文、字号是不是太小导致渲染异常。我建议直接用系统自带的中文字体别去网上随便下有些字体文件损坏或者不完整加载了也没用。还有一个隐蔽的坑字体路径里的反斜杠。Windows 路径是C:\Windows\Fonts\msyh.ttc但在 Python 字符串里\是转义字符你得写成rC:\Windows\Fonts\msyh.ttc或者用正斜杠C:/Windows/Fonts/msyh.ttc。我见过有人因为这个卡了半小时。5.3 cv2 读取图片返回 None除了前面说的中文路径问题还有两个原因。一是文件本身损坏下载的时候没下完整。二是格式不支持比如 webp 格式在某些老版本 OpenCV 里读不了。解决办法是升级opencv-python到较新版本或者用 PIL 先转成 JPG 再用 cv2 处理。5.4 内存占用过高如果你一次处理几百张高分辨率图片内存可能会爆。我的做法是处理完一张就释放一张不要把所有图片都读进内存再统一处理。用del img显式删除或者干脆用循环逐个处理让 Python 的垃圾回收及时生效。另外下载阶段就限制图片尺寸别下那种几千万像素的原图。5.5 脚本跑一半崩了这种情况通常是某个环节没做异常处理。我的经验是网络请求、文件读写、图像处理这三个环节每一个都要用 try-except 包住。宁可跳过一张图也不要让整个脚本挂掉。另外加个日志记录把失败的 URL 和原因写到一个文件里方便事后排查。6. 几个提升效果的小技巧6.1 文字位置和样式的讲究文字别老是放正中间那样太呆板。我一般放在底部偏上或者顶部留白处根据图片内容调整。如果图片主体在下方文字就放上面主体在上方文字就放下面。颜色上白色描边黑字是最保险的组合几乎适配所有背景。字号也有讲究。图片宽度 800 像素的话字号 40 左右比较合适。太小看不清太大又显得突兀。你可以先跑一张看看效果再批量处理。6.2 批量处理时的命名规范我强烈建议用零填充的序号命名比如img_001.jpg、img_002.jpg。为什么因为这样文件在文件夹里是按顺序排列的你一眼就能看出处理到哪了。如果用img_1.jpg、img_10.jpg、img_2.jpg这种排序会乱掉找文件很痛苦。6.3 素材分类存放别把所有图都堆在一个文件夹里。我习惯按日期或者按主题分文件夹比如2024-01-15/或者cats/。这样时间长了也不会乱想找某类图直接进对应文件夹。6.4 控制单次生成数量这个项目最大的诱惑就是多生成点但我的经验是每次生成 20 到 30 张就够了。生成太多你根本发不完而且质量参差不齐反而显得没诚意。少而精比多而滥强。7. 关于合规和边界的几点说明做这类项目有几个边界必须守住。第一素材来源要合法。只抓取公开可访问、允许使用的资源不要碰有明确版权限制的内容。第二不要对目标站点造成压力。控制请求频率别用多线程疯狂轰炸这是基本的网络礼仪。第三生成的内容要健康。这个项目的初衷是给生活加点乐子不是传播不良信息所有文字和图片都要符合公序良俗。我在实际做的时候会把抓取范围限制在几个固定的、结构简单的页面上不追求全网抓取。这样既降低了技术复杂度也避免了不必要的麻烦。技术是工具怎么用取决于人守住底线才能玩得长久。8. 后续可以怎么扩展这个项目跑通之后其实还有很多可以玩的方向。比如接入图像识别用 cv2 判断图片的主色调自动选择对比度最高的文字颜色。再比如做一个简单的图形界面用 tkinter 或者 PyQt 把参数做成可调的不用每次改代码。还可以加一个定时任务每天固定时间自动跑一次攒够一周的素材。如果你对图像处理感兴趣可以研究一下 cv2 的滤镜效果给图片加上复古、黑白、模糊等风格。PIL 那边也可以玩图层叠加把多张图拼成一张。这些扩展都不难核心逻辑和现在这套是一样的。我个人在实际操作中的体会是别一上来就追求功能大而全先把最小可用的流程跑通再一点点加东西。我第一版代码只有下载和加文字两个功能跑通之后才慢慢加上尺寸统一、异常处理、批量命名这些。这样每一步都有正反馈不容易半途而废。踩过几次坑之后你会发现真正花时间的不是写代码而是处理各种边界情况和异常而这些恰恰是让脚本从能跑变成好用的关键。
返回列表