丹青识画系统Python爬虫实战:自动构建艺术图像数据集

你有没有想过,要训练一个能看懂世界名画的AI模型,第一步最头疼的是什么?没错,就是数据。成千上万张高清画作图片,还要配上画家、年代、风格、主题这些详细的标签,手动收集简直是不可能完成的任务。

我之前就遇到过这个难题。当时想做一个艺术风格分类的小项目,光是找几百张带标签的图片就花了好几天,效率低不说,标签还不统一。后来我发现,其实很多博物馆和艺术网站都有公开的数字藏品,如果能自动把它们“搬”下来,再让AI帮忙打上标签,不就完美了吗?

今天,我就来分享一套完整的实战方案:用Python写个爬虫,从网上自动抓取艺术图像,然后调用丹青识画系统的API,让AI给每张画作自动分析并打上丰富的标签。最终,我们就能得到一个规整的、带有多维度标签的大规模艺术图像数据集。整个过程完全自动化,特别适合需要大量艺术图像数据的研究者、开发者或者艺术爱好者。

1. 为什么需要自动化构建艺术数据集?

手动收集艺术图像数据,通常意味着你要一个个打开博物馆网站,右键保存图片,然后再去查资料,把画家、创作年份、艺术流派等信息填到表格里。这个过程有三大痛点:

第一是效率极低。一个人的精力有限,一天能整理几十张高质量数据已经算很快了。但对于机器学习项目来说,几百张数据往往只是起步,想要模型效果好,几千甚至上万张数据是常态。

第二是标签不一致。不同来源对同一幅画的描述可能不同,比如“后印象派”和“印象派后期”,手动整理很容易出现混乱,给后续的数据清洗和模型训练带来麻烦。

第三是信息维度单一。我们通常只能找到最基础的元数据(如画名、作者)。但一幅画所包含的信息远不止这些:它的构图特点、色彩运用、情感表达、画面中的物体等等,这些更深层的标签对于训练一个“懂画”的AI至关重要,而人工标注这些标签的成本高得惊人。

我们的解决方案正好能解决这些问题。爬虫负责高效、批量地获取原始图像和基础元数据;丹青识画系统则扮演一个不知疲倦的“艺术顾问”,通过API为每一幅画提供统一、丰富、多维度的深度分析标签。两者结合,就能以极低的成本,构建出一个高质量、高价值的数据集。

2. 实战前的准备工作

工欲善其事,必先利其器。在开始写代码之前,我们需要把环境和工具准备好。

2.1 核心工具与库

我们的项目主要依赖以下几个Python库,你可以通过pip命令一键安装:

  • requests / aiohttp: 用于向目标网站和丹青识画API发送网络请求。requests简单易用,aiohttp则用于异步请求,能大幅提升爬取速度。
  • BeautifulSoup4 / lxml: 这是解析网页HTML的利器。当爬虫拿到网页源代码后,就用它们来提取我们需要的图片链接和文本信息。
  • Pandas: 数据处理和分析的核心。我们将用它来清洗、整理元数据,并最终保存为结构化的表格(如CSV文件)。
  • Pillow (PIL): Python的图像处理库。可以用来验证下载的图片是否完整,或者进行简单的格式转换。
  • os, json, time: Python的标准库,分别用于管理文件路径、处理JSON格式的API返回数据,以及在请求间添加延时以避免被封。

安装命令很简单,打开你的终端或命令行,输入:

pip install requests beautifulsoup4 pandas pillow aiohttp

2.2 目标网站分析与策略制定

不是所有网站都适合爬取。在选择目标时,我们要优先考虑那些对公众开放、有清晰版权声明(通常为CC0或公共领域)的机构,例如:

  • 纽约大都会艺术博物馆:其官网提供了海量的高清艺术品图像和元数据API。
  • 芝加哥艺术博物馆:同样有丰富的开放获取资源。
  • 维基艺术:一个大型的线上艺术百科全书。

在动手之前,一定要做这几件事:

  1. 查看robots.txt:访问 目标网站/robots.txt,了解网站允许或禁止爬虫访问的路径。尊重规则是爬虫工程师的基本素养。
  2. 分析网页结构:用浏览器的“开发者工具”(F12打开),查看图片和元数据在网页HTML中的具体位置。找到包裹它们的标签和属性(如<img src="..."><div class="artist-name">)。
  3. 规划数据存储结构:想好最终的数据集怎么放。我建议的文件夹结构如下:
    art_dataset/
    ├── images/               # 存放所有下载的画作图片
    │   ├── van_gogh_001.jpg
    │   ├── monet_001.jpg
    │   └── ...
    ├── metadata_raw.json    # 爬虫获取的原始元数据
    ├── metadata_enriched.csv # 合并了API分析结果的最终元数据表
    └── spider_log.txt       # 运行日志,记录成功与失败
    

