ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python爬虫实战:从jj20.com批量下载高清壁纸的完整方案

2026/8/4 9:30:31 拓冰建站 浏览量
Python爬虫实战:从jj20.com批量下载高清壁纸的完整方案

1. 项目概述与核心思路

最近在整理电脑桌面,想换一批高质量的风景壁纸,但手动去网站一张张下载实在太费时间。作为一个常年和代码打交道的开发者,我第一时间想到的就是用Python写个脚本,把喜欢的壁纸网站“打包”下来。这次我选择的目标是jj20.com,一个资源丰富、分类清晰的壁纸网站。这个项目看似简单,就是一个基础的网络爬虫,但其中涉及到的反爬策略应对、图片链接解析、以及高效的文件存储,每一个环节都有不少值得深究的细节。如果你也厌倦了手动收集资源,或者想系统学习Python爬虫如何应对一个结构稍复杂的图片站,那么跟着我走一遍这个流程,你不仅能收获一整套壁纸,更能掌握一套实用的爬虫实战方法。

整个项目的核心思路非常清晰:模拟浏览器访问网站、解析网页HTML结构、定位并提取图片的真实下载地址、最后将图片文件保存到本地。但jj20.com这类网站通常不会直接把高清大图的链接放在页面上,而是通过缩略图或JavaScript动态加载,这就需要我们多花点心思去分析网络请求。接下来,我会从环境准备开始,一步步拆解如何稳健、高效地完成这次壁纸抓取任务。

2. 环境准备与工具选型

工欲善其事,必先利其器。在开始写代码之前,我们需要搭建好Python环境并安装必要的库。这个项目对Python版本要求不高,Python 3.6及以上均可。我个人的开发环境是Python 3.9,兼顾了稳定性和对新特性的支持。

2.1 核心库安装与作用解析

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

pip install requests beautifulsoup4 lxml
  • Requests: 这是Python中用于发送HTTP请求的明星库,比原生的urllib更简洁易用。我们将用它来获取网页的HTML源代码。
  • BeautifulSoup4 (bs4): 用于解析HTML或XML文档,它能帮助我们以非常直观的方式从复杂的网页标签中提取出需要的数据,比如图片的链接、标题等。
  • lxml: 这是一个高性能的HTML/XML解析器。BeautifulSoup可以搭配不同的解析器,lxml的速度通常比Python内置的html.parser快很多,特别是在处理大量页面时,优势明显。

注意:有些教程可能会推荐使用urllib,但对于新手而言,requests的API设计更加友好,错误处理也更清晰,能让你更专注于爬虫逻辑本身。

2.2 辅助工具:浏览器开发者工具

