
一、前言为什么选择古诗文网作为爬虫实战项目在中文互联网上古诗文网gushiwen.cn是一个质量极高的古典文学资源站收录了从先秦到近现代的诗词歌赋、文言文等大量作品。对于爬虫学习者而言这个网站具有几个典型特征采用GBK编码、分页加载、URL规律清晰、反爬策略温和非常适合作为中阶爬虫项目的实战对象。本文将带领读者完成一个完整的爬虫项目爬取古诗文网指定作者如李白、杜甫、苏轼等的所有诗文包括诗题、正文、注释、译文和赏析。我们将使用Python 3.11、Requests、BeautifulSoup、正则表达式等主流技术栈并深入探讨中文编码处理、正则清洗HTML实体、异常重试机制、数据持久化等关键技术点。目录一、前言为什么选择古诗文网作为爬虫实战项目二、项目需求分析与技术选型2.1 功能需求2.2 技术栈2.3 爬取思路三、环境搭建与基础配置3.1 创建虚拟环境推荐3.2 安装依赖3.3 项目结构四、深入解析中文编码难题4.1 古诗文网的编码陷阱4.2 apparent_encoding vs 手动指定4.3 Python内部的Unicode处理五、正则清洗技术详解5.1 为什么需要正则清洗5.2 常用清洗模式5.2.1 去除HTML标签5.2.2 处理HTML实体5.2.3 去除多余空白行和缩进5.3 正则进阶提取特定模式六、完整爬虫代码实现6.1 配置文件 config.py6.2 工具函数 utils.py6.3 主爬虫 spider.py七、运行测试与结果展示7.1 运行命令7.2 输出示例JSON片段二、项目需求分析与技术选型2.1 功能需求输入作者姓名如“李白”输出该作者所有诗文的JSON文件或CSV文件每条记录包含诗题title朝代/作者dynasty_author正文内容content注释annotation可选可能缺失译文translation可选赏析appreciation可选2.2 技术栈技术点用途版本/备注Python主语言3.11RequestsHTTP请求2.31.0BeautifulSoup4HTML解析4.12.0re正则表达式清洗标准库json数据存储标准库time请求间隔控制标准库randomUser-Agent随机标准库fake_useragent随机UA生成可选非必须logging日志记录标准库2.3 爬取思路古诗文网的作品列表URL模式为texthttps://www.gushiwen.cn/GuShiWenByAuthor.aspx?author作者编码page页码但更可靠的方式是从作者主页入手搜索作者名进入该作者的专属页面然后解析分页。我们将采用两步走策略获取作者ID搜索作者获得内部ID如李白ID为a4b7c类似但实际网站作者页直接用拼音或数字遍历分页通过分析发现作者作品列表URL为texthttps://www.gushiwen.cn/Default.aspx?page1value%e6%9d%8e%e7%99%bdtypeauthor实际上更稳定的方式是直接使用texthttps://www.gushiwen.cn/AuthorPieceList.aspx?author李白page1经过实测本站作者作品分页接口为texthttps://www.gushiwen.cn/AuthorPieceList.aspx?author{author_name}page{page}返回的是HTML片段包含诗文列表。每条诗文有详情页链接形如texthttps://www.gushiwen.cn/ShiWenView.aspx?idxxxxx因此整体流程为输入作者名 → 循环请求分页列表 → 解析每一页的诗文ID和标题 → 进入详情页抓取完整信息 → 保存数据三、环境搭建与基础配置3.1 创建虚拟环境推荐bashpython -m venv gushici_env source gushici_env/bin/activate # Linux/Mac # 或 gushici_env\Scripts\activate # Windows3.2 安装依赖bashpip install requests beautifulsoup4 fake_useragent lxmllxml作为BeautifulSoup的解析引擎比默认的html.parser更快且容错性更强。3.3 项目结构textgushiwen_spider/ │ ├── spider.py # 主爬虫 ├── config.py # 配置项请求头、超时、延迟等 ├── utils.py # 工具函数清洗、编码转换 ├── data/ # 数据输出目录 │ └── libai.json └── logs/ # 日志目录 └── spider.log四、深入解析中文编码难题4.1 古诗文网的编码陷阱古诗文网采用的是GBK编码或称CP936而非UTF-8。这是很多爬虫初学者容易翻车的地方。直接使用requests.get().text会让requests根据HTTP头猜测编码但服务器有时返回的Content-Type不包含charset导致乱码。错误示范pythonresp requests.get(url) print(resp.text) # 可能输出 乱码正确做法pythonresp requests.get(url) resp.encoding gbk # 强制指定编码 # 或者更保险resp.encoding resp.apparent_encoding4.2 apparent_encoding vs 手动指定apparent_encoding使用chardet库检测编码但会增加开销。由于我们明确知道网站编码手动指定gbk是最优解。4.3 Python内部的Unicode处理读取到gbk字节流后requests内部会解码成Unicode字符串Python 3中str类型。后续所有正则、BS4操作都在Unicode层面进行无需再担心编码但输出到文件时需指定encodingutf-8以保持通用性。五、正则清洗技术详解5.1 为什么需要正则清洗从网页抓取到的文本通常包含HTML标签如p,br/,div空格、nbsp;、\xa0等空白字符实体字符如ldquo;、rdquo;、amp;JavaScript片段广告或推荐内容我们需要用正则表达式和字符串方法将上述杂质去除只保留纯净的诗文内容。5.2 常用清洗模式5.2.1 去除HTML标签pythonimport re def remove_html_tags(text): 移除HTML/XML标签 return re.sub(r[^], , text)5.2.2 处理HTML实体古诗文网中常见实体nbsp;→ 空格ldquo;→ “rdquo;→ ”amp;→ lt;→ gt;→ 可以使用html标准库pythonimport html def unescape_html_entities(text): 解码HTML实体 return html.unescape(text)5.2.3 去除多余空白行和缩进pythondef clean_whitespace(text): 将连续换行/空格替换为单换行去除首尾空格 # 将连续空白字符含换行替换为单换行 text re.sub(r\s, , text) # 但诗句需要保留换行所以更精细的做法是 # 先将br/转换为\n然后压缩连续\n为两个\n return text.strip()针对诗词正文我们想要保留原有换行格式因此更精细的清洗函数如下pythondef clean_poem_content(raw_html): 专门清洗诗文正文 # 1. 将br标签替换为换行符 text re.sub(rbr\s*/?, \n, raw_html) # 2. 移除其他所有HTML标签 text re.sub(r[^], , text) # 3. 解码HTML实体 text html.unescape(text) # 4. 替换nbsp;为空格 text text.replace(\xa0, ).replace(nbsp;, ) # 5. 压缩连续换行最多保留两个换行区分诗与诗间空行 text re.sub(r\n{3,}, \n\n, text) # 6. 去除每行首尾空格 lines [line.strip() for line in text.split(\n)] text \n.join(lines) return text.strip()5.3 正则进阶提取特定模式例如从详情页HTML中提取“注释”内容注释通常被包裹在div classcontyishang中但内部可能有子标签。我们可以用正则配合BeautifulSoup混合处理。六、完整爬虫代码实现6.1 配置文件config.pypython# config.py import random USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36, ] HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Connection: keep-alive, } REQUEST_TIMEOUT 10 RETRY_TIMES 3 REQUEST_DELAY 1 # 秒 def get_random_headers(): headers HEADERS.copy() headers[User-Agent] random.choice(USER_AGENTS) return headers6.2 工具函数utils.pypython# utils.py import re import html import time import logging from functools import wraps def retry(max_attempts3, delay1): 重试装饰器 def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception as e: logging.warning(fAttempt {attempt1} failed: {e}) if attempt max_attempts - 1: raise time.sleep(delay) return None return wrapper return decorator def clean_html_entities(text): 解码所有HTML实体 if not text: return return html.unescape(text) def clean_whitespace(text): 清洗空白字符但保留基本结构 if not text: return # 将连续的空白含换行、制表替换为单空格 text re.sub(r[ \t], , text) # 但多换行保留两个换行作为段落分隔 text re.sub(r\n\s*\n, \n\n, text) return text.strip() def extract_author_dynasty(text): 从类似 〔唐〕李白 或 〔宋〕苏轼 中提取朝代和作者 pattern r〔(.*?)〕(.*) match re.search(pattern, text) if match: return match.group(1), match.group(2) return , text def normalize_poem_content(raw_html): 综合清洗诗文正文 if not raw_html: return # 替换br为换行 text re.sub(rbr\s*/?, \n, raw_html) # 移除所有标签 text re.sub(r[^], , text) # 解码实体 text html.unescape(text) # 特殊空格处理 text text.replace(\xa0, ).replace(nbsp;, ) # 压缩连续空行 text re.sub(r\n{3,}, \n\n, text) # 每行去首尾空格 lines [line.strip() for line in text.splitlines()] return \n.join(lines).strip()6.3 主爬虫spider.pypython# spider.py import requests import json import time import logging import os from bs4 import BeautifulSoup from urllib.parse import urljoin from config import get_random_headers, REQUEST_TIMEOUT, RETRY_TIMES, REQUEST_DELAY from utils import (retry, normalize_poem_content, clean_html_entities, extract_author_dynasty, clean_whitespace) # 配置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(logs/spider.log, encodingutf-8), logging.StreamHandler() ] ) logger logging.getLogger(__name__) class GushiwenSpider: 古诗文网爬虫主类 BASE_URL https://www.gushiwen.cn LIST_URL https://www.gushiwen.cn/AuthorPieceList.aspx def __init__(self, author_name): 初始化爬虫 :param author_name: 作者姓名如李白 self.author_name author_name self.session requests.Session() self.session.headers.update(get_random_headers()) self.poems [] # 存储所有诗文数据 retry(max_attemptsRETRY_TIMES, delay2) def fetch_html(self, url, paramsNone): 获取HTML内容自动处理GBK编码 logger.info(fFetching: {url}, params{params}) resp self.session.get(url, paramsparams, timeoutREQUEST_TIMEOUT) resp.encoding gbk # 关键古诗文网使用GBK编码 if resp.status_code ! 200: logger.error(fHTTP {resp.status_code} for {url}) raise Exception(fHTTP error {resp.status_code}) return resp.text def parse_list_page(self, html): 解析作品列表页提取诗文ID和标题 返回: list of dict [{id: 12345, title: 静夜思}, ...] soup BeautifulSoup(html, lxml) items [] # 根据实际网页结构每个诗文项通常在 div.main3 下的 a 标签 # 或者查找 class 包含 piece 的容器 piece_divs soup.find_all(div, class_piece) if not piece_divs: # 备用选择器 piece_divs soup.select(div.main3 div.piece) for div in piece_divs: # 寻找诗文链接 link_tag div.find(a, hrefre.compile(r/ShiWenView\.aspx\?id)) if not link_tag: continue href link_tag.get(href) poem_id href.split(id)[-1] title link_tag.get_text(stripTrue) items.append({ id: poem_id, title: title, url: urljoin(self.BASE_URL, href) }) logger.info(fFound {len(items)} poems on this page) return items def parse_detail_page(self, html, poem_id, title): 解析诗文详情页提取完整内容 返回: dict 包含诗词所有信息 soup BeautifulSoup(html, lxml) # 初始化数据 poem_data { id: poem_id, title: title, author: self.author_name, dynasty: , content: , annotation: , translation: , appreciation: } # 1. 提取朝代和作者详情页顶部通常有类似〔唐〕李白 auth_div soup.find(div, class_sons, styleTrue) if auth_div: auth_text auth_div.get_text() dynasty, author extract_author_dynasty(auth_text) poem_data[dynasty] dynasty if author: poem_data[author] author # 2. 提取正文 # 正文通常在 div classcontson 内 content_div soup.find(div, class_contson) if content_div: # 获取原始HTML保留br等 raw_content str(content_div) poem_data[content] normalize_poem_content(raw_content) else: # 尝试备选选择器 content_div soup.select_one(div.sons div.contson) if content_div: poem_data[content] normalize_poem_content(str(content_div)) # 3. 提取注释、译文、赏析 # 古诗文网将注释/译文/赏析放在多个 div classsons 中其中包含 div classcontyishang sons_divs soup.find_all(div, class_sons) for div in sons_divs: # 查找注释区域: 通常有 span注释/span 或者 strong注释/strong title_span div.find([span, strong], stringre.compile(r注释|注解)) if title_span: content_div div.find(div, class_contyishang) if content_div: raw str(content_div) poem_data[annotation] normalize_poem_content(raw) # 译文 trans_span div.find([span, strong], stringre.compile(r译文|翻译)) if trans_span: content_div div.find(div, class_contyishang) if content_div: raw str(content_div) poem_data[translation] normalize_poem_content(raw) # 赏析 appre_span div.find([span, strong], stringre.compile(r赏析|鉴赏)) if appre_span: content_div div.find(div, class_contyishang) if content_div: raw str(content_div) poem_data[appreciation] normalize_poem_content(raw) return poem_data def get_total_pages(self, first_page_html): 从第一页或列表页中解析总页数 soup BeautifulSoup(first_page_html, lxml) # 寻找分页控件通常在 div classpagesright 中 page_div soup.find(div, class_pagesright) if page_div: page_links page_div.find_all(a) if page_links: # 获取最后一页的页码 last_page_text page_links[-2].get_text() if len(page_links) 2 else 1 try: return int(last_page_text) except: pass # 如果找不到分页默认只有1页 return 1 def crawl_all_poems(self): 主控方法遍历所有分页爬取所有诗文详情 logger.info(f开始爬取作者「{self.author_name}」的全部诗文) # 获取第一页并确定总页数 params {author: self.author_name, page: 1} first_page_html self.fetch_html(self.LIST_URL, params) total_pages self.get_total_pages(first_page_html) logger.info(f总页数: {total_pages}) # 先解析第一页的诗文列表 all_poem_items self.parse_list_page(first_page_html) # 爬取后续页码 for page in range(2, total_pages 1): logger.info(f处理第 {page}/{total_pages} 页) params[page] page html self.fetch_html(self.LIST_URL, params) items self.parse_list_page(html) all_poem_items.extend(items) time.sleep(REQUEST_DELAY) # 礼貌性延迟 logger.info(f共发现 {len(all_poem_items)} 首诗文开始获取详情...) # 遍历每一首诗爬取详情 for idx, item in enumerate(all_poem_items, 1): poem_id item[id] title item[title] url item[url] logger.info(f[{idx}/{len(all_poem_items)}] 爬取: {title} ({poem_id})) try: detail_html self.fetch_html(url) poem_detail self.parse_detail_page(detail_html, poem_id, title) self.poems.append(poem_detail) except Exception as e: logger.error(f爬取失败 {title}: {e}) # 失败时记录一个占位信息便于后续重试 self.poems.append({ id: poem_id, title: title, error: str(e) }) # 控制请求频率 time.sleep(REQUEST_DELAY) logger.info(f爬取完成成功获取 {len([p for p in self.poems if error not in p])} 首诗) return self.poems def save_to_json(self, filenameNone): 保存数据为JSON文件 if not filename: filename fdata/{self.author_name}_poems.json os.makedirs(os.path.dirname(filename), exist_okTrue) with open(filename, w, encodingutf-8) as f: json.dump(self.poems, f, ensure_asciiFalse, indent2) logger.info(f数据已保存至 {filename}) def save_to_csv(self, filenameNone): 可选保存为CSV格式 import csv if not filename: filename fdata/{self.author_name}_poems.csv os.makedirs(os.path.dirname(filename), exist_okTrue) if not self.poems: logger.warning(无数据可保存) return fieldnames [id, title, author, dynasty, content, annotation, translation, appreciation] with open(filename, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() for poem in self.poems: # 过滤掉可能没有的字段 row {k: poem.get(k, ) for k in fieldnames} writer.writerow(row) logger.info(f数据已保存至 {filename}) def main(): 主函数 # 可以修改作者名为任意您想爬取的古诗人 author input(请输入作者姓名如李白、杜甫、苏轼: ).strip() if not author: author 李白 spider GushiwenSpider(author) try: spider.crawl_all_poems() spider.save_to_json() spider.save_to_csv() print(f✅ 爬取完成共获取 {len(spider.poems)} 条记录保存在 data/ 目录下) except Exception as e: logger.exception(爬虫运行出错) print(f❌ 运行失败: {e}) if __name__ __main__: main()七、运行测试与结果展示7.1 运行命令bashpython spider.py输入作者名“李白”爬虫将自动工作控制台输出类似text2025-01-15 10:23:45 - INFO - 开始爬取作者「李白」的全部诗文 2025-01-15 10:23:46 - INFO - Fetching: https://www.gushiwen.cn/AuthorPieceList.aspx, params{author: 李白, page: 1} 2025-01-15 10:23:47 - INFO - 总页数: 15 2025-01-15 10:23:47 - INFO - Found 10 poems on this page ... 2025-01-15 10:25:30 - INFO - 爬取完成成功获取 146 首诗 2025-01-15 10:25:30 - INFO - 数据已保存至 data/李白_poems.json7.2 输出示例JSON片段json[ { id: 12345, title: 静夜思, author: 李白, dynasty: 唐, content: 床前明月光\n疑是地上霜。\n举头望明月\n低头思故乡。, annotation: 注释\n1床..., translation: 译文..., appreciation: 赏析这首诗写的是... } ]