2.3 获取丹青识画系统API密钥

这是让我们的数据集“增值”的关键。你需要访问丹青识画系统的官方网站,注册账号并申请API访问权限。成功后会获得一个唯一的API密钥(通常是一串字符),它就像一把钥匙,每次调用API时都需要带上它。

请妥善保管这个密钥,不要把它直接硬编码在代码里然后上传到公开的代码仓库(如GitHub),这会导致密钥泄露。安全的做法是把它保存在本地的环境变量或一个单独的配置文件中。

3. 爬虫核心:抓取画作图像与元数据

接下来,我们进入实战环节,一步步构建爬虫的核心功能。

3.1 基础爬虫搭建:请求与解析

我们以抓取一个模拟的艺术画廊列表页为例。假设列表页列出了多幅画作,点击每幅画进入详情页才能看到大图和详细信息。

首先,我们写一个函数来获取并解析列表页,提取所有画作详情页的链接。

import requests
from bs4 import BeautifulSoup
import time

def get_artwork_links(list_url, headers):
    """
    从列表页获取所有画作详情页的链接。
    """
    try:
        response = requests.get(list_url, headers=headers, timeout=10)
        response.raise_for_status()  # 检查请求是否成功
        soup = BeautifulSoup(response.content, 'html.parser')

        # 假设每个画作卡片由一个带有特定class的<a>标签包裹
        artwork_cards = soup.find_all('a', class_='artwork-link')
        links = [card['href'] for card in artwork_cards if card.get('href')]

        # 处理相对链接,将其转换为绝对链接
        base_url = "https://example-art-museum.org"
        full_links = [base_url + link if link.startswith('/') else link for link in links]

        print(f"从列表页找到了 {len(full_links)} 个画作链接。")
        return full_links

    except requests.RequestException as e:
        print(f"获取列表页失败: {e}")
        return []

# 使用示例
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
list_url = "https://example-art-museum.org/collection"
artwork_urls = get_artwork_links(list_url, headers)
time.sleep(1)  # 礼貌性延时,避免请求过快

3.2 详情页数据提取

拿到详情页链接后,我们逐个访问,提取高清图片地址和基础元数据。

def scrape_artwork_detail(detail_url, headers):
    """
    从画作详情页提取图像URL和元数据。
    """
    artwork_data = {}
    try:
        resp = requests.get(detail_url, headers=headers, timeout=10)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.content, 'html.parser')

        # 1. 提取高清图片URL (假设是第一个高分辨率img标签)
        img_tag = soup.find('img', class_='high-res-image')
        if img_tag and img_tag.get('src'):
            image_url = img_tag['src']
            # 确保是绝对URL
            if image_url.startswith('//'):
                image_url = 'https:' + image_url
            elif image_url.startswith('/'):
                image_url = 'https://example-art-museum.org' + image_url
            artwork_data['image_url'] = image_url
        else:
            artwork_data['image_url'] = None
            print(f"未在 {detail_url} 找到图片。")

        # 2. 提取元数据 (根据实际网页结构调整选择器)
        artwork_data['title'] = soup.find('h1', class_='artwork-title').get_text(strip=True) if soup.find('h1', class_='artwork-title') else 'Unknown'
        artwork_data['artist'] = soup.find('span', class_='artist-name').get_text(strip=True) if soup.find('span', class_='artist-name') else 'Unknown'
        artwork_data['year'] = soup.find('span', class_='creation-year').get_text(strip=True) if soup.find('span', class_='creation-year') else 'Unknown'
        artwork_data['style'] = soup.find('a', class_='art-movement').get_text(strip=True) if soup.find('a', class_='art-movement') else 'Unknown'
        artwork_data['source_url'] = detail_url

        return artwork_data

    except Exception as e:
        print(f"解析详情页 {detail_url} 时出错: {e}")
        return None

3.3 图像下载与本地存储

提取到图片URL后,我们需要将其下载到本地images文件夹,并以合理的方式命名(例如艺术家_画作ID.jpg)。

import os
from urllib.parse import urlparse