写爬虫,一半靠代码,另一半靠对目标网站的分析。而浏览器自带的“开发者工具”(按F12键打开)就是我们最重要的分析武器。我们将频繁使用其中的两个面板:

  1. 元素(Elements)面板:用于查看网页的DOM结构。我们可以在这里找到图片标签(<img>)的存放位置,观察其属性(如src,>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' }
  2. 请求频率限制:如果短时间内发出大量请求,服务器可能会暂时封禁你的IP。

    • 应对:在请求之间加入随机延时。使用time.sleep(),并配合random模块让延时时间不规律,模拟人类操作。
    import time import random time.sleep(random.uniform(1, 3)) # 随机休眠1到3秒
  3. 动态内容加载:部分数据可能通过JavaScript异步加载(Ajax)。初始HTML中只有骨架,图片链接是后续填充的。

    • 应对:在“网络”面板中过滤XHR/Fetch请求,寻找获取图片数据的真实API接口。然后,我们的爬虫直接去请求这个接口地址,而不是解析初始HTML。
  4. 链接混淆或参数校验:图片链接可能带有时间戳、加密签名等一次性参数,直接复制页面上的链接可能无法访问。

    • 应对:仔细分析图片请求的URL规律,尝试找出固定部分和可变参数的计算方法。有时,参数可能来源于页面中某个隐藏的JavaScript变量。
  5. 3.3 确定图片链接提取策略

    经过对jj20.com多个页面的分析,我发现了一种比较可靠的路径(网站结构可能变更,此方法为示例):

    1. 从列表页的HTML中,可以解析出每个壁纸项的<a>标签,其href属性指向详情页。
    2. 进入详情页后,最高清的图片往往在一个具有特定class(例如.pic-large#bigImg)的<img>标签的src属性里。但有时这个src是一个低清图,真正的原图链接藏在像># utils.py import requests import time import random from typing import Optional, Dict, Any def get_page(url: str, headers: Optional[Dict] = None, timeout: int = 10, retry: int = 3) -> Optional[str]: """ 发送HTTP GET请求,获取页面内容,包含重试机制。 参数: url: 目标URL headers: 请求头,默认为None时会使用默认浏览器UA timeout: 请求超时时间(秒) retry: 失败重试次数 返回: 成功则返回页面文本内容,失败则返回None并打印错误信息 """ if headers is None: 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' } for attempt in range(retry): try: response = requests.get(url, headers=headers, timeout=timeout) # 检查HTTP状态码,200为成功 response.raise_for_status() # 检查编码,避免乱码 response.encoding = response.apparent_encoding or 'utf-8' # 随机延时,避免请求过快 time.sleep(random.uniform(1, 2)) return response.text except requests.exceptions.RequestException as e: print(f"第{attempt + 1}次请求失败: {url}, 错误: {e}") if attempt < retry - 1: wait_time = (attempt + 1) * 2 # 重试等待时间递增 print(f"{wait_time}秒后重试...") time.sleep(wait_time) else: print(f"请求{url}最终失败,跳过。") return None

      这个函数封装了请求、编码处理、异常重试和礼貌延时,是爬虫稳定运行的基础。

      4.2 解析列表页获取详情链接

      接下来,在parser.py中编写解析列表页的函数。我们需要从类似/bz/ktmh/list_33_1.html的页面中,提取出所有壁纸详情页的链接。

      # parser.py from bs4 import BeautifulSoup from typing import List import re def parse_list_page(html_content: str, base_url: str = "http://www.jj20.com") -> List[str]: """ 解析壁纸列表页,提取所有详情页的URL。 参数: html_content: 列表页的HTML文本 base_url: 网站基础URL,用于拼接相对链接 返回: 详情页URL的列表 """ if not html_content: return [] soup = BeautifulSoup(html_content, 'lxml') detail_urls = [] # 需要根据实际网站结构调整选择器 # 例如,列表项可能包裹在 <li class="item"> 下的 <a> 标签里 link_elements = soup.select('ul.pic-list li a') # 这是一个示例选择器,需按实际情况修改 for link in link_elements: href = link.get('href') if href: # 处理相对链接,拼接成完整URL if href.startswith('/'): full_url = base_url + href elif not href.startswith('http'): # 如果是不标准的相对链接,可能需要更复杂的处理 full_url = base_url + '/' + href.lstrip('/') else: full_url = href # 去重并添加到列表 if full_url not in detail_urls: detail_urls.append(full_url) return detail_urls

      实操心得:网页结构可能会改版,ul.pic-list li a这个CSS选择器不一定永远正确。最可靠的方法是:在浏览器中用“检查”功能定位到一个缩略图链接,右键“Copy” -> “Copy selector”,就能得到精确的选择器路径。解析器要写得灵活,随时准备根据网站变化调整。

      4.3 解析详情页获取高清图链接

      这是最关键的一步。我们需要在详情页的HTML中,定位到最高清的那张图片的真实地址。

      # parser.py def parse_detail_page(html_content: str) -> Dict[str, str]: """ 解析壁纸详情页,提取高清图片URL和图片标题。 参数: html_content: 详情页的HTML文本 返回: 包含 'img_url' 和 'title' 的字典。如果解析失败,值可能为空字符串。 """ result = {'img_url': '', 'title': ''} if not html_content: return result soup = BeautifulSoup(html_content, 'lxml') # 1. 尝试多种方式获取高清图片URL img_url = '' # 策略一:查找带有特定属性(如data-original)的高清图img标签 hd_img = soup.select_one('img#bigImg, img.pic-large, img[data-original], img[data-src]') if hd_img: img_url = hd_img.get('data-original') or hd_img.get('data-src') or hd_img.get('src') else: # 策略二:如果找不到,尝试查找页面中最大的图片,或特定的div容器内的图片 all_imgs = soup.find_all('img') # 这里可以添加一些启发式规则,比如筛选src中包含特定关键词(如‘big’、‘large’)的图片 for img in all_imgs: src = img.get('src', '') if src and ('big' in src or 'large' in src or 'wallpaper' in src): img_url = src break # 2. 清理和补全图片URL if img_url: # 去除可能的URL首尾空格 img_url = img_url.strip() # 如果图片URL是相对路径,需要补全(这里需要知道图片域,可能与主站不同) if img_url.startswith('//'): img_url = 'http:' + img_url elif img_url.startswith('/'): # 注意:图片可能托管在单独的CDN域名下,这里简单处理,实际情况需分析 img_url = 'http://www.jj20.com' + img_url result['img_url'] = img_url # 3. 提取壁纸标题(用于本地保存文件名) title_elem = soup.select_one('h1.title, div.pic-title') if title_elem: title = title_elem.get_text().strip() # 清理文件名中不合法的字符 title = re.sub(r'[\\/*?:"<>|]', '_', title) result['title'] = title[:100] # 限制文件名长度 else: # 如果没有明确标题,可以用其他信息替代,如详情页URL的ID result['title'] = 'untitled' return result

      这个函数体现了爬虫解析的“防御性编程”思想:提供多种备选解析策略,并对结果进行清洗和校验,确保后续流程的健壮性。

      4.4 实现图片下载与本地存储

      获取到高清图片的URL后,最后一步就是下载并保存。我们在downloader.py中实现这个功能。

      # downloader.py import os import requests from urllib.parse import urlparse def download_image(img_url: str, save_dir: str, filename: str, headers: Dict = None) -> bool: """ 下载单张图片并保存到本地。 参数: img_url: 图片的完整URL save_dir: 本地保存目录 filename: 保存的文件名(不含扩展名) headers: 请求头,用于图片请求 返回: 成功返回True,失败返回False """ if not img_url: print(f"无效的图片URL,跳过下载。") return False if headers is None: 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', 'Referer': 'http://www.jj20.com/' # 添加Referer,有些图片服务器会校验来源 } # 从URL中提取文件扩展名 parsed_url = urlparse(img_url) path = parsed_url.path # 获取扩展名,如 .jpg, .png _, ext = os.path.splitext(path) if not ext: # 如果URL中没有扩展名,可以尝试从Content-Type判断,这里先给一个默认值 ext = '.jpg' # 确保保存目录存在 os.makedirs(save_dir, exist_ok=True) # 构建完整的保存路径 save_path = os.path.join(save_dir, f"{filename}{ext}") try: print(f"正在下载: {filename}{ext}") response = requests.get(img_url, headers=headers, stream=True, timeout=30) response.raise_for_status() # 以二进制流方式写入文件,适合大文件 with open(save_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) print(f"下载成功: {save_path}") return True except requests.exceptions.RequestException as e: print(f"下载失败 {img_url}: {e}") # 可选:删除下载失败的空文件或部分文件 if os.path.exists(save_path): os.remove(save_path) return False except IOError as e: print(f"文件写入失败 {save_path}: {e}") return False

      这里使用了stream=True参数进行流式下载,这对于下载较大的图片文件非常有用,可以避免一次性将整个文件加载到内存中。同时,添加了Referer请求头,模拟从网站页面跳转过来的行为,能绕过一些简单的防盗链措施。

      5. 主流程整合与分页抓取

      现在,我们把各个模块像拼积木一样组合起来,并在main.pycrawler.py中实现主控制逻辑,特别是处理多页列表。

      # crawler.py import time import random from utils import get_page from parser import parse_list_page, parse_detail_page from downloader import download_image class WallpaperCrawler: def __init__(self, base_url="http://www.jj20.com", save_root="./wallpapers"): self.base_url = base_url self.save_root = save_root self.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' } def crawl_category(self, category_path, start_page=1, end_page=5): """ 抓取某个分类下的多页壁纸。 参数: category_path: 分类路径,如 '/bz/ktmh/list_33_' start_page: 起始页码 end_page: 结束页码 """ for page in range(start_page, end_page + 1): print(f"\n=== 开始处理第 {page} 页 ===") # 构建列表页URL,例如:http://www.jj20.com/bz/ktmh/list_33_1.html list_url = f"{self.base_url}{category_path}{page}.html" # 1. 获取列表页HTML list_html = get_page(list_url, self.headers) if not list_html: print(f"获取列表页失败: {list_url}") continue # 2. 解析列表页,得到详情页链接列表 detail_urls = parse_list_page(list_html, self.base_url) print(f"本页找到 {len(detail_urls)} 个壁纸详情链接。") # 3. 遍历每个详情页 for idx, detail_url in enumerate(detail_urls): print(f"\n处理第 {idx + 1}/{len(detail_urls)} 个: {detail_url}") # 获取详情页HTML detail_html = get_page(detail_url, self.headers) if not detail_html: print(f"获取详情页失败,跳过。") continue # 解析详情页,获取高清图链接和标题 img_info = parse_detail_page(detail_html) img_url = img_info.get('img_url') title = img_info.get('title') if not img_url: print(f"无法解析出高清图片URL,跳过。") continue # 4. 下载图片 # 创建按分类和日期命名的子目录 import datetime date_str = datetime.datetime.now().strftime("%Y%m%d") # 从category_path中提取分类名,例如从‘/bz/ktmh/list_33_’提取‘ktmh’ category_name = category_path.split('/')[-2] if len(category_path.split('/')) >= 2 else 'unknown' save_dir = os.path.join(self.save_root, category_name, date_str) # 使用标题作为文件名,如果标题为空则使用详情页URL的ID部分 if not title or title == 'untitled': # 简单地从URL中提取ID import re match = re.search(r'/(\d+)\.html$', detail_url) filename = match.group(1) if match else str(int(time.time())) else: filename = title success = download_image(img_url, save_dir, filename, self.headers) # 更长的随机延时,避免对单个详情页请求过快 time.sleep(random.uniform(2, 4)) # 处理完一页后,延时稍长 print(f"第 {page} 页处理完成,等待片刻...") time.sleep(random.uniform(3, 6)) # main.py if __name__ == "__main__": crawler = WallpaperCrawler(save_root="./downloaded_wallpapers") # 示例:抓取“卡通漫画”分类的前3页 # 你需要根据网站实际URL结构调整 category_path # 观察发现列表页URL模式为:/bz/ktmh/list_33_{page}.html crawler.crawl_category(category_path='/bz/ktmh/list_33_', start_page=1, end_page=3) print("\n所有任务完成!")

      这个主流程清晰地展示了爬虫的工作链条:构造列表页URL -> 获取并解析 -> 得到N个详情页URL -> 逐个访问详情页 -> 解析出图片真实地址 -> 下载保存。通过crawl_category方法,我们可以轻松控制抓取的分类和页码范围。

      6. 高级技巧与优化策略

      一个能跑起来的爬虫只是开始,一个健壮、高效、可持续的爬虫才是目标。下面分享几个进阶技巧。

      6.1 处理动态加载与Ajax请求

      如果发现列表页的图片数据是滚动到底部后通过JavaScript加载的,那么直接解析初始HTML是拿不到全部数据的。此时需要分析“网络”面板中的XHR请求。

      1. 打开浏览器开发者工具,进入“网络”面板,并勾选“保留日志”。
      2. 滚动列表页到底部,触发新内容加载。
      3. 在“网络”面板中,筛选XHRFetch类型的请求,寻找包含图片或列表数据的请求。
      4. 分析该请求的URL、请求方法(GET/POST)、请求头(特别是可能需要的Referer,X-Requested-With等)以及请求参数。
      5. 在我们的爬虫中,改为直接向这个API接口发送请求。通常这种接口返回的是JSON格式数据,解析起来比HTML更简单,直接用response.json()即可。
      # 示例:假设发现加载更多数据的API api_url = "http://www.jj20.com/api/pic/list" params = { 'category': 'ktmh', 'page': 2, 'size': 20 } headers = { 'User-Agent': '...', 'X-Requested-With': 'XMLHttpRequest' # 有时需要这个头来标识Ajax请求 } response = requests.get(api_url, params=params, headers=headers) data = response.json() # 从data中提取图片信息

      6.2 实现增量抓取与断点续传

      如果计划定期抓取新壁纸,每次都从头开始抓取既浪费流量,也浪费时间。我们可以实现增量抓取。

      • 思路:将已成功下载的图片URL或其唯一标识(如详情页ID)记录到一个文件或数据库中。每次抓取前,先加载这个记录集。当解析出一个新的图片URL时,先检查是否已存在于记录中,如果存在则跳过。
      • 实现:可以使用简单的文本文件、JSON文件,或者轻量级数据库如SQLite。
        import json import os class RecordKeeper: def __init__(self, record_file='downloaded.json'): self.record_file = record_file self.downloaded_set = self._load_record() def _load_record(self): if os.path.exists(self.record_file): with open(self.record_file, 'r', encoding='utf-8') as f: return set(json.load(f)) return set() def is_downloaded(self, identifier): return identifier in self.downloaded_set def mark_downloaded(self, identifier): self.downloaded_set.add(identifier) def save_record(self): with open(self.record_file, 'w', encoding='utf-8') as f: json.dump(list(self.downloaded_set), f, ensure_ascii=False, indent=2) # 在主流程中使用 keeper = RecordKeeper() # 假设用详情页URL作为唯一标识 if not keeper.is_downloaded(detail_url): # ... 执行下载流程 ... keeper.mark_downloaded(detail_url) # 程序退出前保存记录 keeper.save_record()

      6.3 提升下载效率与稳定性

      • 并发下载:使用concurrent.futures模块的ThreadPoolExecutor可以轻松实现多线程下载,大幅提升下载多张图片时的效率。但要注意线程数不宜过高,避免对目标服务器造成过大压力,通常5-10个线程为宜。

        from concurrent.futures import ThreadPoolExecutor, as_completed def batch_download(img_info_list, save_dir, max_workers=5): with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_url = {executor.submit(download_image, info['url'], save_dir, info['name']): info for info in img_info_list} for future in as_completed(future_to_url): info = future_to_url[future] try: success = future.result() except Exception as exc: print(f'{info["name"]} 下载时产生异常: {exc}')
      • 超时与重试:我们在get_page函数中已经实现了基本的重试机制。对于下载图片,同样可以封装一个带重试的下载函数,并合理设置超时时间(如30秒),避免因单张图片卡住整个流程。

      • 日志记录:使用Python的logging模块替代print,可以将运行信息、错误信息分级记录到文件,方便后期排查问题。

        import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('crawler.log'), logging.StreamHandler() # 同时输出到控制台 ] ) logger = logging.getLogger(__name__) logger.info(f"开始处理第 {page} 页") logger.error(f"下载失败 {img_url}", exc_info=True)

      7. 常见问题排查与实战心得

      在实际运行中,你几乎一定会遇到各种问题。下面是我踩过的一些坑和对应的解决方案。

      7.1 问题速查表

      问题现象可能原因排查步骤与解决方案
      请求返回403 Forbidden1. User-Agent被识别为爬虫。
      2. 缺少必要的请求头(如Referer)。
      3. IP被暂时封禁。
      1. 检查并更换更真实的User-Agent字符串。
      2. 在请求头中添加Referer(来源页URL)。
      3. 大幅降低请求频率,增加随机延时。考虑使用代理IP。
      解析不到图片链接或链接为空1. 网页结构已更新,CSS选择器失效。
      2. 图片由JavaScript动态加载,初始HTML中没有。
      3. 图片链接藏在非src的属性中。
      1. 重新使用浏览器开发者工具检查元素,更新选择器。
      2. 在“网络”面板中查找获取图片数据的XHR请求。
      3. 打印soup.prettify()的一部分,或检查img标签的所有属性(img.attrs)。
      下载的图片文件损坏或很小1. 下载到的是缩略图或预览图,不是原图。
      2. 请求被重定向到了错误页面(如验证页)。
      1. 确认解析出的img_url是高清图地址。对比浏览器中“查看图片地址”得到的链接。
      2. 检查下载请求的响应状态码和内容类型(Content-Type)。确保是image/jpegimage/png
      程序运行一段时间后突然停止或报错1. 网络连接不稳定。
      2. 触发了网站的反爬机制,后续请求被拦截。
      3. 本地文件系统错误(如磁盘满)。
      1. 增强异常捕获和重试机制。
      2. 加入更长的随机休眠,模拟人类行为。考虑使用代理池轮换IP。
      3. 检查保存目录的权限和磁盘空间。在文件操作处添加更细致的异常处理。
      文件名乱码或包含非法字符1. 网页标题包含非ASCII字符(如中文)。
      2. 标题包含Windows文件名禁用的字符(如`/:*?"<>
      `)。

      7.2 核心实战心得

      1. 尊重robots.txt:在爬取任何网站前,先访问http://www.jj20.com/robots.txt,查看网站是否允许爬虫抓取目标路径。虽然这不是法律强制,但这是良好的网络公民礼仪。
      2. 控制爬取速度:这是最重要的道德和技术准则。在请求间设置足够的、随机的延时(例如2-5秒),绝对不要用无限循环疯狂请求。你的目标是获取数据,而不是拖垮别人的服务器。
      3. 及时处理异常:网络世界充满不确定性。你的代码必须能妥善处理连接超时、HTTP错误、解析失败等各种异常,并记录日志,让程序在遇到问题时能优雅地跳过或重试,而不是直接崩溃。
      4. 定期维护与更新:网站前端是经常变化的。今天能用的爬虫,几个月后可能就因为网站改版而完全失效。将解析规则(特别是CSS选择器)集中管理,并定期测试你的爬虫是否还能正常工作。
      5. 数据去重与清洗:下载的图片可能会有重复(不同页面链接到同一张图)。可以在保存前计算图片的MD5值,或者根据解析出的唯一ID进行去重。保存的文件名也最好做清洗,避免特殊字符导致的问题。

      这个项目从构思到实现,再到优化,是一个典型的“发现问题 -> 分析问题 -> 解决问题”的工程过程。它不仅仅是一个下载壁纸的工具,更是一个学习HTTP协议、前端基础、数据解析和Python编程的绝佳练手项目。当你亲手构建的爬虫稳定地跑起来,看着壁纸一张张自动存入文件夹时,那种成就感是无可替代的。希望这份详细的指南能帮你少走弯路,顺利构建出你自己的资源收集利器。如果在实践中遇到新的问题,不妨再回头仔细分析网络请求,那里面藏着所有答案。