Python3 爬虫实战教程:静态网站全站爬取

1. 前言

不想啃复杂的解析库/框架?原生Python也能搞定全站爬取!

今天用 requests + 内置正则re 就能完成一个完整的公开练手电影站项目——从列表页分页抓链接,到详情页提取字段,再到JSON存储,最后加个多进程提速。练手站反爬不严格,放心用!

通过这个案例你会掌握:

  • 轻量级HTTP请求的封装
  • 静态HTML的快速正则解析
  • 列表-详情页的「分层跳转」逻辑
  • 多进程并行爬取提升效率
  • 入门级的爬虫坑修复

2. 技术准备

先搭个极简环境,几乎不用额外装东西:

  1. 确保本地有 Python 3.6+(兼容F-string和多进程Pool的主流写法)
  2. 一键安装核心依赖:
    pip install requests

💡 用到的都是轻量级/原生库

  • requests:比urllib写起来爽10倍的HTTP库
  • re:Python内置正则,解析简单静态HTML足够
  • logging:记录爬取状态,调试必备
  • json/os:数据存储和文件管理
  • multiprocessing:多进程提速(练手站单进程也行,但学了总有用)

3. 目标网站分析

练手站地址:https://ssr1.scrape.center/ 先戳进去看一眼,结构超级清晰:

3.1 页面结构

  • 列表页:URL规则固定为 /page/{页码}(从1到10,对应后面的常量TOTAL_PAGE),每页10部电影
  • 详情页:每个电影块的「电影名」是带class="name"的a标签,href是相对路径,要拼接base_url

3.2 要抓的字段

详情页固定有这些公开信息:

  • 封面图片URL
  • 电影名称
  • 电影类别数组
  • 上映时间
  • 评分
  • 剧情简介

4. 带坑修复的完整代码

我们直接上最终可用的完整版,里面解决了几个入门级爬虫常见的坑(比如转义警告、死等、文件名非法字符等),逻辑也更规范:

import json
from os import makedirs
from os.path import exists
import requests
import logging
import re
from urllib.parse import urljoin
import multiprocessing

# 日志配置:记录时间、级别、内容
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s - %(levelname)s: %(message)s')

# 核心常量
BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10
RESULTS_DIR = 'results'  # 存储JSON的文件夹
if not exists(RESULTS_DIR):
    makedirs(RESULTS_DIR)


def scrape_page(url):
    """
    通用「套娃」爬取函数:统一处理请求、状态码、异常
    :param url: 目标URL
    :return: 成功返回HTML,失败返回None
    """
    logging.info('正在爬取 %s...', url)
    try:
        # 坑1:增加 timeout(10秒)防止死等
        response = requests.get(url, timeout=10)
        if response.status_code == 200:
            return response.text
        logging.error('无效状态码 %d,URL:%s', response.status_code, url)
    except requests.RequestException:
        logging.error('请求异常!URL:%s', url, exc_info=True)


def scrape_index(page):
    """
    构造并爬取指定页码的列表页
    :param page: 页码
    :return: 列表页HTML
    """
    index_url = f'{BASE_URL}/page/{page}'
    return scrape_page(index_url)


def parse_index(html):
    """
    解析列表页,返回详情页URL生成器(节省内存)
    :param html: 列表页HTML
    :return: 详情页URL生成器
    """
    if not html: return []  # 坑2:空值保护,防止后续正则崩溃
    # 坑3:所有正则前加 r'' 前缀,解决转义警告
    pattern = re.compile(r'<a.*?href="(.*?)".*?class="name">')
    items = re.findall(pattern, html)
    if not items:
        return []
    for item in items:
        detail_url = urljoin(BASE_URL, item)  # 自动拼接base_url(处理相对路径)
        logging.info('获取到详情页链接:%s', detail_url)
        yield detail_url


def scrape_detail(url):
    """
    爬取详情页
    :param url: 详情页URL
    :return: 详情页HTML
    """
    return scrape_page(url)