def download_image(image_url, save_dir, artwork_id, headers):
    """
    下载图片并保存到本地。
    """
    if not image_url:
        return None

    try:
        # 从URL中提取文件扩展名
        parsed_url = urlparse(image_url)
        file_ext = os.path.splitext(parsed_url.path)[1]
        if not file_ext:
            file_ext = '.jpg'  # 默认扩展名

        # 生成文件名和保存路径
        filename = f"{artwork_id}{file_ext}"
        save_path = os.path.join(save_dir, filename)

        # 下载图片
        img_response = requests.get(image_url, headers=headers, timeout=30, stream=True)
        img_response.raise_for_status()

        with open(save_path, 'wb') as f:
            for chunk in img_response.iter_content(chunk_size=8192):
                f.write(chunk)

        print(f"图片已保存: {save_path}")
        return filename  # 返回保存的文件名,用于后续记录

    except Exception as e:
        print(f"下载图片失败 {image_url}: {e}")
        return None

# 使用示例
os.makedirs('./art_dataset/images', exist_ok=True)
for idx, url in enumerate(artwork_urls[:3]):  # 先试3个
    data = scrape_artwork_detail(url, headers)
    if data and data.get('image_url'):
        file_id = f"art_{idx:04d}"
        saved_name = download_image(data['image_url'], './art_dataset/images', file_id, headers)
        if saved_name:
            data['local_image_path'] = saved_name  # 记录本地路径
        # 将data保存到元数据列表...
    time.sleep(2)  # 重要!在请求间加入延时

3.4 应对反爬虫策略

直接按上面的代码跑,很可能很快就被网站屏蔽了。我们需要一些策略来模拟真人行为:

  • 设置请求头:最重要的就是User-Agent,让它看起来像一个真实的浏览器。可以准备一个列表轮流使用。
  • 添加请求延时:在请求之间随机休眠一段时间,比如time.sleep(random.uniform(1, 3))
  • 处理Cookie和Session:对于需要登录或复杂交互的网站,使用requests.Session()来维持会话状态。
  • 使用代理IP:如果请求频率非常高,可以考虑使用代理IP池来分散请求来源。
  • 错误重试机制:网络请求可能失败,添加重试逻辑可以提高鲁棒性。
import random

def polite_request(url, headers, session=None, retries=3):
    """一个带有延时和重试机制的礼貌请求函数"""
    for attempt in range(retries):
        try:
            time.sleep(random.uniform(1, 2))  # 随机延时1-2秒
            if session:
                resp = session.get(url, headers=headers, timeout=10)
            else:
                resp = requests.get(url, headers=headers, timeout=10)
            resp.raise_for_status()
            return resp
        except requests.RequestException as e:
            print(f"请求失败 (尝试 {attempt+1}/{retries}): {e}")
            if attempt == retries - 1:
                return None
            time.sleep(2 ** attempt)  # 指数退避延时
    return None

4. 数据增值:调用丹青识画API进行分析

爬虫拿到了“原材料”(图片和基础信息),现在该请出我们的“艺术顾问”——丹青识画系统API,来为每幅画作添加深度洞察。

4.1 调用API获取画作分析

假设丹青识画API提供了一个接口,我们可以上传图片或提供图片URL,它返回一个包含多项分析结果的JSON数据。

import json

def analyze_artwork_with_api(image_path_or_url, api_key, analysis_type='full'):
    """
    调用丹青识画API分析画作。
    :param image_path_or_url: 本地图片路径或在线图片URL
    :param api_key: 你的API密钥
    :param analysis_type: 分析类型,如 'full', 'style', 'object'等
    :return: API返回的JSON数据
    """
    api_endpoint = "https://api.danqing.com/v1/analyze"  # 示例端点,需替换为真实地址
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }

    # 根据参数判断是上传文件还是传递URL
    payload = {
        'analysis_type': analysis_type
    }
    if image_path_or_url.startswith('http'):
        payload['image_url'] = image_path_or_url
        data = json.dumps(payload)
        response = requests.post(api_endpoint, headers=headers, data=data)
    else:
        # 假设API支持multipart/form-data文件上传
        with open(image_path_or_url, 'rb') as img_file:
            files = {'image': img_file}
            data = {'analysis_type': analysis_type}
            response = requests.post(api_endpoint, headers=headers, files=files, data=data)

    if response.status_code == 200:
        return response.json()
    else:
        print(f"API调用失败,状态码: {response.status_code}, 响应: {response.text}")
        return None

# 使用示例
api_key = "YOUR_API_KEY_HERE"  # 请务必从安全的地方读取,不要硬编码
local_image_path = "./art_dataset/images/art_0001.jpg"

