Python爬虫实战:博物馆官网的“展览预告/正在热展”栏目,抓取展览名称、精确展期、具体展厅位置以及票务/预约规则(附CSV导出)!

㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!

㊗️爬虫难度指数:⭐⭐⭐

🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:

      • [🌟 开篇语](#🌟 开篇语)
      • [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
      • [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
      • [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
      • [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
      • [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
      • [6️⃣ 核心实现:请求层(Fetcher)](#6️⃣ 核心实现:请求层(Fetcher))
      • [7️⃣ 核心实现:解析层(Parser)------ 正则与启发式提取 🕵️](#7️⃣ 核心实现:解析层(Parser)—— 正则与启发式提取 🕵️)
      • [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
      • [9️⃣ 运行方式与结果展示(必写)](#9️⃣ 运行方式与结果展示(必写))
      • [🔟 常见问题与排错(必看经验)](#🔟 常见问题与排错(必看经验))
      • [1️⃣1️⃣ 进阶优化(可选但加分)](#1️⃣1️⃣ 进阶优化(可选但加分))
      • [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
      • [🌟 文末](#🌟 文末)
        • [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
        • [✅ 互动征集](#✅ 互动征集)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈

💕订阅后更新会优先推送,按目录学习更高效💯~

1️⃣ 摘要(Abstract)

项目代号: Art_Radar
核心目标: 遍历目标博物馆官网的"展览预告/正在热展"栏目,抓取展览名称、精确展期、具体展厅位置以及票务/预约规则。
工具栈: Requests (翻页请求) + BeautifulSoup (DOM 解析) + Re (正则表达式提取非结构化信息)。
难点攻克:

  • 非结构化文本挖掘 :门票价格通常写在"一段话"里(如"成人票60元,学生半价"),没有专门的 class="price"。我们需要写正则去匹配它。
  • 深度遍历 :必须采用 List-to-Detail (列表-详情)模式,因为列表页信息严重不足。
    最终产出: 一份 museum_exhibitions.csv,让你一眼看清周末去哪看展。

💡 读完这篇你将获得:

  1. 正则实战能力:学会用正则表达式从长文本中提取"价格(数字+元)"和"日期范围"。
  2. 二级爬虫架构:掌握"先抓列表链接,再逐个请求详情"的标准爬虫设计模式。
  3. 文本清洗策略 :如何去除 HTML 中的  、多余空行,还原干净的介绍文本。

2️⃣ 背景与需求(Why)

  • 为什么要爬: 很多好展览展期短,容易错过。官方公众号推送不及时,而官网通常是最全的。做一个聚合器,每周五自动发给自己,规划周末行程。

  • 目标站点: 以一个典型的公立博物馆结构为例(通常有 /exhibitions/current/news/exhibition 页面)。

  • 目标字段清单:

    • Exhibition_Title (展览名,如 "只此青绿------宋代山水画特展")
    • Date_Range (展期,如 "2023.10.1 - 2024.1.15")
    • Location (地点,如 "三楼书画馆")
    • Ticket_Info (票务,如 "免费预约" 或 "¥60")
    • Status (状态,正在热展/即将开幕 - 可选)

3️⃣ 合规与注意事项(必写)

文化机构的服务器通常预算有限,不仅要 ,还要

  • 深层抓取的风险: 我们采用"列表->详情"模式,请求量是 1(列表页) + N(详情页)。如果列表页有 20 个展,就要发 21 次请求。必须 在请求详情页时加入 time.sleep(2),否则极易被封 IP 或导致对方服务器卡顿。
  • 尊重版权: 展览介绍文案和海报图片版权归主办方所有。爬取内容仅供个人浏览索引,严禁直接复制图文搭建"山寨官网"。
  • 预约通道: 我们的爬虫只读取 预约规则,绝不尝试自动化抢票或刷预约号,这是严重的违规行为。

4️⃣ 技术选型与整体流程(What/How)

技术痛点分析:

在详情页中,Html 可能是这样的:

html 复制代码
<div class="content">
    <p>本次展览将于10月1日在三号厅开幕。</p>
    <p>票价:全价票60元,老人免票。</p>
</div>

这里没有 <span id="price">
解决方案:

我们提取整个 .content 的纯文本,然后用 Python 的 if "票" in text 或正则表达式 \d+元 来"猜测"票价。

🛠️ 流程图:

Fetch 列表页 → Parse 展览链接与状态 → Loop 遍历链接 → Fetch 详情页 → 提取正文 → Regex 匹配日期/地点/票价 → Save CSV

5️⃣ 环境准备与依赖安装(可复现)

Python 版本: 3.9+
依赖安装:

bash 复制代码
pip install requests beautifulsoup4 pandas

目录结构:

text 复制代码
art_radar/
├── radar.py            # 主程序
├── keywords_conf.py    # (可选) 存放提取规则的关键词配置
└── results/            # 结果存放

6️⃣ 核心实现:请求层(Fetcher)

为了支持"列表"和"详情"两种请求,我们封装一个通用的 Fetcher,并加上重试机制,因为详情页有时候链接会坏死(404)。

python 复制代码
import requests
import time
import random

# 全局 Session 对象,复用 TCP 连接,提高效率
session = requests.Session()
session.headers.update({
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Referer': 'https://www.google.com/'
})

def fetch_content(url):
    """
    通用抓取函数:支持重试和自动编码处理
    """
    if not url.startswith('http'):
        print(f"⚠️ Invalid URL: {url}")
        return None
        
    for attempt in range(3):
        try:
            # 详情页请求必须有随机延时!
            # 放在请求前,确保每次调用都会暂停
            time.sleep(random.uniform(1.5, 3.0))
            
            response = session.get(url, timeout=10)
            response.raise_for_status()
            
            # 处理中文乱码:如果 meta charset 是 gbk,requests 可能会猜错
            # 这里用 apparent_encoding 比较稳妥
            if response.encoding == 'ISO-8859-1':
                response.encoding = response.apparent_encoding
                
            return response.text
            
        except requests.RequestException as e:
            print(f"⚠️ Fetch Error (Try {attempt+1}): {url} - {e}")
            
    return None

7️⃣ 核心实现:解析层(Parser)------ 正则与启发式提取 🕵️

这是最考验逻辑的地方。我们将编写两个解析器:一个处理列表,一个处理详情文本。

Helper Function: 文本嗅探器

python 复制代码
import re

def heuristic_extract(text_block):
    """
    从一大段文本中,利用关键词和正则 '猜测' 关键信息
    """
    info = {
        'price': '详情见公告',
        'location': '馆内'
    }
    
    # 1. 提取票价 (Heuristic: 寻找包含 "元" 或 "票" 的句子)
    # 匹配模式:数字 + "元" (e.g., "60元", "20元")
    # 或者关键词 "免费", "Free"
    if "免费" in text_block or "Free" in text_block:
        info['price'] = "免费/Free"
    else:
        # 查找类似 "票价:30元" 或者 "60元/人"
        prices = re.findall(r'(\d+)\s*元', text_block)
        if prices:
            # 取最大值通常是全价票,或者列出所有
            info['price'] = f"{'/'.join(prices)} 元"
            
    # 2. 提取地点 (Heuristic: 寻找 "厅", "馆", "层")
    # 这比较难,通常寻找短句。此处仅做简单示例。
    # 实际项目中可能需要定位到 "地点:" 关键词后面的文本
    loc_match = re.search(r'地点[::]\s*([^\n,。]+)', text_block)
    if loc_match:
        info['location'] = loc_match.group(1).strip()
    elif "三层" in text_block:
        info['location'] = "三层展厅" # 示例硬编码规则
        
    return info

主解析逻辑:

python 复制代码
from bs4 import BeautifulSoup
from urllib.parse import urljoin

BASE_DOMAIN = "https://www.example-museum.com" # 假设的域名

def parse_exhibition_list(html):
    """
    解析展览列表页,返回展览的基本信息和详情页链接
    """
    soup = BeautifulSoup(html, 'html.parser')
    exhibitions = []
    
    # 假设列表结构:ul.exhibit-list > li > a (包含标题) + span (包含时间)
    items = soup.find_all('div', class_='exhibition-item') # 需根据实际网站调整
    
    for item in items:
        try:
            # 1. 提取标题和链接
            title_tag = item.find('h3').find('a')
            title = title_tag.text.strip()
            link = urljoin(BASE_DOMAIN, title_tag['href'])
            
            # 2. 提取列表页展示的日期 (如果有)
            date_tag = item.find('span', class_='date')
            date_str = date_tag.text.strip() if date_tag else "N/A"
            
            exhibitions.append({
                'title': title,
                'url': link,
                'list_date': date_str # 暂时存下来,详情页可能还有更详细的
            })
        except AttributeError:
            continue
            
    return exhibitions

def parse_detail_page(html, basic_info):
    """
    进入详情页,挖掘更深度的信息
    """
    soup = BeautifulSoup(html, 'html.parser')
    
    # 1. 提取正文纯文本
    # 通常在 div.article-content 或 div.description
    content_div = soup.find('div', class_='article-content')
    if not content_div:
        return basic_info # 如果解析失败,至少返回列表页拿到的信息
        
    # 获取纯文本,去除 HTML 标签
    full_text = content_div.get_text(separator='\n', strip=True)
    
    # 2. 调用启发式提取器
    extra_info = heuristic_extract(full_text)
    
    # 3. 合并数据
    final_data = basic_info.copy()
    final_data['location'] = extra_info['location']
    final_data['ticket'] = extra_info['price']
    
    # 4. (可选) 尝试在详情页找更精确的日期
    # 有时候列表页写 "2023年",详情页写 "2023.10.1 - 10.7"
    # 这里可以再加正则匹配日期的逻辑
    
    return final_data

8️⃣ 数据存储与导出(Storage)

这次我们稍微高级一点,除了存 CSV,我们把包含"免费"字眼的展览标记出来(Highlighter),方便后续筛选。

python 复制代码
import pandas as pd
import os
from datetime import datetime

def save_exhibitions(data_list):
    if not data_list:
        print("⚠️ No exhibitions found.")
        return

    df = pd.DataFrame(data_list)
    
    # 数据清洗
    df['title'] = df['title'].str.replace('\n', '')
    
    # 增加一列:是否值得去(示例逻辑)
    # 如果是免费的,或者标题里有 "特展" 两个字
    df['Recommendation'] = df.apply(
        lambda x: '⭐⭐⭐' if ('免费' in str(x['ticket']) or '特展' in x['title']) else '', 
        axis=1
    )
    
    # 调整列顺序
    cols = ['title', 'list_date', 'location', 'ticket', 'Recommendation', 'url']
    # 防止某些列不存在的报错
    existing_cols = [c for c in cols if c in df.columns]
    df = df[existing_cols]
    
    # 文件名带时间
    filename = f"exhibitions_{datetime.now().strftime('%Y%m%d')}.csv"
    output_path = os.path.join("results", filename)
    os.makedirs("results", exist_ok=True)
    
    df.to_csv(output_path, index=False, encoding='utf-8-sig')
    print(f"💾 Saved {len(df)} exhibitions to {output_path}")
    print("\n🎫 Top Picks:")
    print(df[['title', 'ticket', 'Recommendation']].head().to_markdown(index=False))

9️⃣ 运行方式与结果展示(必写)

这里我们模拟整个流程:先抓列表,再循环抓详情。

python 复制代码
if __name__ == "__main__":
    # 假设的目标 URL (需要替换为真实的博物馆展览列表页 URL)
    # 例如:某美术馆的 "当前展览" 页面
    target_list_url = "https://www.example-art-museum.org/exhibitions/current"
    
    print("🎨 Art Radar Initiated...")
    
    # Step 1: 获取列表
    print(f"📡 Scanning List: {target_list_url}")
    list_html = fetch_content(target_list_url)
    
    if list_html:
        exhibitions = parse_exhibition_list(list_html)
        print(f"✅ Found {len(exhibitions)} exhibitions. Digging details...")
        
        full_data = []
        
        # Step 2: 深度遍历详情页
        for i, expo in enumerate(exhibitions):
            print(f"   [{i+1}/{len(exhibitions)}] Analyzing: {expo['title']} ...")
            
            detail_html = fetch_content(expo['url'])
            if detail_html:
                detailed_expo = parse_detail_page(detail_html, expo)
                full_data.append(detailed_expo)
            else:
                # 如果详情页抓失败,至少保留基本信息
                full_data.append(expo)
                
        # Step 3: 保存
        save_exhibitions(full_data)
        
    else:
        print("❌ Failed to fetch list page.")

📊 示例结果展示 (CSV):

title list_date location ticket Recommendation
达芬奇的手稿------文艺复兴特展 2023.10.01-12.31 二楼特展厅 120/80 元 ⭐⭐⭐
馆藏明清瓷器常设展 常设展览 四楼瓷器馆 免费/Free ⭐⭐⭐
现代水墨实验展 2023.11.15-11.20 一楼侧厅 详情见公告

🔟 常见问题与排错(必看经验)

  1. 正则提取不到价格?

    • 原因: 网站没写"元",写的是"CNY 60"或者"票价:陆拾"。
    • 解法: 正则表达式需要不断迭代。可以把没提取到的详情页文本保存下来,人工观察规律,优化 Regex。比如增加 r'CNY\s*(\d+)' 的匹配。
  2. 详情页是 PDF?

    • 现象: 点击展览链接,直接下载了一个 .pdf 文件。
    • 原因: 很多传统机构习惯把公告做成 PDF 上传。
    • 解法: 这种 BeautifulSoup 解析不了。需要检测 Content-Type 是否为 application/pdf,如果是,需要用 pdfplumber 库来读取 PDF 文本,这属于进阶内容。
  3. 日期格式千奇百怪?

    • 现象: 有的写 "10.1-10.7",有的写 "Oct 1st to 7th",有的写 "每周一闭馆"。
    • 解法: 不要试图把日期标准化成 Date 对象(太难了),直接作为字符串存下来。让人类自己去读,往往比写代码解析更高效。

1️⃣1️⃣ 进阶优化(可选但加分)

  • 自然语言处理 (NLP) 介入:

    • 使用 spacy 或中文分词工具 jieba,识别文本中的 Location (LOC)Date (DATE) 实体,比正则匹配更智能,能识别出"故宫博物院午门展厅"这样的复杂地点。
  • 日历集成 (iCal):

    • 使用 ics 库,把抓取到的展览生成 .ics 日历文件。用户双击文件,就能把展览直接添加到手机日历里,这才是极致的用户体验!

1️⃣2️⃣ 总结与延伸阅读

复盘:

本篇我们不仅是一个爬虫工程师,更是一个**"信息抽取工程师"。我们面对的不再是整齐的表格,而是杂乱的自然语言。通过 列表遍历正则嗅探**,我们成功地将非结构化的文本转化为了决策数据。

下一步:

既然搞定了博物馆,为什么不试试**"Livehouse 演出信息"或者"电影节排片表"**?它们的逻辑是通用的。去发现城市里更多美好的角落吧!🌃

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。

相关推荐
啥都想学点2 小时前
关于制作技术视频讲解的问卷调查
python
喵手2 小时前
Python爬虫实战:电商实体消歧完整实战 - 从混乱店铺名到标准化知识库的工程化实现,一文带你搞定!
爬虫·python·算法·爬虫实战·零基础python爬虫教学·同名实体消除·从混乱店铺名到标准化知识库
aluluka2 小时前
Emacs折腾日记(三十六)——打造个人笔记系统
笔记·python·emacs
黎子越2 小时前
python相关练习
java·前端·python
小白学大数据2 小时前
实测数据:多进程、多线程、异步协程爬虫速度对比
开发语言·爬虫·python·php
小鸡吃米…2 小时前
机器学习 - 精确率与召回率
人工智能·python·机器学习
sonrisa_2 小时前
Python同一类不同方法中变量值的传递
开发语言·windows·python
逻极3 小时前
OpenClaw「Clawdbot/Moltbot」 深入解析:核心架构深度剖析
python·ai·架构·agent·ai编程·moltbot·openclaw
sayang_shao3 小时前
C++ ONNX Runtime 与 Python Ultralytics 库实现 YOLOv8 模型检测的区别
c++·python·yolo