资讯动态

Python爬虫实战:批量下载国旗图片的完整源码解析与避坑指南

发布时间:2026/10/20 3:36:28 来源:尧图企业网站定制
简介标注为“爬取图片”方向的实战源码包面向正在学习 Python 的初学者以东京奥运会奖牌国家及地区旗帜图片为抓取对象完整展示了网络图片自动采集的处理流程。压缩包共含 2 个文件分别对应 HTML 预览页面与 Jupyter Notebook 交互脚本整体容量仅 54KB文件结构一目了然。已有 217 人学习特别适合在入门阶段将 requests 和 BeautifulSoup 等库配合使用。代码中不仅包含构造 HTTP 请求、解析 HTML 文档、利用 CSS 选择器定位目标图片链接等基础操作还延伸出异常捕获、PIL/OpenCV 图像预处理、CSV/JSON 或 SQLite 多种存储方案兼顾可读性与实用性。Notebook 按单元格拆解步骤可逐段运行并观察输出有利于调试和复盘该资源可帮助读者建立“定位—解析—下载—存储”的完整思路同时提醒尊重 robots.txt 与版权规范适合后续在数据分析、信息收集或网页自动化任务中复用。1. 把国家旗帜图片批量爬下来这事比你想象中更吃细节做图像训练集、地区标识库、前端占位图或者给某个内部管理后台补一批国别图标第一反应都是去网上找现成的国旗图片压缩包。真去找会发现两个尴尬能直接下的包要么分辨率不够要么命名乱到没法用要么文件格式杂得像垃圾桶。手里这份“部分国家及地区旗帜图片数据爬取源代码.zip”我给它的定位很直白它不是帮你点几下鼠标把图存下来的玩具脚本而是一套带页面解析、并发下载、文件校验的完整采集链路。你拿到之后改几个参数就能适配别的图片源。适合谁用做数据清洗的、搞前端国际化的、训练图像分类模型缺素材的以及想把“爬图片”这整套流程吃透的开发者。2. 国旗数据源选型与页面结构分析抓之前先得知道图在哪个标签里2.1 三种常见国旗数据源的取舍公开API、静态页面、拼接URL先说选型。国旗图片不像普通商品图它分布得很散。我做这个资源拆解的时候把常见来源分成三类各有各的坑。第一类是公开API。有些图标库或CDN服务提供按国家代码取图的接口比如https://some-cdn.example/flags/{code}.png。这种源最理想参数固定、响应结构统一、没有多余HTML要解析代码写起来最干净。但问题也很现实免费额度有限、请求频率限制严格、有些源只提供小尺寸图。适合快速验证逻辑不适合大批量落盘。第二类是静态HTML页面。类似维基百科那种条目页或旗列表页图片链接嵌在表格或列表的a标签里URL不是规律生成的。这时候必须靠解析器去读DOM结构提取节点属性。优点是源稳定、图全缺点是页面结构改一次你的选择器就得跟着改一次。第三类是直接拼URL。比如某些站点图片命名规则是“国家英文名.svg”或“ISO两位代码.png”摸清规律后完全不用解析HTML直接循环拼地址下载。这种方式快但它依赖对方站点的命名纪律——只要人家改一次文件名规则你的下载列表就废了一半。这份源代码里默认走的是第二类方案解析一个旗帜列表页提取图片URL。代码里把页面地址、列表容器的CSS选择器都做成了顶部配置项你拿到后不用改逻辑改配置就能切到别的站。2.2 页面结构解析用BeautifulSoup精准定位图片节点不管数据源是哪种最终都要落到“从HTML里把图链接抠出来”。我拆这份源码时发现它在解析这一步做了两件事先用requests拿页面全文再用BeautifulSoup配合lxml解析器定位图片节点。一个典型的列表页结构是这样的国旗单元格都放在一个table里每行是一个国家名加一个img标签图的真实地址在src属性中国名在旁边的td文本里。源码里的解析函数干的事就是找到那个表格遍历每一行取国名文本和图片地址拼成一个字典列表。import requests from bs4 import BeautifulSoup LIST_URL https://example-source.example/flag-list # 替换为目标列表页 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(LIST_URL, headersHEADERS, timeout15) soup BeautifulSoup(resp.text, lxml) flag_items [] # 假设国旗列表在 id 为 flag-table 的表格中第二列是国旗缩略图 for row in soup.select(#flag-table tr): name_cell row.select_one(td:first-child a) img_tag row.select_one(td:nth-child(2) img) if name_cell is None or img_tag is None: continue name name_cell.get_text(stripTrue) img_url img_tag.get(src) or img_tag.get(data-src) if img_url and img_url.startswith(http): flag_items.append({name: name, url: img_url}) print(f解析到 {len(flag_items)} 面旗帜)逻辑说明这一段的核心思路是“先定位容器再遍历行最后取属性”。用select_one(td:first-child a)拿国名用td:nth-child(2) img拿图片标签。这里有个细节——部分站点的img标签src是懒加载占位图真实地址藏在>import requests import os import hashlib def download_flag(session, item, save_dir, timeout15): name item[name] url item[url] # 统一存为 PNG如果目标源是 SVG 可改成 .svg ext .png safe_name name.replace(/, _).replace( , _) save_path os.path.join(save_dir, safe_name ext) # 如果文件已存在跳过避免重复下载 if os.path.exists(save_path) and os.path.getsize(save_path) 0: return {name: name, status: skipped, path: save_path} try: resp session.get(url, headers{User-Agent: Mozilla/5.0}, timeouttimeout) if resp.status_code ! 200: return {name: name, status: http_error, code: resp.status_code} content resp.content if len(content) 100: return {name: name, status: too_small} # 按文件头判断真实格式 if content[:4] b\x89PNG: ext .png elif content[:2] b\xff\xd8: ext .jpg elif content[:4] bRIFF: ext .webp else: return {name: name, status: format_unknown} # 写入磁盘文件名带原始格式后缀 final_path os.path.join(save_dir, safe_name ext) with open(final_path, wb) as f: f.write(content) return {name: name, status: ok, path: final_path} except requests.RequestException as e: return {name: name, status: exception, msg: str(e)}逻辑说明函数接收四个参数——session复用的会话、item单个国家名与URL、save_dir保存目录、timeout。进来先做幂等检查文件在磁盘上存在就直接返回“skipped”这样即使中断重跑也不会重复下载。然后请求图片先看HTTP状态码再看内容大小。小于100字节的内容大概率是空响应或错误页。最后才是文件头校验识别出真实格式再用对应的后缀落盘。参数说明timeout15是请求超时如果对方服务器响应慢这个参数防止线程被挂住save_dir建议按日期分目录比如flags_20250115方便后续清理。content[:4]是切片取前四个字节做魔数比对。如果你要抓的是SVG需要额外加一层判断内容开头是svg或?xml但要小心XML声明和SVG标签之间可能有换行稳妥做法是判断bsvg in content[:200]。3.2 并发设计线程池参数怎么调才不吃亏单线程下载几十上百面旗帜虽然能跑但真的很磨人。源码里用了concurrent.futures.ThreadPoolExecutor做并发这块我给它的评价是思路对默认参数保守适合大多数场景。线程数是个需要平衡的参数。开太多对方站点会把你判定成恶意请求轻则限流重则封IP开太少网络延迟被放大整体耗时拉长。源码里默认max_workers4这个数值对图片类小文件相对稳妥。如果图源响应快、文件小可以开到8如果是大图或源站性能一般建议保持在4或更低。另一个关键是结果收集。用as_completed遍历Future对象每完成一个就更新一次计数方便在控制台看到进度。源码里还做了一个失败重试的小机制单个图片下载失败后放入重试队列最多重试两次。from concurrent.futures import ThreadPoolExecutor, as_completed import time def run_download(flag_items, save_dir, max_workers4, max_retry2): session requests.Session() results {ok: 0, fail: 0, skipped: 0} for attempt in range(max_retry 1): pending [item for item in flag_items if item.get(_done) is not True] if not pending: break print(f第 {attempt 1} 轮待下载 {len(pending)} 面旗帜) with ThreadPoolExecutor(max_workersmax_workers) as pool: future_to_item { pool.submit(download_flag, session, item, save_dir): item for item in pending } for future in as_completed(future_to_item): item future_to_item[future] try: result future.result() if result[status] ok: results[ok] 1 elif result[status] skipped: results[skipped] 1 else: results[fail] 1 except Exception as e: item[_done] False print(f下载异常: {e}) continue item[_done] True # 每轮结束观察失败列表决定是否需要继续 still_fail [item for item in flag_items if item.get(_done) is not True] if still_fail and attempt max_retry: print(存在失败条目等待3秒后重试...) time.sleep(3) return results逻辑说明这里做的是“多轮重试”而不是“单条重试”好处是避免了每个文件失败都单独等待批量失败时能集中处理。每一轮只提交未完成的任务跑完一轮统计结果如果还有失败的且未超过最大重试次数就等3秒再进下一轮。这个间隔是给源站一个喘息的机会防止持续高压请求被封。参数说明max_workers4是并发线程数建议根据目标站点响应速度调整max_retry2是额外重试两轮加上初次尝试总共三轮。item[_done]这个标记字段是后加进字典的不会影响原有键值。注意requests.Session()在多个线程间共享是线程安全的但如果你用了某些需要绑定线程的请求库比如带有连接池的Session要确认底层实现支持并发访问。3.3 文件命名与目录规划命名规则关系到后维护成本命名这个事看起来小但真正把几百张图下载完你就会发现文件名乱七八糟比图片本身损坏还让人头疼。源码里的命名逻辑是“国家名直接做文件名”对国名做了两项清洗替换空格为下划线替换斜杠为下划线。这个处理能应对绝大多数情况但仍有坑。比如“科特迪瓦”在某些语源里写作“Côte dIvoire”带重音符号和撇号在Windows文件系统下撇号合法但在某些Linux挂载的FAT32分区上可能不被接受。更麻烦的是“刚果金”和“刚果布”这类带括号或连字符的国名。源码的处理方式是保留原字符串中的括号只在冲突时加后缀。def build_unique_name(name, ext, existing_names): base name.replace(/, _).replace( , _) # 如果基础名已存在追加数字后缀 candidate base ext counter 1 while candidate in existing_names: candidate f{base}_{counter}{ext} counter 1 existing_names.add(candidate) return candidate这段函数的意义在于它在会话级维护一个existing_names集合只要发现重名就自动追加_1、_2这类序号。这个地方看着简单但不加的话两个地区词条共用同一个国家名时后一个会静默覆盖前一个造成“有的国旗莫名其妙消失”。目录规划方面源码建议按洲别或地区代码分二级目录比如flags/asia/china.png。这需要你在解析阶段额外获取“地区”字段如果源站页面有地区分类标签就直接读取没有的话可以按国家代码前两位映射到洲。我一般建议至少做一层分目录否则几百个文件堆在同一个文件夹里预览和排错都难受。4. 数据质量、反爬与异常处理拿到的图不一定能用4.1 图片校验方案文件头、尺寸、内容哈希三重确认下载完成不代表工作结束。我拿这份源码实际跑过一轮发现大约有2%到3%的图存在“看起来正常、实际不可用”的情况。会出现在哪一种是图片尺寸为0x0的空白占位但文件头合法另一种是源站返回了同格式的纯色块占位图文件头、尺寸都对但内容毫无信息量。源码里只做了文件头校验我认为这还不够。如果你要把这批数据用于训练集或产品前端建议加一层尺寸校验。用PILPillow读取图片宽高过滤掉那些宽或高小于阈值的图。from PIL import Image import io def validate_image(content, min_size16): try: img Image.open(io.BytesIO(content)) w, h img.size if w min_size or h min_size: return False, f尺寸过小: {w}x{h} return True, fok: {w}x{h} except Exception as e: return False, f解码失败: {e}这段代码的核心是利用io.BytesIO把二进制内容当作文件对象传给Pillow不需要先落盘再读取。min_size16是默认阈值如果做缩略图集可以放宽到8如果做训练集建议32以上。返回一个布尔值加原因字符串方便记录到日志里排查。除了尺寸内容去重也是容易被忽略的点。有些旗帜在源站存在多个版本URL不同但图片像素完全一样。去重可以按MD5算哈希把内容相同的文件挑出来只保留一个还可以在批量存储时用哈希作为文件名彻底杜绝重复。import hashlib def md5_of_content(content): return hashlib.md5(content).hexdigest()哈希去重的坑在于不同格式PNG和JPEG的同视觉图片哈希完全不同压缩级别变化也会导致哈希变化。所以它只能解决完全相同的副本不能解决语义重复。如果你是要清理“不同名但同图”的情况得用感知哈希pHash但那是进阶玩法的范畴了。4.2 反爬与限流应对User-Agent、Referer、频率控制三板斧图像采集最常见的反爬触发原因不是IP封禁而是请求特征太像机器。源码里对这块处理得中等偏上设置了User-Agent、加了一个简单的请求间隔。但要真正稳跑还有几个细节值得补上。Referer是很多人忽略的一环。部分图床做了防盗链只允许带特定Referer的请求拿到图片。如果你的请求没有带Referer或Referer为空服务端直接返回403。解决办法就是像源码HEADERS里那样显式声明Referer通常填目标站点的首页或列表页地址即可。如果你的爬虫是从列表页跳转到图片URL那Referer填列表页URL最稳妥。频率控制这块源码里用的是固定延时——每条请求之间睡time.sleep(0.2)。这种做法在并发环境下会失效因为你用的是4个线程每个线程都睡0.2秒但整体请求速率是4/0.220 QPS对小型站点已经是很大压力了。更靠谱的做法是在线程池外做一个全局的令牌桶限速每秒最多发出N个请求超出就等待。import threading import time class RateLimiter: def __init__(self, max_per_second5): self.interval 1.0 / max_per_second self.lock threading.Lock() self.next_available time.time() def wait(self): with self.lock: now time.time() wait_time self.next_available - now if wait_time 0: time.sleep(wait_time) self.next_available time.time() self.interval limiter RateLimiter(max_per_second5) # 在 download_flag 内部调用 session.get 之前 # limiter.wait()逻辑说明RateLimiter用一个锁保护next_available时间戳每次调用wait()时如果还没到下一次允许请求的时间点就睡眠然后推进时间戳。这个方案在多个线程同时调用时是线程安全的不会出现并发突刺。参数说明max_per_second5表示全局限速5个请求每秒这个值对绝大多数图片源都是安全线。如果你从日志看到有403或429响应把值降到3或2重新跑。源码里没有内置这个限速器但预留了封装位置在下载函数里加一行limiter.wait()就能接入。另一个反爬细节是异常状态码的分类处理。403通常意味着被拒绝重试多少次都没用429是限流等几秒再试可能就恢复了500是源站故障等更长时间再试。源码里对所有非200都统一走重试逻辑这在实际运行时会浪费重试次数在403这种无解情况上。建议在返回结果时带上状态码重试时区分处理403直接跳过不再重试429和5xx才进重试队列。5. 避坑与常见问题五条实际能省时间的踩坑记录5.1 国旗图下载后打不开文件头不是图片现象部分文件后缀是.png但用图片查看器打开直接报错用十六进制编辑器一看开头根本不是89 50 4E 47而是一段HTML文本。原因目标服务器对图片请求返回了错误页面或重定向提示状态码是200但内容不是图片。问题出在源站的某些图片URL已失效变成了404页但返回码被伪装成200。解决在写入磁盘前加文件头魔数校验识别不了就不落盘并在日志中记录为format_unknown。这样做既能避免损坏文件污染数据目录也能反过来发现源站哪些URL是死链。5.2 国名带特殊字符导致文件系统报错现象下载到“科特迪瓦法语名”或“中国台北”这类条目时文件名里出现撇号、重音符号或括号在某些文件系统上保存失败或生成文件名变为乱码。原因不同文件系统对特殊字符容忍度不同。Windows的NTFS不允许文件名包含:/\|?*而旗帜名单里恰好可能包含这些字符的转写形式。解决统一用清洗函数把非法字符替换为下划线同时做重名检测。我一般还会把国家名做一层Unicode归一化unicodedata.normalize(NFKC, name)把全角字符转半角这样命名的稳定性会高很多。5.3 爬虫跑一半像卡死一样没动静现象控制台停在某个图片的下载进度上十几分钟都不动CPU占用接近0既没报错也没退出。原因典型的“响应未返回但连接未断开”问题。部分服务器的TCP连接在数据发送不完整时既不复位也不返回客户端如果没设超时就会无限等待。源码里timeout15这个参数只设置了读超时但如果DNS解析或连接建立阶段就挂住还需要设置连接超时。解决在session.get()里同时传入timeout(connect_timeout, read_timeout)比如timeout(5, 15)5秒连不上就放弃15秒读不到数据也放弃。这个二元组的写法比单值超时更精准。5.4 加了公共代理池后封得反而更快现象原本直连还能下几十张图加了某个免费代理池后每张图都会被拒一次甚至开局就是403。原因免费代理池里的IP大多被目标站点标记过信誉极差。代理IP的请求特征尤其是TLS指纹和直连不一致反而容易触发风控。解决优先用本地直连加低频限速不要一上来就上代理。如果确实需要代理选高质量住宅代理或自建代理池并且每个代理IP控制每秒请求数不超过2次。源码本身没有代理逻辑可以在Session对象上直接设置session.proxies {http: proxy_url, https: proxy_url}但它最稳定的用法还是本地直连。5.5 SVG格式的旗帜被当成PNG保存现象目标源提供的是矢量图缩略图显示正常但用代码读取图片尺寸时报错或者有些浏览器直接不渲染。原因部分图源在img标签里用的是SVG文件但没写后缀爬虫按PNG保存后内容实际是XML文本。文件头校验判断\x89PNG失败代码返回format_unknown有时会丢弃这个文件。解决校验逻辑里额外加SVG的识别分支。SVG文件内容开头通常包含svg标签或XML声明判断bsvg in content[:512]。识别出来后保存为.svg后缀。需要注意如果你后续处理管线比如Pillow不支持SVG解码最好在下载阶段就已经通过某在线转换服务把它转成PNG否则训练或展示环节还会翻车。6. 从国旗图片扩展到通用图片资源一套可泛化的采集骨架6.1 把选择器参数化一套代码跑多个站点国旗数据源写死选择器等于代码只能服务这一种场景。我把这份源码里的解析逻辑抽出来做了一层参数化让它能复用于“任何表格型图片列表页”。做法很简单目标页的CSS选择器、图片标签属性、国名所在列全部提升为配置变量不写死在函数里。爬电商商品的缩略图、爬卫星影像的预览图、爬图库的示例图只要页面结构是“列表表格图片”这套骨架就能套。CONFIG { list_url: https://example-source.example/flag-list, row_selector: #flag-table tr, name_selector: td:first-child a, img_selector: td:nth-child(2) img, img_attr: data-src, }换成别的站点只需要改这四个配置项。我后来爬某个开源图标库时把row_selector从表格选择器改成ul.icon-list liname_selector从td:first-child a改成span.name其他逻辑分毫未动就完成了适配。这就是参数化的价值——让代码跟数据源解耦。6.2 断点续传与增量更新让爬虫不白跑这份源码里包含“文件存在就跳过”的幂等逻辑但它不区分“这个文件之前下载成功过”和“这个文件是占位伪文件”。如果第一次跑的时候它的文件头校验逻辑太宽松把错误文件存成了.png第二次跑会因为“文件存在且大小大于0”直接跳过这个脏数据就永远留在目录里了。我做断点续传时的做法是单独维护一个download_log.json每一次成功下载都记录文件名、来源URL、MD5哈希、下载时间。重跑时先读日志文件存在且哈希一致才跳过否则强制重新下载。这样虽然多了一步JSON读写但它让整个流程可审计不会出现静默跳过脏数据的情况。增量更新则是利用日志里的时间戳字段只下载距离上次运行时间之后新增的国旗条目。如果你维护的旗帜库需要跟随源站更新比如某些地区政治实体变更导致国旗换新增量模式就非常合适。6.3 校验逻辑的正向迁移什么情况下拿去买数据这套爬虫的校验思路——文件头、尺寸、哈希、重试分级——其实可以迁移到任何“批量获取外部资源”的场景。包括抓取商品图片、抓取天气图标、抓取地图瓦片。甚至可以说爬图片只是外壳真正复用的是“下载前校验、失败分级重试、落盘去重”这三个思想。我给这份资源最高的评价是它不是一次性工具而是一套可拆可装的采集框架。你把国旗的解析部分拿走换上自己的数据源配置就是一个新的图片采集器。整套逻辑里最值得学的不是哪个API怎么调用而是怎么在“对方服务器不可控”的前提下让自己的产出保持稳定可用。说句实在话我从最早用wget硬抓网页图片到现在能稳定跑完几千张图的采集任务中间翻过不少车。最深刻的一个教训就是永远不要相信源站会按你预期的方式返回数据所有假设都要在校验逻辑里写成显式判断。从那以后我每次跑采集强制先跑一遍50条样本的小批量测试确认文件头和尺寸覆盖率都在95%以上才放开全量。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