analysis_result = analyze_artwork_with_api(local_image_path, api_key)
if analysis_result:
    print("API分析成功!")
    # 打印出一些关键分析结果
    print(f"风格分析: {analysis_result.get('style', {})}")
    print(f"检测到的物体: {analysis_result.get('objects', [])}")
    print(f"色彩分析: {analysis_result.get('color_palette', [])}")

4.2 解析与整合API返回数据

API返回的数据可能很丰富。我们需要从中提取出对我们构建数据集最有用的标签,并整合到之前的元数据中。

def enrich_metadata(basic_metadata, api_result):
    """
    将API分析结果整合到基础元数据中。
    """
    if not api_result:
        return basic_metadata

    enriched = basic_metadata.copy()

    # 提取并整合风格标签 (假设API返回一个风格列表,按置信度排序)
    styles = api_result.get('style_prediction', [])
    if styles:
        # 取置信度最高的风格作为主风格,所有风格作为列表
        enriched['primary_style'] = styles[0].get('name') if isinstance(styles[0], dict) else styles[0]
        enriched['all_styles'] = [s.get('name') if isinstance(s, dict) else s for s in styles]

    # 提取检测到的物体/场景标签
    objects = api_result.get('detected_objects', [])
    if objects:
        # 假设每个物体是一个字典,包含'name'和'confidence'
        enriched['objects'] = [obj.get('name') for obj in objects if obj.get('confidence', 0) > 0.5]

    # 提取情感或主题标签
    emotions = api_result.get('emotion_tags', [])
    if emotions:
        enriched['emotion_tags'] = emotions

    # 提取主色调
    colors = api_result.get('dominant_colors', [])
    if colors:
        enriched['dominant_colors'] = colors[:3]  # 取前三个主色

    # 记录API分析时间
    enriched['api_analysis_time'] = api_result.get('processing_time')

    return enriched

# 整合流程示例
basic_data = {'title': 'Starry Night', 'artist': 'Vincent van Gogh', ...}
api_data = analyze_artwork_with_api('./images/art_0001.jpg', api_key)
final_artwork_record = enrich_metadata(basic_data, api_data)

5. 数据清洗、存储与项目管理

最后一步,我们把所有处理好的数据规整地保存起来,并考虑如何管理这个可能持续运行的项目。

5.1 数据清洗与格式化

爬取和API返回的数据可能存在缺失值、格式不一致等问题,需要进行清洗。

import pandas as pd

def clean_and_format_data(artwork_records):
    """
    清洗和格式化所有画作记录。
    """
    df = pd.DataFrame(artwork_records)

    # 1. 处理缺失值
    # 将某些字段的缺失值设为‘Unknown’
    columns_to_fill = ['artist', 'year', 'style']
    for col in columns_to_fill:
        df[col].fillna('Unknown', inplace=True)

    # 2. 规范化格式
    # 例如,确保年份是字符串或整数,去除多余空格
    df['title'] = df['title'].astype(str).str.strip()
    df['artist'] = df['artist'].astype(str).str.strip()

    # 3. 展开列表型标签(如all_styles, objects)为用分号分隔的字符串,便于CSV存储
    list_columns = ['all_styles', 'objects', 'emotion_tags']
    for col in list_columns:
        if col in df.columns:
            df[col] = df[col].apply(lambda x: ';'.join(x) if isinstance(x, list) else x)

    return df

# 假设artwork_list是包含所有画作记录(基础+API)的字典列表
cleaned_df = clean_and_format_data(artwork_list)
print(cleaned_df.head())

5.2 数据存储方案

将清洗后的数据保存为通用格式,方便后续使用。

