YOLOv12数据采集实战:编写Python爬虫构建自定义数据集
YOLOv12数据采集实战:编写Python爬虫构建自定义数据集
想用YOLOv12训练一个识别稀有商品的模型,第一步也是最关键的一步,就是得有足够多、足够好的图片数据。网上公开的数据集往往没有你想要的特定目标,比如某种限量版手办、某个小众品牌的包包,或者是你自己工厂生产的特殊零件。这时候,自己动手从网上“抓”图片,就成了必经之路。
今天,我就来手把手带你走一遍这个流程。咱们不聊复杂的理论,就聚焦一件事:如何用Python写一个爬虫,从电商网站、公开图库这些地方,自动化地把你需要的图片“搬”到自己的电脑里,并且整理成YOLOv12能直接用的格式。过程中会遇到的“反爬虫”拦路虎、图片重复、格式不对这些问题,我都会给出实用的解决办法。只要你有一点Python基础,跟着做,就能拥有自己的专属数据集。
1. 准备工作:想清楚再动手
在打开代码编辑器之前,花几分钟把这几件事想明白,能让你后续的工作顺畅十倍。
1.1 明确你的目标与数据源
首先,你得非常具体地知道你要识别什么。是“运动鞋”还是“某品牌某型号的白色运动鞋”?目标越具体,你采集数据时就越精准,模型训练效果也越好。
然后,根据目标去找数据源。常见的有:
- 电商平台:比如淘宝、京东、亚马逊。优点是商品图片角度规范、背景干净,且通常有白底图,非常适合做训练数据。但反爬措施也最严格。
- 搜索引擎图片:用百度、Google图片搜索。数据量大,但图片质量参差不齐,背景复杂,需要更多的清洗工作。
- 专业图库网站:如Flickr、Pexels。图片质量高,版权相对清晰,但可能缺乏你需要的特定商品图。
- 社交媒体:如Instagram、Pinterest。适合抓取生活场景下的物体图片,更贴近真实应用环境。
建议:对于商品识别,优先考虑电商平台;对于复杂场景下的物体识别,可以混合使用搜索引擎和图库。
1.2 搭建你的Python环境
你需要一个干净的Python环境。我强烈建议使用conda来创建独立环境,避免包版本冲突。
# 创建一个新的conda环境,命名为yolo_data conda create -n yolo_data python=3.9 conda activate yolo_data # 安装核心库 pip install requests beautifulsoup4 lxml selenium pillow tqdm简单解释一下这些库是干什么的:
requests:用来向网站发送请求,获取网页HTML内容。这是最基础的网络请求库。beautifulsoup4和lxml:这对好搭档用来解析HTML文档,帮你从复杂的网页代码里轻松提取出图片链接、标题等信息。lxml是解析器,速度很快。selenium:当网站内容是通过JavaScript动态加载的时候(比如很多电商网站滚动才会加载更多商品),requests就无能为力了。selenium可以模拟真实浏览器操作,搞定这些动态页面。pillow:Python里处理图片的标准库,我们用它来打开、验证和保存下载的图片。tqdm:一个能给你的循环加上美观进度条的小工具,下载几百张图片时,看着进度条一点点走,心里有底。
2. 编写你的第一个图片爬虫
我们从最简单的静态页面开始。假设我们要从某个允许爬取的公开图库(例如,一个模拟的图片列表页)下载图片。
2.1 解析静态页面获取图片链接
这个例子的思路适用于很多博客、图库类网站。
import os import requests from bs4 import BeautifulSoup from urllib.parse import urljoin from tqdm import tqdm def download_images_from_page(url, save_dir='./images', keyword='sneakers'): """ 从一个静态网页下载所有图片 :param url: 目标网页地址 :param save_dir: 图片保存目录 :param keyword: 可选,用于过滤图片链接中的关键词 """ # 创建保存目录 os.makedirs(save_dir, exist_ok=True) # 设置请求头,模拟浏览器访问 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } try: # 1. 发送请求获取网页内容 response = requests.get(url, headers=headers) response.raise_for_status() # 如果请求失败则抛出异常 response.encoding = response.apparent_encoding # 自动识别编码 # 2. 用BeautifulSoup解析HTML soup = BeautifulSoup(response.text, 'lxml') # 3. 查找所有的图片标签 <img> # 通常图片链接在 'src' 或 'data-src' 属性里 img_tags = soup.find_all('img') print(f"在该页面找到 {len(img_tags)} 个图片标签") # 4. 提取并过滤图片链接 img_urls = [] for img in img_tags: img_url = img.get('src') or img.get('data-src') if img_url: # 将相对路径转换为绝对路径 full_url = urljoin(url, img_url) # 简单过滤:只保留常见图片格式且链接中包含关键词(如果提供了) if full_url.lower().endswith(('.jpg', '.jpeg', '.png', '.webp')): if keyword is None or keyword.lower() in full_url.lower(): img_urls.append(full_url) print(f"过滤后得到 {len(img_urls)} 个有效图片链接") # 5. 下载图片 for i, img_url in enumerate(tqdm(img_urls, desc="下载图片")): try: img_data = requests.get(img_url, headers=headers, timeout=10).content # 生成文件名:使用链接中的文件名或序号 filename = os.path.join(save_dir, f"image_{i:04d}.jpg") with open(filename, 'wb') as f: f.write(img_data) except Exception as e: print(f"下载失败 {img_url}: {e}") continue print(f"下载完成!图片保存在: {os.path.abspath(save_dir)}") except requests.RequestException as e: print(f"请求网页时出错: {e}") # 使用示例(请替换为实际可访问的、允许爬取的测试页面) # download_images_from_page('https://example.com/photos', save_dir='./shoes', keyword='shoe')代码要点:
User-Agent头很重要,很多网站会屏蔽没有这个头的请求。urljoin函数能智能地处理相对路径和绝对路径,避免拼错链接。- 异常处理(
try...except)是爬虫的必备品,网络请求不稳定,不能让一个错误导致整个程序崩溃。 - 使用
tqdm包裹你的下载循环,体验会好很多。
2.2 应对动态加载页面:使用Selenium
电商网站的商品列表通常是滚动加载的。这时候就需要请出Selenium。你需要先下载一个浏览器驱动(如ChromeDriver),并确保与你的Chrome浏览器版本匹配。
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import time def scrape_dynamic_page(search_url, save_dir, max_scroll=5): """ 使用Selenium爬取动态加载的电商商品图片 :param search_url: 搜索结果的URL(例如:某宝搜索‘运动鞋’) :param save_dir: 保存目录 :param max_scroll: 模拟滚动的次数,控制加载的商品数量 """ os.makedirs(save_dir, exist_ok=True) # 设置Chrome选项(无头模式,不显示浏览器窗口) options = webdriver.ChromeOptions() options.add_argument('--headless') # 后台运行 options.add_argument('--disable-gpu') options.add_argument('--no-sandbox') options.add_argument('user-agent=Mozilla/5.0 ...') # 同上,设置User-Agent driver = webdriver.Chrome(options=options) # 确保chromedriver在PATH中 driver.get(search_url) img_urls_set = set() # 用集合避免重复链接 last_height = driver.execute_script("return document.body.scrollHeight") for _ in range(max_scroll): # 等待页面新内容加载 time.sleep(2) # 查找当前页面中的所有商品图片元素(需要根据实际网站调整选择器) # 例如,某网站的商品主图可能放在 <img class='main-img'> 里 img_elements = driver.find_elements(By.CSS_SELECTOR, "img.J_ItemPic, img.main-img") # 示例选择器 for elem in img_elements: src = elem.get_attribute('src') or elem.get_attribute('data-src') if src and src.startswith('http') and src.lower().endswith(('.jpg', '.jpeg', '.png')): # 有时链接可能是小图,尝试获取大图链接(规则因网站而异) # 例如,将链接中的“60x60”替换为“400x400” high_res_src = src.replace('60x60', '400x400').replace('50x50', '800x800') img_urls_set.add(high_res_src) # 模拟滚动到底部 driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(3) # 等待新内容加载 new_height = driver.execute_script("return document.body.scrollHeight") if new_height == last_height: break # 如果高度不再变化,说明已到底部 last_height = new_height driver.quit() print(f"共收集到 {len(img_urls_set)} 个唯一图片链接") # 下载图片(复用之前的下载逻辑) headers = {'User-Agent': '...'} for i, url in enumerate(tqdm(list(img_urls_set)[:50], desc="下载商品图")): # 先下载前50张试试 try: img_data = requests.get(url, headers=headers, timeout=10).content filename = os.path.join(save_dir, f"product_{i:04d}.jpg") with open(filename, 'wb') as f: f.write(img_data) except Exception as e: print(f"下载失败 {url}: {e}") # 使用示例(请谨慎使用,遵守网站robots.txt) # scrape_dynamic_page('https://search.example.com?q=运动鞋', './ecommerce_shoes')关键点:
headless模式让浏览器在后台运行,不弹出窗口。CSS_SELECTOR是定位页面元素最强大的工具,你需要用浏览器的“开发者工具”(F12)去查看目标图片的HTML结构,找到它独特的类名或属性,然后替换代码中的选择器。- 滚动和等待时间(
time.sleep)需要根据目标网站的加载速度调整,太短可能加载不出来,太长效率低。 - 重要:在实际使用前,务必检查目标网站的
robots.txt文件(通常在网站根目录,如https://www.example.com/robots.txt),并尊重其规则。对于商业网站,大规模爬取可能违反其服务条款。
3. 数据清洗与去重:让数据集更干净
下载下来的图片什么都有:大小不一、有水印、有重复、甚至可能损坏。这一步就是做“保洁”。
3.1 基础清洗:过滤无效文件
from PIL import Image import hashlib def clean_image_dataset(image_dir): """ 基础清洗:删除无法用PIL打开的损坏图片,以及尺寸过小的图片。 """ valid_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.webp') deleted_count = 0 for filename in os.listdir(image_dir): filepath = os.path.join(image_dir, filename) # 检查扩展名 if not filename.lower().endswith(valid_extensions): try: os.remove(filepath) deleted_count += 1 print(f"删除非图片文件: {filename}") except: pass continue # 尝试用PIL打开,检查是否损坏 try: with Image.open(filepath) as img: img.verify() # 验证文件完整性 # 重新打开以获取尺寸(verify后需要重新打开) img = Image.open(filepath) width, height = img.size # 删除尺寸过小的图片(例如小于100x100) if width < 100 or height < 100: os.remove(filepath) deleted_count += 1 print(f"删除尺寸过小图片({width}x{height}): {filename}") img.close() # 确保关闭文件 except (IOError, SyntaxError, Exception) as e: # 文件损坏或不是有效图片 print(f"删除损坏图片 {filename}: {e}") try: os.remove(filepath) deleted_count += 1 except: pass print(f"基础清洗完成,共删除 {deleted_count} 个文件。")3.2 基于哈希值的去重
同一张图片可能以不同文件名被多次下载。我们可以计算图片内容的哈希值来找出重复项。
def find_and_remove_duplicates(image_dir, hash_func='md5'): """ 通过计算图片哈希值来查找并删除重复图片。 :param hash_func: 哈希算法,可选 'md5', 'sha1', 'average_hash'(感知哈希,用于相似图) """ hashes = {} duplicates = [] total_files = 0 for filename in os.listdir(image_dir): filepath = os.path.join(image_dir, filename) if not os.path.isfile(filepath): continue try: with Image.open(filepath) as img: # 计算哈希值 if hash_func == 'md5': with open(filepath, 'rb') as f: file_hash = hashlib.md5(f.read()).hexdigest() elif hash_func == 'average_hash': # 感知哈希,能发现内容相似但尺寸、质量不同的图片 # 需要安装 imagehash 库: pip install imagehash import imagehash file_hash = str(imagehash.average_hash(img)) else: with open(filepath, 'rb') as f: file_hash = hashlib.sha1(f.read()).hexdigest() total_files += 1 if file_hash in hashes: duplicates.append((filepath, hashes[file_hash])) print(f"发现重复: {filename} 与 {os.path.basename(hashes[file_hash])}") else: hashes[file_hash] = filepath except Exception as e: print(f"处理文件 {filename} 时出错: {e}") # 删除重复文件(保留最早的那个) for dup, original in duplicates: try: os.remove(dup) print(f"已删除重复文件: {os.path.basename(dup)}") except Exception as e: print(f"删除失败 {dup}: {e}") print(f"去重完成。共处理 {total_files} 个文件,发现并删除 {len(duplicates)} 个重复项。")说明:md5哈希对文件的任何字节修改都敏感,适合找完全相同的文件。average_hash(感知哈希)能找出内容相似但可能经过压缩、裁剪或轻微调色的图片,更适合找“看起来一样”的图,但计算稍慢。
4. 为YOLOv12准备标注格式
YOLO需要的标注文件是.txt格式,每个图片对应一个同名的txt文件。内容如下:
<object-class> <x_center> <y_center> <width> <height>这些坐标是归一化的,即相对于图片宽度和高度的比例值,范围在0到1之间。
假设我们现在还没有标注工具,但我们已经有了图片。我们可以先创建空的占位符标注文件,或者用一些自动标注工具(如使用预训练模型进行初标)来生成初步标注,然后人工修正。这里展示如何生成符合格式的空文件,以及如何将常见标注格式(如COCO的JSON)转换为YOLO格式。
4.1 生成空的占位符标注文件
def create_empty_annotation_for_images(image_dir, annotation_dir='./labels'): """ 为图片目录下的每张图片创建一个同名的空txt文件(或包含默认类别)。 用于在手动标注前准备好文件结构。 """ os.makedirs(annotation_dir, exist_ok=True) img_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.webp') for filename in os.listdir(image_dir): if filename.lower().endswith(img_extensions): # 生成对应的txt文件名 base_name = os.path.splitext(filename)[0] txt_filename = os.path.join(annotation_dir, base_name + '.txt') # 如果文件不存在,则创建一个空文件 if not os.path.exists(txt_filename): with open(txt_filename, 'w') as f: # 可以写入一行默认标注,例如假设图片中心有一个类别0的物体(根据实际情况修改或留空) # f.write("0 0.5 0.5 0.2 0.2\n") pass print(f"创建空标注文件: {txt_filename}") print("空标注文件创建完成。请使用标注工具(如LabelImg)打开图片进行标注。")4.2 将COCO JSON格式转换为YOLO格式
如果你从某些公开数据集下载的数据是COCO格式的,可以用下面的函数转换。
import json def coco_to_yolo(coco_json_path, output_label_dir, class_name_to_id): """ 将COCO格式的标注JSON文件转换为YOLO格式的txt文件。 :param coco_json_path: COCO annotations.json 文件路径 :param output_label_dir: YOLO标签输出目录 :param class_name_to_id: 字典,映射COCO类别名到YOLO类别ID(从0开始) """ os.makedirs(output_label_dir, exist_ok=True) with open(coco_json_path, 'r') as f: coco_data = json.load(f) # 构建映射:image_id -> file_name images = {img['id']: img for img in coco_data['images']} # 构建映射:category_id -> category_name categories = {cat['id']: cat['name'] for cat in coco_data['categories']} # 按图片分组标注 from collections import defaultdict anns_by_image = defaultdict(list) for ann in coco_data['annotations']: anns_by_image[ann['image_id']].append(ann) for img_id, ann_list in anns_by_image.items(): img_info = images.get(img_id) if not img_info: continue img_width = img_info['width'] img_height = img_info['height'] base_filename = os.path.splitext(img_info['file_name'])[0] txt_filename = os.path.join(output_label_dir, base_filename + '.txt') with open(txt_filename, 'w') as f_txt: for ann in ann_list: # COCO标注格式是 [x_min, y_min, width, height] x_min, y_min, bbox_width, bbox_height = ann['bbox'] # 计算中心点和归一化坐标 x_center = (x_min + bbox_width / 2) / img_width y_center = (y_min + bbox_height / 2) / img_height norm_width = bbox_width / img_width norm_height = bbox_height / img_height # 获取类别ID cat_name = categories.get(ann['category_id']) yolo_class_id = class_name_to_id.get(cat_name) if yolo_class_id is None: print(f"警告:类别 '{cat_name}' 未在映射表中,跳过此标注。") continue # 写入YOLO格式行 f_txt.write(f"{yolo_class_id} {x_center:.6f} {y_center:.6f} {norm_width:.6f} {norm_height:.6f}\n") print(f"转换完成。YOLO标签文件已保存至: {output_label_dir}")5. 整合与最佳实践建议
把上面的步骤串起来,一个完整的数据采集流水线大概是这样的:
- 规划:明确目标 -> 选择3-5个高质量数据源。
- 爬取:针对每个数据源,分析页面结构,编写或调整爬虫脚本。务必遵守
robots.txt,添加延迟(如time.sleep(random.uniform(1,3)))避免对服务器造成压力。 - 清洗:运行基础清洗和去重脚本,得到一个干净的图片文件夹。
- 标注:使用专业的标注工具,如LabelImg、CVAT或Roboflow。这是最耗时但最关键的一步,标注质量直接决定模型上限。建议先标注100-200张,训练一个初版模型,用这个模型对剩余图片进行“预标注”,然后再人工检查和修正,能极大提升效率。
- 划分数据集:将图片和对应的标注文件按比例(如70%训练,20%验证,10%测试)分配到
train、val、test文件夹中。同时,创建data.yaml配置文件,指明路径和类别名。
一个简单的数据集划分脚本:
import os import random import shutil def split_dataset(image_dir, label_dir, output_base_dir, ratios=(0.7, 0.2, 0.1)): """ 划分数据集为训练集、验证集和测试集。 """ sets = ['train', 'val', 'test'] for s in sets: os.makedirs(os.path.join(output_base_dir, 'images', s), exist_ok=True) os.makedirs(os.path.join(output_base_dir, 'labels', s), exist_ok=True) # 获取所有图片文件名(不含扩展名) all_images = [os.path.splitext(f)[0] for f in os.listdir(image_dir) if f.endswith(('.jpg', '.png'))] random.shuffle(all_images) total = len(all_images) train_end = int(total * ratios[0]) val_end = train_end + int(total * ratios[1]) train_set = all_images[:train_end] val_set = all_images[train_end:val_end] test_set = all_images[val_end:] for img_base in train_set: _copy_files(img_base, image_dir, label_dir, output_base_dir, 'train') for img_base in val_set: _copy_files(img_base, image_dir, label_dir, output_base_dir, 'val') for img_base in test_set: _copy_files(img_base, image_dir, label_dir, output_base_dir, 'test') print(f"数据集划分完成:训练集 {len(train_set)},验证集 {len(val_set)},测试集 {len(test_set)}") def _copy_files(base_name, src_img_dir, src_lbl_dir, dst_base_dir, subset): # 找到源图片和标签文件(考虑不同扩展名) for ext in ['.jpg', '.jpeg', '.png']: src_img = os.path.join(src_img_dir, base_name + ext) if os.path.exists(src_img): shutil.copy(src_img, os.path.join(dst_base_dir, 'images', subset)) break src_lbl = os.path.join(src_lbl_dir, base_name + '.txt') if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(dst_base_dir, 'labels', subset))最后,你的data.yaml文件看起来应该是这样:
# data.yaml path: /path/to/your/dataset # 数据集根目录 train: images/train # 训练集图片相对路径 val: images/val # 验证集图片相对路径 test: images/test # 测试集图片相对路径(可选) # 类别列表,ID从0开始 names: 0: rare_sneaker_white 1: rare_sneaker_black 2: limited_edition_bag6. 总结
走完这一趟,你应该能感受到,构建一个高质量的定制数据集,技术本身(写爬虫、清洗数据)只占一部分,更多的功夫花在规划、标注和整理上。爬虫是你的“采矿机”,但最终“矿石”的纯度,需要你精心筛选和打磨。
整个过程最需要耐心的是标注环节,这也是AI项目中无法完全自动化的一环。我的建议是,在爬取和清洗阶段尽可能严格,从源头保证图片质量,这样能大幅减少后续标注时的无效劳动。另外,善用现有的预训练模型进行辅助标注,或者利用半自动标注工具,都是提升效率的好办法。
最后要再次强调合规与伦理。只爬取允许爬取的数据,尊重版权,控制请求频率,不要给目标网站带来不必要的负担。你的模型最终要创造价值,它的起点也应该是干净、合法的。
希望这篇实战指南能帮你顺利跨出YOLOv12项目的第一步。当你的模型开始准确地识别出你精心收集的那些目标时,那种成就感,绝对值得前面的所有折腾。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
