Python 3.12 + Requests 2.31 爬取笔趣阁:10章小说防封策略与编码处理实战
在Python爬虫的世界里,笔趣阁这类小说网站一直是新手练习的热门目标。但很多初学者在兴奋地写出第一个爬虫后,很快就会发现两个令人头疼的问题:要么爬着爬着就被封了IP,要么好不容易爬下来的内容全是乱码。今天我们就来彻底解决这两个痛点,用Python 3.12和Requests 2.31打造一个既防封又不会乱码的笔趣阁小说爬虫。
1. 环境准备与基础爬虫搭建
首先确认你的Python环境是3.12版本,这能确保我们使用最新的语言特性。安装必要的库:
pip install requests==2.31.0 beautifulsoup4==4.12.2 fake-useragent==1.3.0基础爬虫的核心代码其实很简单:
import requests from bs4 import BeautifulSoup def basic_crawler(url): 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' } response = requests.get(url, headers=headers) response.encoding = 'utf-8' # 这里埋了个坑,后面会讲 soup = BeautifulSoup(response.text, 'html.parser') content = soup.find('div', id='chaptercontent').text return content这个基础版本能跑,但很快就会遇到问题。让我们先来看看如何应对反爬机制。
2. 防封策略全解析
笔趣阁这类网站通常会有以下几种反爬手段:
- User-Agent检测- 识别非浏览器请求
- 请求频率限制- 短时间内过多请求会被封
- IP封禁- 同一IP频繁访问会被暂时封禁
- 行为分析- 检测非人类浏览模式
2.1 User-Agent轮换策略
单一User-Agent很容易被识别。我们可以使用fake-useragent库动态生成:
from fake_useragent import UserAgent def get_random_ua(): ua = UserAgent() return ua.random headers = { 'User-Agent': get_random_ua() }提示:fake-useragent会从在线数据库获取最新UA列表,首次使用需要联网下载数据
2.2 智能请求间隔控制
固定sleep时间太容易被识别。更好的做法是:
import random import time def smart_delay(): base = 2 # 基础间隔秒数 variation = random.uniform(0.5, 1.5) # 随机波动 time.sleep(base + variation)还可以根据响应状态动态调整:
if response.status_code == 429: # 被限速 time.sleep(10) # 延长等待时间2.3 状态码检查与重试机制
完善的请求应该包含错误处理:
max_retries = 3 for attempt in range(max_retries): try: response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: break elif response.status_code == 403: raise Exception('IP可能被封禁') except Exception as e: if attempt == max_retries - 1: raise time.sleep(5 * (attempt + 1))3. 编码处理深度剖析
乱码问题往往比封禁更让人抓狂。笔趣阁网站常见的编码有:
| 网站版本 | 常见编码 | 特征 |
|---|---|---|
| 老版笔趣阁 | GBK/GB2312 | 中文Windows服务器 |
| 新版笔趣阁 | UTF-8 | 现代Linux服务器 |
| 移动版 | UTF-8 | 响应头可能不准确 |
3.1 智能编码检测方法
不要盲目相信响应头或meta标签。可靠的检测顺序:
- 检查HTTP响应头中的charset
- 检查HTML meta标签中的charset
- 使用chardet库自动检测
- 常见编码逐一尝试
实现代码:
import chardet def detect_encoding(response): # 优先级1:HTTP头 encoding = response.encoding # 优先级2:HTML meta标签 if not encoding: soup = BeautifulSoup(response.content, 'html.parser') meta = soup.find('meta', attrs={'charset': True}) if meta: encoding = meta['charset'] # 优先级3:内容自动检测 if not encoding: detected = chardet.detect(response.content) encoding = detected['encoding'] # 常见中文编码回退 common_encodings = ['utf-8', 'gbk', 'gb2312', 'big5'] if encoding.lower() not in common_encodings: for enc in common_encodings: try: response.content.decode(enc) encoding = enc break except: continue return encoding3.2 内容清洗与规范化
即使编码正确,爬下来的内容也常有杂质:
def clean_content(text): # 替换各种空白字符 text = text.replace('\xa0', ' ') text = text.replace('\u3000', ' ') # 移除广告文本 ad_patterns = [ r'亲.*?老婆哦!', r'请记住本书首发域名.*?', r'手机站.*?同步' ] for pattern in ad_patterns: text = re.sub(pattern, '', text) # 规范化段落 text = re.sub(r'\n{3,}', '\n\n', text) return text.strip()4. 完整实战代码
结合所有优化点,这是我们的终极爬虫:
import requests import random import time import chardet import re from bs4 import BeautifulSoup from fake_useragent import UserAgent from functools import wraps def retry(max_attempts=3, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception as e: if attempt == max_attempts - 1: raise sleep_time = delay * (attempt + 1) + random.random() time.sleep(sleep_time) return wrapper return decorator class BiqugeCrawler: def __init__(self): self.ua = UserAgent() self.session = requests.Session() self.session.headers.update({ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', }) def get_random_ua(self): return self.ua.random @retry(max_attempts=3, delay=2) def fetch_page(self, url): headers = {'User-Agent': self.get_random_ua()} response = self.session.get(url, headers=headers, timeout=10) if response.status_code != 200: raise Exception(f'请求失败,状态码:{response.status_code}') encoding = self.detect_encoding(response) response.encoding = encoding return response def detect_encoding(self, response): # 实现同前文的detect_encoding方法 pass def clean_content(self, text): # 实现同前文的clean_content方法 pass def crawl_chapter(self, url): response = self.fetch_page(url) soup = BeautifulSoup(response.text, 'html.parser') title = soup.find('h1').text if soup.find('h1') else '未知章节' content = soup.find('div', id='chaptercontent').text content = self.clean_content(content) return title, content def crawl_novel(self, start_url, chapters=10): results = [] current_url = start_url for i in range(chapters): try: title, content = self.crawl_chapter(current_url) results.append((title, content)) print(f'成功爬取: {title}') # 寻找下一章URL(根据实际网站结构调整) next_link = soup.find('a', text=re.compile(r'下一[页章]|下章')) if next_link and 'href' in next_link.attrs: current_url = next_link['href'] else: break # 智能延迟 time.sleep(random.uniform(1.5, 3.5)) except Exception as e: print(f'爬取失败: {str(e)}') break return results5. 高级技巧与注意事项
5.1 分布式爬虫设计
如果需要大规模爬取,可以考虑:
- 使用Redis作为任务队列
- 多进程/协程并发(但要注意控制频率)
- 代理IP池轮换
import redis from multiprocessing import Pool r = redis.Redis(host='localhost', port=6379) def worker(url): crawler = BiqugeCrawler() result = crawler.crawl_chapter(url) return result # 从Redis获取任务 def start_workers(num_workers=4): with Pool(num_workers) as p: while True: url = r.lpop('biquge:task_queue') if not url: break p.apply_async(worker, (url.decode(),))5.2 法律与道德考量
在开发爬虫时,请务必注意:
- 遵守网站的robots.txt协议
- 控制爬取频率,不影响网站正常运营
- 不爬取敏感或隐私内容
- 爬取的数据仅用于个人学习
5.3 常见问题排查
遇到问题时,可以检查以下几点:
突然无法爬取
- 检查IP是否被封
- 验证User-Agent是否有效
- 查看网站是否改版
部分内容缺失
- 检查JavaScript渲染的内容
- 验证CSS选择器是否正确
- 查看是否有反爬干扰代码
编码仍然混乱
- 尝试强制不同编码
- 检查是否有混合编码内容
- 验证内容是否被加密或混淆
在实际项目中,我遇到过最棘手的情况是一个笔趣阁分站使用了动态编码——不同章节竟然使用不同编码。解决方案是实现了章节级的编码检测和自动回退机制。