def save_dataset(dataframe, image_dir, output_dir='./art_dataset'):
    """
    保存数据集:元数据CSV和图片文件夹。
    """
    os.makedirs(output_dir, exist_ok=True)

    # 1. 保存元数据为CSV
    csv_path = os.path.join(output_dir, 'metadata_enriched.csv')
    dataframe.to_csv(csv_path, index=False, encoding='utf-8-sig') # utf-8-sig支持中文
    print(f"元数据已保存至: {csv_path}")

    # 2. 图片已经保存在image_dir中,这里可以记录一下统计信息
    image_count = len([f for f in os.listdir(image_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg'))])
    print(f"数据集包含 {len(dataframe)} 条记录,{image_count} 张图片。")

    # (可选)3. 保存一份原始API响应的JSON,以备深度分析
    # json_path = os.path.join(output_dir, 'raw_api_responses.json')
    # with open(json_path, 'w') as f:
    #     json.dump(artwork_list, f, indent=2)

# 执行保存
save_dataset(cleaned_df, './art_dataset/images')

5.3 构建健壮的爬虫脚本

将以上所有步骤整合到一个主函数中,并加入日志和错误处理,形成一个可以稳定运行的脚本。

import logging

def main():
    """主爬虫流程"""
    # 配置日志
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
    logger = logging.getLogger(__name__)

    # 1. 初始化
    headers = {...}
    api_key = os.getenv('DANQING_API_KEY')  # 从环境变量读取密钥
    base_save_dir = './art_dataset'
    os.makedirs(os.path.join(base_save_dir, 'images'), exist_ok=True)

    all_artwork_data = []

    # 2. 获取画作链接列表 (这里可以循环多个列表页)
    list_urls = ["https://example-art-museum.org/collection?page=1"]
    all_detail_urls = []
    for l_url in list_urls:
        links = get_artwork_links(l_url, headers)
        all_detail_urls.extend(links)
        time.sleep(random.uniform(2, 4))

    # 3. 遍历每个详情页,抓取数据并调用API
    for idx, detail_url in enumerate(all_detail_urls):
        logger.info(f"处理 ({idx+1}/{len(all_detail_urls)}): {detail_url}")
        try:
            # 抓取基础数据
            basic_meta = scrape_artwork_detail(detail_url, headers)
            if not basic_meta or not basic_meta.get('image_url'):
                logger.warning(f"跳过,未获取到有效数据或图片: {detail_url}")
                continue

            # 下载图片
            file_id = f"art_{idx:05d}"
            saved_filename = download_image(basic_meta['image_url'],
                                             os.path.join(base_save_dir, 'images'),
                                             file_id,
                                             headers)
            if not saved_filename:
                continue
            basic_meta['local_image_path'] = saved_filename

            # 调用API分析
            local_img_path = os.path.join(base_save_dir, 'images', saved_filename)
            api_result = analyze_artwork_with_api(local_img_path, api_key)

            # 整合数据
            final_record = enrich_metadata(basic_meta, api_result)
            all_artwork_data.append(final_record)

            # 每处理10条,保存一次进度(防止中途出错全部丢失)
            if (idx + 1) % 10 == 0:
                temp_df = pd.DataFrame(all_artwork_data)
                temp_df.to_csv(os.path.join(base_save_dir, 'metadata_temp.csv'), index=False)
                logger.info(f"已保存进度,处理了 {idx+1} 条记录。")

            # 礼貌延时
            time.sleep(random.uniform(3, 6))

        except Exception as e:
            logger.error(f"处理 {detail_url} 时发生严重错误: {e}", exc_info=True)
            continue

    # 4. 最终清洗与保存
    logger.info("所有画作处理完成,开始清洗和保存最终数据集。")
    final_df = clean_and_format_data(all_artwork_data)
    save_dataset(final_df, os.path.join(base_save_dir, 'images'), base_save_dir)
    logger.info("数据集构建完成!")

if __name__ == '__main__':
    main()

6. 总结与展望

走完这一整套流程,你会发现自动化构建数据集虽然前期需要一些开发投入,但一旦跑通,其效率是手动收集无法比拟的。这套方法的核心价值在于“爬虫解决量,API解决质”,它不仅能快速获取海量图像,还能为每张图像附上专业、统一、多维度的语义标签,极大地提升了数据集的可用性和价值。

在实际操作中,有几个点需要特别注意。一是法律与伦理,务必只爬取明确声明可以公开、免费使用的资源,并严格遵守网站的robots.txt协议。二是稳定性,网络环境和API服务都可能出现波动,完善的错误处理和日志记录是保证长时间稳定运行的关键。三是数据质量,定期抽样检查下载的图片和API生成的标签,确保流程没有出现系统性偏差。

未来,这个流程还有很多可以优化的地方。比如,引入异步IO库(如aiohttp)可以让爬取速度提升一个数量级;设计更复杂的调度器,可以同时从多个博物馆网站抓取数据;甚至可以将API返回的JSON原始响应也保存下来,为后续更精细的数据分析和模型训练提供原材料。

希望这个实战指南能为你打开一扇门。艺术数据的世界很大,有了自动化的工具,你可以更自由地去探索和创造。不妨就从一个小目标开始,先试着构建一个专注于某个画家或某个流派的小型数据集,体验一下从零到一的完整过程。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

更多推荐