def parse_detail(html):
    """
    解析详情页,返回电影信息字典
    :param html: 详情页HTML
    :return: 电影信息字典
    """
    if not html: return None  # 空值保护

    # 逐个定义正则,加re.S让.匹配换行符
    cover_pattern = re.compile(r'class="item.*?<img.*?src="(.*?)".*?class="cover">', re.S)
    name_pattern = re.compile(r'<h2.*?>(.*?)</h2>')
    categories_pattern = re.compile(r'<button.*?category.*?<span>(.*?)</span>.*?</button>', re.S)
    published_at_pattern = re.compile(r'(\d{4}-\d{2}-\d{2})\s?上映')
    drama_pattern = re.compile(r'<div.*?drama.*?>.*?<p.*?>(.*?)</p>', re.S)
    score_pattern = re.compile(r'<p.*?score.*?>(.*?)</p>', re.S)

    # 逐个提取字段,找不到给默认值,保证程序不会崩
    name_match = re.search(name_pattern, html)
    name = name_match.group(1).strip() if name_match else "未命名电影"

    cover_match = re.search(cover_pattern, html)
    cover = cover_match.group(1).strip() if cover_match else None

    categories = re.findall(categories_pattern, html)

    pub_match = re.search(published_at_pattern, html)
    published_at = pub_match.group(1) if pub_match else None

    drama_match = re.search(drama_pattern, html)
    drama = drama_match.group(1).strip() if drama_match else None

    score_match = re.search(score_pattern, html)
    score = float(score_match.group(1).strip()) if score_match else None

    return {
        'cover': cover,
        'name': name,
        'categories': categories,
        'published_at': published_at,
        'drama': drama,
        'score': score
    }


def save_data(data):
    """
    保存单条电影数据到JSON文件
    :param data: 电影信息字典
    """
    if not data: return
    name = data.get('name')
    # 坑4:完整清洗文件名非法字符(Windows/Linux通用)
    safe_name = re.sub(r'[\\/:*?"<>|]', '_', name)
    data_path = f'{RESULTS_DIR}/{safe_name}.json'

    # 坑5:用with open自动关闭文件,避免资源泄漏
    try:
        with open(data_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)  # ensure_ascii=False防止中文乱码
        logging.info('已成功保存:%s', data_path)
    except Exception as e:
        logging.error('保存失败!文件路径:%s,错误:%s', data_path, e)


def main(page):
    """
    单页主处理函数:串联整个单页流程
    :param page: 页码
    """
    index_html = scrape_index(page)
    detail_urls = parse_index(index_html)
    for detail_url in detail_urls:
        detail_html = scrape_detail(detail_url)
        data = parse_detail(detail_html)
        if data:
            logging.info('获取到电影数据:%s', data['name'])
            save_data(data)


if __name__ == '__main__':
    # 多进程并行爬取(单进程可以直接循环 main(page) for page in range(1, 11))
    pool = multiprocessing.Pool()
    pages = range(1, TOTAL_PAGE + 1)
    pool.map(main, pages)
    pool.close()  # 关闭进程池,不再接受新任务
    pool.join()  # 等待所有子进程完成

5. 核心流程拆解(方便理解)

为了让你更好上手,我们把完整代码的核心逻辑拆成3步:

5.1 分层抓内容

不管列表页还是详情页,都用统一的scrape_page函数——避免重复写代码,统一处理异常。

5.2 分层解析

  • 列表页解析:用正则找class="name"的a标签,拼接base_url生成详情页链接(用生成器yield返回,节省内存)
  • 详情页解析:用正则逐个匹配目标字段,找不到给默认值

5.3 数据存储+多进程

  • 存储:用电影名做文件名(清洗非法字符),保存到results文件夹
  • 多进程:用multiprocessing.Pool并行处理10页,比单进程快很多

6. 进阶优化建议

完整版已经能用,但如果想爬更复杂的站,可以试试这些优化:

  1. 加合理请求头:目标站没封,但大多数站会看User-Agent,比如加:
    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'
    }
    # 然后在scrape_page里用 requests.get(url, headers=headers, timeout=10)
  2. 随机延迟:每次请求间隔1-3秒,避免被封IP
  3. 代理支持:用代理池解决IP限制
  4. 断点续爬:用数据库或JSON记录已爬取的URL,中断后直接跳过
  5. 替换解析库:如果正则太复杂,可以换BeautifulSoup4lxml

7. 总结

今天的案例覆盖了静态爬虫的核心流程:抓→分层解析→存,还加了多进程提速和5个入门级坑修复。练手站的逻辑很清晰,你可以试着改成自己需要的字段,或者换个公开的静态练手站试试手!

最后提醒一句:爬虫虽好,不要爬取未经授权的内容,不要给目标站造成太大的服务器压力哦~