引言
XKCD是一个广受欢迎的网络漫画网站,以其幽默、科学和技术主题的漫画而闻名。本文将详细介绍如何使用Python构建一个爬虫程序来自动下载XKCD网站上的所有漫画。这个实践项目不仅适合Python初学者学习基本的爬虫技术,也包含了一些高级技巧,如进度显示、异常处理和持久化存储。
一、项目概述
我们的目标是创建一个能够自动下载XKCD网站上所有漫画的Python脚本。该脚本将:
- 从XKCD主页开始抓取
- 识别并下载当前页面的漫画图片
- 找到"上一页"链接,继续抓取过程
- 保存下载进度,支持断点续传
- 显示下载进度条
二、核心代码解析
2.1 基础设置与导入
python
import requests # 用于发送网络请求
import os # 用于判断文件是否存在、创建文件夹
import bs4 # 用于解析网页
import time # 用于暂停程序
import json # 用于将图片链接保存到json文件
这些导入语句包含了我们需要的所有核心库。requests
用于HTTP请求,bs4
(BeautifulSoup)用于HTML解析,os
用于文件操作,time
用于控制请求频率,json
用于保存进度。
2.2 初始化设置
python
url = ''
# 从JSON文件加载上次的进度
with open('/Users/yc-space/PYTHON/NEWpy 3.9/XKCD/xkcd/index.json', 'r') as f:
url = json.load(f)
这段代码实现了断点续传功能。程序会从一个JSON文件中读取上次下载的进度,如果文件不存在或为空,则会从最新的漫画开始下载。
2.3 进度条显示函数
python
def loading(times: int) -> None:
'''打印下载进度条'''
for i in range(0, 101):
print(f'{i}% <-下载进度', end='\r')
time.sleep(times/100)
这个简单的进度条函数通过循环打印0-100%的进度,times
参数控制整个进度条的持续时间。end='\r'
使得每次打印都会回到行首,实现进度条效果。
三、主下载逻辑
3.1 主函数框架
python
def main():
global url
while not url.endswith('#'): # 如果url不是#,则继续循环
# 下载逻辑...
主函数使用一个while循环,直到遇到URL以"#"结尾(XKCD的第一页链接格式),表示已经下载完所有漫画。
3.2 页面下载与解析
python
print('准备下载:' + url)
res = requests.get(url)
res.raise_for_status()
soup = bs4.BeautifulSoup(res.text, 'lxml')
这部分代码下载当前URL的页面内容,并使用BeautifulSoup+lxml解析HTML。raise_for_status()
会在请求失败时抛出异常。
3.3 漫画图片识别
python
comicElem = soup.select('#comic img')
if comicElem == []:
print('ERROR: 无法找到图片')
else:
comicUrl = 'https:' + comicElem[0].get('src')
XKCD的漫画图片位于id="comic"
的div中的img
标签。我们使用CSS选择器#comic img
来定位它。有些页面可能没有漫画(如特殊页面),所以需要检查结果是否为空。
3.4 图片下载与保存
python
print('下载图片:' + comicUrl + ' '*3)
loading(4)
res = requests.get(comicUrl)
res.raise_for_status()
x_path = '/Users/yc-space/PYTHON/NEWpy 3.9/XKCD/xkcd'
with open(os.path.join(x_path, os.path.basename(comicUrl)), 'wb') as f:
f.write(res.content)
下载实际的漫画图片并保存到本地。os.path.basename()
从URL中提取文件名,os.path.join()
构建完整的保存路径。
3.5 导航到上一页
python
prevLink = soup.select('a[rel="prev"]')[0]
url = 'https://xkcd.com' + prevLink.get('href')
找到"上一页"链接并更新URL,以便下次循环下载更早的漫画。
3.6 保存进度
python
with open('/Users/yc-space/PYTHON/NEWpy 3.9/XKCD/xkcd/index.json', 'w') as f:
json.dump(url, f)
将当前进度保存到JSON文件,实现断点续传功能。
四、代码优化建议
4.1 路径处理优化
硬编码路径不利于代码移植。可以改为:
python
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, 'xkcd')
os.makedirs(DATA_DIR, exist_ok=True)
JSON_PATH = os.path.join(DATA_DIR, 'index.json')
IMAGE_DIR = os.path.join(DATA_DIR, 'images')
os.makedirs(IMAGE_DIR, exist_ok=True)
4.2 异常处理增强
python
try:
res = requests.get(url, timeout=10)
res.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"请求失败: {str(e)}")
time.sleep(5) # 等待后重试
continue
4.3 用户代理设置
python
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
res = requests.get(url, headers=headers)
4.4 更精确的进度条
python
def download_file(url, save_path):
response = requests.get(url, stream=True)
total_size = int(response.headers.get('content-length', 0))
with open(save_path, 'wb') as f:
downloaded = 0
for data in response.iter_content(chunk_size=4096):
downloaded += len(data)
f.write(data)
progress = (downloaded / total_size) * 100
print(f"{progress:.1f}%", end='\r')
print("\n下载完成!")
五、完整优化版代码
python
import requests
import os
import bs4
import time
import json
from urllib.parse import urljoin
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, 'xkcd_data')
os.makedirs(DATA_DIR, exist_ok=True)
JSON_PATH = os.path.join(DATA_DIR, 'progress.json')
IMAGE_DIR = os.path.join(DATA_DIR, 'images')
os.makedirs(IMAGE_DIR, exist_ok=True)
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
def load_progress():
try:
with open(JSON_PATH, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return 'https://xkcd.com/'
def save_progress(url):
with open(JSON_PATH, 'w') as f:
json.dump(url, f)
def download_file(url, save_path):
try:
response = requests.get(url, headers=HEADERS, stream=True, timeout=30)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
downloaded += len(chunk)
progress = downloaded / total_size * 100
print(f"下载进度: {progress:.1f}%", end='\r')
print("\n下载完成!")
return True
except Exception as e:
print(f"\n下载失败: {str(e)}")
return False
def main():
url = load_progress()
while not url.endswith('#'):
print(f"\n处理页面: {url}")
try:
# 下载页面
response = requests.get(url, headers=HEADERS, timeout=30)
response.raise_for_status()
soup = bs4.BeautifulSoup(response.text, 'lxml')
# 查找漫画图片
comic_img = soup.select_one('#comic img')
if not comic_img:
print("警告: 未找到漫画图片")
else:
img_url = urljoin('https://', comic_img.get('src', ''))
img_name = os.path.basename(img_url)
save_path = os.path.join(IMAGE_DIR, img_name)
if os.path.exists(save_path):
print(f"文件已存在: {img_name}")
else:
print(f"下载图片: {img_name}")
if download_file(img_url, save_path):
print(f"保存到: {save_path}")
# 查找上一页链接
prev_link = soup.select_one('a[rel="prev"]')
if not prev_link:
break
url = urljoin('https://xkcd.com/', prev_link.get('href', ''))
save_progress(url)
# 礼貌延迟
time.sleep(2)
except Exception as e:
print(f"发生错误: {str(e)}")
time.sleep(5)
continue
print("\n所有漫画下载完成!")
if __name__ == '__main__':
main()
六、项目扩展思路
- 多线程下载 :使用
concurrent.futures
实现并行下载,加快速度 - GUI界面:使用Tkinter或PyQt添加图形界面
- 漫画浏览器:将下载的漫画制作成电子书或本地浏览应用
- 自动更新:定期检查新漫画并自动下载
- 云存储集成:支持将漫画备份到Google Drive或Dropbox
七、法律与道德考虑
在开发和使用网络爬虫时,务必注意:
- 尊重网站的
robots.txt
规则 - 设置合理的请求间隔(如代码中的
time.sleep(2)
) - 不要对服务器造成过大负担
- 仅用于个人使用,不要大规模分发下载内容
- 遵守版权法律法规
XKCD网站的robots.txt
允许爬虫访问,但建议控制请求频率。
结语
通过这个项目,我们不仅实现了一个实用的XKCD漫画下载器,还学习了Python爬虫开发的核心技术。包括:
- HTTP请求处理
- HTML解析
- 文件操作
- 进度显示
- 异常处理
- 持久化存储
这些技能可以应用于各种网络数据采集任务,为数据分析、内容聚合等应用打下基础。希望本文对你的Python学习和项目开发有所帮助!