1. 项目背景与核心需求
最近在做一个图片资源归档项目,需要定期从特定网站抓取最新图片。经过多次尝试,发现mm131这个站点的资源质量稳定且更新及时,但手动下载效率实在太低。于是决定用Python写个自动化脚本,既能高效抓取图片,又能自动识别新增内容。
这个爬虫需要解决三个核心问题:首先是高效抓取,要能在短时间内完成大量图片下载;其次是稳定性,要能应对网站的反爬机制;最后是可持续性,要能自动识别新内容并增量更新。下面我就把实战中总结的经验分享给大家,这个方案已经稳定运行了半年多,日均抓取上千张图片从没翻车。
2. 环境准备与基础配置
2.1 必备工具清单
工欲善其事必先利其器,先准备好这些工具:
- Python 3.8+(我用的3.9.7版本最稳定)
- Requests库(处理HTTP请求)
- BeautifulSoup4(解析HTML)
- lxml(加速解析)
- tqdm(显示进度条)
安装命令很简单:
pip install requests beautifulsoup4 lxml tqdm2.2 反爬应对策略
现在网站都有基础反爬措施,我们需要做好这些准备:
- 用户代理轮换:准备10个常见浏览器的User-Agent
- 请求间隔控制:随机延迟1-3秒
- 代理IP池(可选):如果是大规模抓取建议配置
- Referer设置:模拟从站内跳转
这是我的headers模板,实测有效:
headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.mm131.net/' }3. 网页结构分析与数据提取
3.1 分页机制解析
首先分析列表页规律,比如首页是:
https://www.mm131.net/xinggan/翻页后变成:
https://www.mm131.net/xinggan/list_1_2.html可以看出分页参数在list_1_2.html中,其中1是分类ID,2是页码。
3.2 图片链接提取技巧
通过开发者工具检查元素,发现图集链接藏在article标签里:
<article> <a href="/xinggan/1234.html" title="清纯写真"> <img src="https://img.mm131.net/pic/1234/1.jpg"> </article>用BeautifulSoup可以这样提取:
soup.find_all('article') for item in articles: title = item.a['title'] url = item.a['href']3.3 图片详情页抓取
进入图集详情页后,发现图片URL有规律:
https://img.mm131.net/pic/1234/1.jpg https://img.mm131.net/pic/1234/2.jpg ...只需要替换最后的数字就能获取所有图片。
4. 核心爬虫代码实现
4.1 分页抓取模块
先写个分页抓取函数:
def get_page_list(category, max_page): base_url = f'https://www.mm131.net/{category}/' for page in range(1, max_page+1): url = f'{base_url}list_1_{page}.html' if page > 1 else base_url response = requests.get(url, headers=headers) soup = BeautifulSoup(response.text, 'lxml') parse_album_list(soup) # 解析图集列表 time.sleep(random.uniform(1, 3)) # 随机延迟4.2 图片下载函数
下载函数要考虑重试机制:
def download_image(url, save_path): for retry in range(3): # 最多重试3次 try: resp = requests.get(url, headers=headers, stream=True) if resp.status_code == 200: with open(save_path, 'wb') as f: for chunk in resp: f.write(chunk) return True except Exception as e: print(f'下载失败,重试 {retry+1}/3') time.sleep(2) return False4.3 自动创建目录结构
按分类+日期自动创建目录:
def create_folder(category): today = datetime.now().strftime('%Y%m%d') path = f'./downloads/{category}/{today}/' if not os.path.exists(path): os.makedirs(path) return path5. 自动化更新策略
5.1 增量更新机制
每次运行先读取已下载记录:
def get_downloaded_ids(): try: with open('downloaded.json', 'r') as f: return set(json.load(f)) except FileNotFoundError: return set()下载完成后更新记录:
def update_record(album_id): downloaded = get_downloaded_ids() downloaded.add(album_id) with open('downloaded.json', 'w') as f: json.dump(list(downloaded), f)5.2 定时任务配置
用APScheduler设置每日定时运行:
from apscheduler.schedulers.blocking import BlockingScheduler sched = BlockingScheduler() @sched.scheduled_job('cron', hour=2) # 每天凌晨2点运行 def daily_job(): main() sched.start()6. 异常处理与日志记录
6.1 常见错误处理
针对不同错误采取不同策略:
try: response = requests.get(url, timeout=10) except requests.exceptions.Timeout: print('请求超时') except requests.exceptions.TooManyRedirects: print('重定向过多') except requests.exceptions.RequestException as e: print(f'严重错误:{e}')6.2 日志系统配置
使用logging模块记录运行情况:
import logging logging.basicConfig( filename='spider.log', level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logging.info('开始下载图集 %s', album_title) logging.warning('图片 %s 下载失败', image_url)7. 性能优化技巧
7.1 多线程加速
使用线程池提高下载速度:
from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=5) as executor: futures = [] for img_url in image_urls: futures.append(executor.submit(download_image, img_url, save_path)) for future in tqdm(as_completed(futures), total=len(futures)): pass # 等待所有任务完成7.2 内存优化
对于大文件使用流式下载:
response = requests.get(url, stream=True) with open(path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: # 过滤keep-alive数据块 f.write(chunk)8. 完整代码示例
最后给出整合后的核心代码:
import os import json import time import random import requests from bs4 import BeautifulSoup from datetime import datetime from tqdm import tqdm from concurrent.futures import ThreadPoolExecutor class MM131Spider: def __init__(self): self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://www.mm131.net/' } self.downloaded = self.load_records() def run(self, category='xinggan', max_page=3): for page in range(1, max_page+1): self.crawl_page(category, page) def crawl_page(self, category, page): url = self.build_url(category, page) soup = self.get_soup(url) albums = soup.find_all('article') with ThreadPoolExecutor(max_workers=5) as executor: list(tqdm( executor.map(self.process_album, albums), total=len(albums), desc=f'第{page}页进度' )) def process_album(self, album): album_url = album.a['href'] if album_url in self.downloaded: return images = self.get_album_images(album_url) save_dir = self.create_save_dir(album.a['title']) for idx, img_url in enumerate(images, 1): self.download_image(img_url, save_dir, idx) self.update_records(album_url) # 其他辅助方法...这套代码已经处理了大多数异常情况,在实际使用中可以根据需要调整线程数、延迟时间等参数。记得要合理控制请求频率,建议每秒钟不超过2次请求,避免给目标网站造成过大压力。