简介:这是一套面向编程零基础学习者的Python网络爬虫实战入门资源,聚焦HTML解析驱动的数据采集与多格式存储全流程,解决初学者从环境搭建、网页请求、结构化提取到持久化落地的核心痛点。资源包共30个文件,含17个可直接运行的Python脚本(覆盖requests抓取、BeautifulSoup/lxml解析、维基百科词条递归采集、链接去重、数据库写入等典型场景)、6份PDF技术文档(含《Web Scraping with Python》《Beautiful Soup官方文档》《Scrapy入门教程》等权威参考资料)、5个XML配置与项目元数据文件,以及README说明和IDE配置文件,整体96.04MB,结构清晰、即学即用。目前已有39人下载学习。学习者可获得完整可复现的爬虫项目链:从静态页面解析、正则提取图片、新闻数据采集,到六度空间链接遍历、维基百科结构化存储,配套代码均经过实践验证,并融入反爬应对思路与合规采集提醒,为后续进阶数据科学与信息检索打下扎实工程基础。
1. 项目概述:从零开始构建你的第一个数据采集器
看到这个标题,很多刚接触编程的朋友可能会觉得“爬虫”是个高深莫测的技术,需要复杂的算法和深厚的计算机功底。其实不然,用Python写一个基础的、基于HTML解析的爬虫,就像学骑自行车一样,一旦掌握了平衡,剩下的就是踩踏板了。这个项目,就是带你从零开始,一步步搭建一个能自动从网页上抓取你需要的数据,并把它规规矩矩存起来的工具。无论你是想收集某个商品的价格变化、追踪新闻热点,还是想为自己的数据分析项目找点“原料”,这套方法都为你提供了一个清晰、可靠的起点。
核心思路很简单:模拟浏览器访问网页 -> 获取网页的HTML源代码 -> 从源代码中“解析”出我们需要的数据 -> 把数据保存到本地文件或数据库。整个过程,我们主要会用到两个Python库:requests负责网络请求,BeautifulSoup负责解析HTML。数据存储方面,我们会从最简单的CSV文件开始,再延伸到轻量级的SQLite数据库。别担心,我会把每一步为什么这么做、可能会遇到什么坑、以及怎么绕过去,都掰开揉碎了讲清楚。即使你昨天才安装好Python,今天也能跟着做出一个能跑起来的爬虫。
2. 环境准备与核心工具解析
2.1 Python环境与必备库安装
工欲善其事,必先利其器。首先确保你的电脑上已经安装了Python。去Python官网下载最新稳定版(比如3.8以上版本)安装即可,记得在安装时勾选“Add Python to PATH”,这样在命令行里就能直接使用python和pip命令了。
安装好后,打开你的命令行(Windows上是CMD或PowerShell,Mac/Linux上是终端),我们来安装这个项目最核心的三个库。
pip install requests beautifulsoup4 lxmlrequests:这是Python里最受欢迎的HTTP库,没有之一。它的口号是“HTTP for Humans”,意思是对人类极其友好。我们用它将一个网址(URL)发送给服务器,然后接收服务器返回的网页内容。相比于Python自带的urllib,requests的API简洁直观得多。BeautifulSoup:我们的“解析神器”。网页的HTML代码就像一棵倒着长的树(DOM树),有根节点、分支和叶子。BeautifulSoup能帮我们在这棵树上轻松地导航、搜索和修改,从而精准地找到藏在标签里的数据。它支持多种解析器,我们安装的lxml就是其中速度最快、容错性较好的一个。lxml:这是一个高性能的HTML/XML解析器。BeautifulSoup本身只是一个“包装器”,它需要依赖像lxml或html.parser这样的解析器来实际处理HTML字符串。选择lxml是因为它在处理复杂或破损的HTML时表现更稳定,速度也更快。
注意:有些教程可能会提到
urllib或urllib3。对于新手,我强烈建议直接从requests开始。它封装了连接池、重试、编码处理等底层细节,让你能更专注于业务逻辑,而不是处理各种网络异常。这是从“能用”到“好用”的关键一步。
2.2 开发工具与调试技巧
写代码需要一个顺手的编辑器。对于新手,我推荐使用Visual Studio Code (VSCode)。它免费、轻量、插件生态丰富。安装后,记得安装Python扩展,它能提供代码高亮、智能提示、调试等功能,极大提升开发效率。
在开始爬虫前,还有一个至关重要的准备工作:使用浏览器开发者工具。以Chrome浏览器为例,在任何网页上右键点击,选择“检查”(Inspect),就能打开开发者工具。
- 元素检查(Elements):这个标签页展示了网页渲染后的完整DOM树。你可以把鼠标移到代码上,页面对应的元素会高亮显示。这是我们寻找数据所在HTML标签结构的主要战场。
- 网络请求(Network):这个标签页记录了浏览器发出的所有请求(HTML、图片、JavaScript、API接口等)。刷新页面后,这里会列出所有请求。找到最主要的文档请求(通常是第一个,类型为
document),点击查看它的Headers和Response。Headers里可以看到我们模拟请求时需要的关键信息,如User-Agent;Response里可以看到服务器返回的原始HTML,有时和“元素”标签里看到的不一样(因为后者是经过JavaScript修改后的),爬虫获取的是Response里的内容。 - 控制台(Console):可以执行JavaScript代码,有时用于测试某些动态加载数据的接口。
一个实用的调试技巧:在写解析代码时,可以先将网页的HTML保存到一个本地文件,然后用BeautifulSoup加载这个文件进行解析测试。这样可以避免频繁网络请求,快速调整你的解析规则,等规则稳定了再接入真实的网络请求。具体操作是:在开发者工具的“元素”标签页,右键点击<html>标签,选择“Copy -> Copy outer HTML”,然后粘贴保存为test.html文件。
3. 核心步骤一:发起请求与获取HTML
3.1 构建一个稳健的HTTP请求
拿到了网址,我们第一步就是让Python程序去“访问”它。直接用requests.get()是最简单的,但一个健壮的爬虫不能这么“裸奔”。
import requests url = 'https://example.com' # 替换成你的目标网址 # 1. 设置请求头,这是模仿浏览器的关键一步 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' } try: # 2. 发送GET请求,并带上请求头 response = requests.get(url, headers=headers, timeout=10) # 3. 检查请求是否成功(状态码为200) response.raise_for_status() # 4. 自动推断编码并获取网页文本内容 response.encoding = response.apparent_encoding html_content = response.text print("成功获取网页内容,长度:", len(html_content)) except requests.exceptions.RequestException as e: print(f"请求出错:{e}") except Exception as e: print(f"发生未知错误:{e}")关键点解析:
User-Agent:这是HTTP请求头中最重要的字段之一,它告诉服务器你是什么类型的客户端(浏览器)。很多网站会屏蔽没有User-Agent或使用默认PythonUser-Agent的请求,认为它是爬虫。所以我们这里伪装成一个常见的Chrome浏览器。你可以在自己浏览器的开发者工具“Network”标签里,找到任意一个请求,复制它的User-Agent值来用。timeout:设置超时时间(单位秒)。网络状况不稳定时,如果一个请求长时间没响应,程序会一直卡住。设置超时后,超过时间会自动抛出异常,避免程序“假死”。raise_for_status():这是一个非常实用的方法。如果响应状态码不是200(成功),比如404(找不到)或503(服务不可用),它会抛出一个HTTPError异常,让我们能及时处理错误,而不是拿着一个错误的页面内容去解析。- 编码处理:网页可能有各种编码(如UTF-8, GBK)。
response.apparent_encoding是requests库根据内容分析出的可能编码,通常比较准确。直接设置response.encoding,后续response.text就会用这个编码来解码字节流,避免中文乱码。
3.2 处理常见的反爬机制与请求异常
直接get可能不会一帆风顺,网站可能会有一些简单的防护。
- 处理简单的重定向:
requests默认会自动处理重定向(状态码301/302)。你不需要额外代码。如果需要观察重定向路径,可以设置allow_redirects=False并手动处理response.headers['Location']。 - 处理连接错误和超时:我们已经用
try...except包裹了请求,并设置了timeout。对于可能出现的ConnectionError(网络连接问题)、Timeout(超时),requests.exceptions.RequestException是它们的基类,可以一并捕获。 - 添加请求间隔:如果你需要连续爬取一个网站的多个页面,千万不要一口气快速请求。这会给对方服务器造成压力,容易被封IP。一个基本的道德和技巧是在请求之间添加随机延时。
import time import random def safe_get(url, headers): try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except requests.exceptions.RequestException as e: print(f"请求 {url} 失败:{e}") return None # 模拟爬取多个页面 url_list = ['https://example.com/page1', 'https://example.com/page2'] for url in url_list: html = safe_get(url, headers) if html: # ... 处理html ... pass # 添加随机延时,模拟人类操作。间隔2到5秒。 time.sleep(random.uniform(2, 5))实操心得:对于初学者,我建议先把一个页面的请求和解析做稳定,再考虑多页面爬取。一开始就处理复杂的翻页和并发,容易让问题纠缠在一起。另外,务必尊重网站的robots.txt协议(通常在网站根目录,如https://example.com/robots.txt),它指明了网站允许和禁止爬虫访问的路径。
4. 核心步骤二:使用BeautifulSoup解析HTML数据
拿到了一长串HTML代码,下一步就是从中“挖”出我们想要的数据。这就是BeautifulSoup的舞台。
4.1 理解HTML结构与选择器
假设我们要爬取一个简单的新闻列表页,其HTML结构可能如下:
<div class="news-list"> <article class="news-item"> <h2><a href="/news/123.html">Python 3.12 正式发布,性能提升显著</a></h2> <p class="summary">Python 3.12版本带来了多项性能优化和新特性...</p> <span class="date">2023-10-02</span> <span class="author">官方团队</span> </article> <article class="news-item"> <h2><a href="/news/124.html">BeautifulSoup 4.12 更新日志</a></h2> <p class="summary">解析库BeautifulSoup发布了新版本...</p> <span class="date">2023-09-28</span> <span class="author">社区贡献者</span> </article> </div>我们的目标是提取每篇文章的标题、链接、摘要、日期和作者。
4.2 多种数据提取方法实战
首先,我们需要创建一个BeautifulSoup对象来装载和解析HTML。
from bs4 import BeautifulSoup # 假设html_content是上一步requests获取的字符串 soup = BeautifulSoup(html_content, 'lxml') # 指定使用lxml解析器方法一:通过标签名和属性查找(find,find_all)
这是最常用、最直观的方法。
# 1. 找到所有 class="news-item" 的 article 标签 news_items = soup.find_all('article', class_='news-item') # 注意:因为`class`是Python关键字,所以BeautifulSoup里用`class_`来匹配CSS类。 news_list = [] for item in news_items: # 2. 在每个item内查找具体元素 # find() 找第一个匹配的,find_all() 找所有匹配的 title_tag = item.find('h2').find('a') # 先找到h2,再找到里面的a标签 title = title_tag.text.strip() # .text获取标签内文本,.strip()去除首尾空白字符 link = title_tag['href'] # 获取标签的属性,如href summary = item.find('p', class_='summary').text.strip() date = item.find('span', class_='date').text.strip() author = item.find('span', class_='author').text.strip() # 3. 将数据组织成字典,方便后续处理 news_data = { 'title': title, 'link': link, 'summary': summary, 'date': date, 'author': author } news_list.append(news_data) # 打印结果 for news in news_list: print(news)方法二:使用CSS选择器(select,select_one)
如果你熟悉CSS,这种方法会更强大和简洁。它使用类似jQuery的选择器语法。
# 选择所有 class="news-item" 的元素 news_items = soup.select('article.news-item') for item in news_items: # select_one 相当于 find,选择第一个匹配项 title_tag = item.select_one('h2 > a') # 选择h2标签下的直接子元素a title = title_tag.text.strip() link = title_tag['href'] summary = item.select_one('p.summary').text.strip() date = item.select_one('span.date').text.strip() author = item.select_one('span.author').text.strip() # ... 后续处理同上方法三:处理缺失数据与复杂结构
现实中的网页不会这么规整,可能某些元素缺失。我们需要更健壮的代码。
for item in news_items: # 使用try-except或条件判断,避免因某个元素找不到而崩溃 title_tag = item.find('h2') if title_tag: a_tag = title_tag.find('a') title = a_tag.text.strip() if a_tag else '无标题' link = a_tag['href'] if a_tag and a_tag.has_attr('href') else '#' else: title = '无标题' link = '#' # 使用get_text()方法,并设置默认值 summary_elem = item.find('p', class_='summary') summary = summary_elem.get_text(strip=True, default='无摘要') # default参数是BeautifulSoup 4.11+特性 # 对于更老的版本,可以这样写 # summary = summary_elem.text.strip() if summary_elem else '无摘要' # 同理处理日期和作者 date_elem = item.find('span', class_='date') date = date_elem.text.strip() if date_elem else '未知日期' author_elem = item.find('span', class_='author') author = author_elem.text.strip() if author_elem else '未知作者' # ... 组装数据注意:
.text和.get_text()的区别:.text返回的是所有子标签文本拼接后的字符串。.get_text()功能更强大,可以指定分隔符(separator)、是否去除空白(strip),并且在新版本中支持默认值(default)。在大多数情况下,使用.get_text(strip=True)是更好的选择。
实操心得:解析HTML时,最耗时间的往往不是写代码,而是分析网页结构。一定要充分利用浏览器的开发者工具。右键点击你想提取的数据,选择“检查”,仔细看它外层包裹的标签和类名。优先使用class和id这类具有唯一性的属性进行定位。如果结构非常复杂或动态生成,CSS选择器(select)的层级写法(如div.content > ul.list > li:first-child)会比一连串的find更清晰。
5. 核心步骤三:数据的清洗与存储
数据抓取和解析出来后,往往是杂乱无章的文本,我们需要进行清洗,然后选择一种合适的方式存储起来,供后续分析使用。
5.1 数据清洗与格式化
清洗通常在解析出文本后立即进行。常见操作包括:
- 去除空白字符:使用
.strip()、.lstrip()、.rstrip(),或get_text(strip=True)。 - 处理特殊字符和编码:有时会遇到HTML实体(如
、&)。BeautifulSoup在解析时通常会自动转换,如果还有残留,可以用html.unescape()处理。 - 字符串替换与分割:例如,日期字符串“发布于:2023-10-02”,我们只需要“2023-10-02”部分。
import html # 示例:清洗一条数据 raw_date = " 发布于:2023-10-02 " cleaned_date = raw_date.strip().replace('发布于:', '') print(cleaned_date) # 输出:2023-10-02 # 处理可能存在的HTML实体 raw_text = "Python & BeautifulSoup" cleaned_text = html.unescape(raw_text) print(cleaned_text) # 输出:Python & BeautifulSoup # 分割字符串获取特定部分 tag_string = "科技, 编程, Python" tag_list = [tag.strip() for tag in tag_string.split(',')] print(tag_list) # 输出:['科技', '编程', 'Python']5.2 存储方案一:CSV文件(简单通用)
CSV(Comma-Separated Values)是一种用逗号分隔的纯文本格式,可以被Excel、Numbers、Python pandas等几乎所有数据处理工具轻松打开,非常适合存储表格数据。
import csv # 假设 news_list 是之前解析得到的字典列表 data_to_save = news_list # 定义CSV文件的列名(表头) fieldnames = ['title', 'link', 'summary', 'date', 'author'] # 使用‘w’模式写入,newline=''防止在Windows下出现空行 # encoding='utf-8-sig' 可以让Excel正确识别UTF-8编码的中文 with open('news_data.csv', 'w', newline='', encoding='utf-8-sig') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) # 写入表头 writer.writeheader() # 写入所有数据行 for news in data_to_save: writer.writerow(news) print("数据已保存到 news_data.csv")关键点解析:
newline='':在Python中打开文本文件写入时,指定这个参数可以避免在Windows系统上产生额外的空行。encoding='utf-8-sig':utf-8-sig会在文件开头写入一个BOM(字节顺序标记),这对于一些老版本的Excel软件识别UTF-8编码的中文至关重要。如果不需要兼容老Excel,使用utf-8即可。csv.DictWriter:因为我们数据是字典格式,用DictWriter非常方便,它会自动根据fieldnames将字典的键值对匹配到对应的列。
5.3 存储方案二:SQLite数据库(结构化、可查询)
当数据量变大,或者你需要进行复杂的查询、去重、更新操作时,CSV就显得力不从心了。SQLite是一个轻量级的、无需单独服务器进程的数据库,整个数据库就是一个文件,完美适配个人爬虫项目。
import sqlite3 # 1. 连接到数据库(如果不存在则会创建) conn = sqlite3.connect('news_data.db') cursor = conn.cursor() # 2. 创建数据表 create_table_sql = ''' CREATE TABLE IF NOT EXISTS news ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, link TEXT UNIQUE, -- 设置链接为唯一,防止重复爬取 summary TEXT, date TEXT, author TEXT, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''' cursor.execute(create_table_sql) # 3. 插入数据 insert_sql = ''' INSERT OR IGNORE INTO news (title, link, summary, date, author) VALUES (?, ?, ?, ?, ?) ''' # 使用 OR IGNORE,当遇到重复的link(违反UNIQUE约束)时,忽略这条插入 for news in news_list: # 将字典中的值按顺序填入 ? 占位符 cursor.execute(insert_sql, ( news['title'], news['link'], news['summary'], news['date'], news['author'] )) # 4. 提交事务并关闭连接 conn.commit() print(f"成功插入 {cursor.rowcount} 条数据到数据库。") cursor.close() conn.close()关键点解析:
IF NOT EXISTS:创建表时使用,避免重复创建报错。UNIQUE约束:我们将link字段设为唯一。这是爬虫数据存储的一个最佳实践。它可以有效避免因重复运行脚本或爬取到相同链接而导致的数据重复。INSERT OR IGNORE语句会在遇到重复唯一键时静默跳过,而不是报错。- 参数化查询 (
?):绝对不要用字符串拼接的方式将变量传入SQL语句(如f"INSERT ... VALUES ('{news['title']}')"),这会导致严重的SQL注入安全风险。使用?作为占位符,然后将值以元组形式传给execute()方法,让数据库驱动库去安全地处理。 AUTOINCREMENT和DEFAULT CURRENT_TIMESTAMP:id字段自动增长,作为主键。created_time字段会自动记录数据插入的时间,对于追踪数据抓取时间很有用。
实操心得:对于初学者,我建议从CSV开始,直观简单。当你需要管理成百上千条数据,或者想练习数据库操作时,再迁移到SQLite。在爬虫项目中,去重是关键。除了利用数据库的UNIQUE约束,也可以在内存中用Python的set()存储已爬取的链接,每次爬取前先判断,这对于防止重复请求、提升效率也很有帮助。
6. 项目整合与优化:构建一个完整的爬虫脚本
现在,我们把前面所有步骤整合起来,形成一个完整的、可复用的爬虫脚本。我们以爬取一个虚构的“书籍信息”网站为例。
6.1 完整脚本示例:爬取书籍信息
import requests from bs4 import BeautifulSoup import sqlite3 import time import random from urllib.parse import urljoin # 用于拼接相对链接为绝对链接 class BookSpider: def __init__(self, base_url, db_path='books.db'): """ 初始化爬虫 :param base_url: 网站基础URL :param db_path: SQLite数据库文件路径 """ self.base_url = base_url self.db_path = db_path self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } self.init_database() def init_database(self): """初始化数据库和表""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, price REAL, author TEXT, rating REAL, link TEXT UNIQUE, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() print("数据库初始化完成。") def fetch_page(self, url): """获取页面HTML内容""" try: resp = requests.get(url, headers=self.headers, timeout=15) resp.raise_for_status() # 有些网站可能不是utf-8,比如gbk,这里可以灵活处理 # 如果知道编码,可以直接指定:resp.encoding = 'gbk' resp.encoding = resp.apparent_encoding or 'utf-8' return resp.text except requests.exceptions.RequestException as e: print(f"请求失败 {url}: {e}") return None def parse_book_list(self, html): """解析书籍列表页,提取每本书的详情页链接""" soup = BeautifulSoup(html, 'lxml') book_links = [] # 假设每本书的链接在一个 class="book-title" 的h3标签下的a标签里 for title_tag in soup.select('h3.book-title'): a_tag = title_tag.find('a') if a_tag and a_tag.has_attr('href'): # 将相对链接转换为绝对链接 full_url = urljoin(self.base_url, a_tag['href']) book_links.append(full_url) return book_links def parse_book_detail(self, html, book_url): """解析书籍详情页,提取具体信息""" soup = BeautifulSoup(html, 'lxml') book_info = {} # 提取书名 title_elem = soup.find('h1', class_='product-title') book_info['title'] = title_elem.get_text(strip=True) if title_elem else '未知书名' # 提取价格,可能包含货币符号 price_elem = soup.find('span', class_='price') if price_elem: price_text = price_elem.get_text(strip=True) # 简单清洗价格,提取数字部分 import re price_num = re.search(r'[\d\.]+', price_text) book_info['price'] = float(price_num.group()) if price_num else 0.0 else: book_info['price'] = 0.0 # 提取作者 author_elem = soup.find('div', class_='author') book_info['author'] = author_elem.get_text(strip=True) if author_elem else '未知作者' # 提取评分 rating_elem = soup.find('div', class_='rating-score') if rating_elem: rating_text = rating_elem.get_text(strip=True) book_info['rating'] = float(rating_text) if rating_text.replace('.', '', 1).isdigit() else 0.0 else: book_info['rating'] = 0.0 book_info['link'] = book_url return book_info def save_to_db(self, book_info): """保存单条书籍信息到数据库""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() try: cursor.execute(''' INSERT OR IGNORE INTO books (title, price, author, rating, link) VALUES (?, ?, ?, ?, ?) ''', (book_info['title'], book_info['price'], book_info['author'], book_info['rating'], book_info['link'])) conn.commit() if cursor.rowcount > 0: print(f"已保存: {book_info['title']}") else: print(f"已跳过(重复): {book_info['title']}") except sqlite3.Error as e: print(f"数据库保存失败 {book_info['title']}: {e}") finally: conn.close() def crawl(self, start_page=1, end_page=3): """主爬取流程:遍历列表页,抓取详情页""" for page in range(start_page, end_page + 1): print(f"\n正在爬取第 {page} 页...") list_url = f"{self.base_url}/list?page={page}" # 假设分页URL格式如此 list_html = self.fetch_page(list_url) if not list_html: print(f"第 {page} 页列表获取失败,跳过。") continue book_urls = self.parse_book_list(list_html) print(f"在第 {page} 页找到 {len(book_urls)} 本书。") for book_url in book_urls: print(f" 处理: {book_url}") detail_html = self.fetch_page(book_url) if not detail_html: print(f" 详情页获取失败,跳过。") continue book_info = self.parse_book_detail(detail_html, book_url) self.save_to_db(book_info) # 礼貌性延时,避免请求过快 time.sleep(random.uniform(1, 3)) # 翻页延时 time.sleep(random.uniform(2, 4)) print("\n爬取任务完成!") # 使用爬虫 if __name__ == '__main__': # 替换成你要爬的网站基础URL spider = BookSpider(base_url='https://example-books.com') spider.crawl(start_page=1, end_page=2) # 爬取前2页6.2 代码结构与设计思路解析
这个脚本采用了简单的面向对象设计,将功能封装在BookSpider类中,好处是结构清晰,状态(如base_url,headers)易于管理,也方便未来扩展。
__init__初始化:设置基础URL、请求头,并初始化数据库。fetch_page请求模块:专门负责发送HTTP请求并处理异常,返回HTML字符串或None。这是为了将网络IO逻辑隔离,让主流程更清晰。parse_book_list与parse_book_detail解析模块:分别处理列表页和详情页。这是爬虫的核心逻辑,高度依赖于目标网站的具体结构。这里也是未来网站改版后最需要修改的地方。save_to_db存储模块:负责将一条条数据持久化到SQLite。使用了INSERT OR IGNORE来去重。crawl调度模块:控制整个爬取流程,串联起“获取列表 -> 提取链接 -> 获取详情 -> 解析详情 -> 保存数据”的循环,并加入了随机延时。if __name__ == '__main__':这是Python脚本的标准写法,意味着当这个文件被直接运行时,下面的代码才会执行。如果它被其他文件作为模块导入,则不会执行。这保证了脚本的复用性。
优化点提示:
- 错误恢复:当前脚本遇到错误(如请求失败)会跳过当前项目。对于更健壮的爬虫,可以考虑加入重试机制(如对失败请求重试3次)。
- 增量爬取:我们的去重依赖于数据库的唯一约束。更复杂的增量爬取可能需要对比内容哈希或更新时间。
- 配置化:可以将
headers、请求延时范围、数据库路径等参数提取到配置文件或类外部,方便调整。
7. 常见问题、反爬策略与进阶方向
7.1 爬虫常见错误与排查清单
即使按照步骤操作,你也可能会遇到一些问题。下面是一个快速排查指南:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 请求返回403/404错误 | 1. 网站屏蔽了Python默认User-Agent。 2. 网址拼写错误或页面不存在。 3. 需要登录或具有其他访问限制。 | 1. 检查并更换为真实的浏览器User-Agent。2. 手动在浏览器访问该URL确认。 3. 检查是否需要Cookie或Session(见下文)。 |
| 获取到的中文是乱码 | 网页编码与requests推断的编码不一致。 | 1. 查看网页源码<meta charset="...">标签确定编码。2. 手动设置 response.encoding = 'gbk'或'utf-8'。 |
BeautifulSoup找不到元素 | 1. 元素是JavaScript动态加载的,初始HTML中没有。 2. 选择器写错了(类名、标签名、结构)。 3. 网站结构已更新。 | 1. 在开发者工具“Network”中查找XHR/Fetch请求,尝试直接请求数据接口(API)。 2. 使用浏览器检查元素,核对选择器路径。 3. 重新分析网页结构。 |
| 数据保存到CSV后Excel打开乱码 | Excel默认可能不是UTF-8编码。 | 保存CSV时使用encoding='utf-8-sig'。 |
| 插入数据库时报错(如重复键) | 1. 违反了UNIQUE约束。2. 数据类型不匹配(如字符串插入了数字字段)。 | 1. 使用INSERT OR IGNORE或先查询是否存在。2. 确保插入的数据类型与表结构定义一致。 |
| 爬取速度很慢 | 1. 网络延迟。 2. 没有使用并发。 3. 延时设置过长。 | 1. 网络问题无法避免。 2. 对于大量页面,可学习 concurrent.futures或aiohttp进行并发请求(新手慎用)。3. 适当调整延时,在礼貌性和效率间平衡。 |
7.2 应对基础反爬策略
当你的爬虫开始规律地访问网站,可能会触发一些基本的反爬机制。
请求头(Headers)校验:这是最低级的防护。除了
User-Agent,有些网站还会检查Referer(来源页)、Accept-Language(接受语言)等。你可以在浏览器开发者工具的“Network”标签里,复制整个请求头信息,直接用作headers字典。headers = { 'User-Agent': '...', 'Referer': 'https://example.com/', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', # 注意:requests自动处理压缩,不要在这里设置 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }Cookie与Session:有些网站需要登录后才能访问,或者利用Cookie跟踪会话。你可以:
- 手动复制:登录后,从浏览器开发者工具中复制
Cookie字符串,将其添加到headers中。 - 使用
requests.Session():Session对象可以自动处理Cookie,像浏览器一样保持登录状态。
session = requests.Session() # 先模拟登录(如果需要) login_data = {'username': 'your_user', 'password': 'your_pass'} session.post(login_url, data=login_data) # 后续请求都用这个session html = session.get(target_url, headers=headers).text- 手动复制:登录后,从浏览器开发者工具中复制
IP限制与代理:如果同一个IP在短时间内发送过多请求,可能会被暂时封禁。解决方案是使用代理IP池。但对于初学者和轻度爬取,更实际的做法是:严格遵守爬取礼仪,加大请求间隔,并尽量在网站访问低峰期(如深夜)运行脚本。
重要提示:务必遵守法律法规和网站的
robots.txt协议。不要对网站造成过大负担(每秒请求数控制在个位数)。爬取的数据仅用于个人学习或分析,切勿用于商业用途或侵犯他人权益。这是每个爬虫开发者应有的职业道德和法律底线。
7.3 项目进阶方向
当你掌握了基础爬虫后,可以尝试以下方向来提升你的技能树:
- 动态内容爬取:很多现代网站(如单页应用SPA)的数据是通过JavaScript异步加载的。
requests+BeautifulSoup无法获取这些内容。这时需要用到Selenium或Playwright这类浏览器自动化工具,它们可以驱动真实的浏览器(如Chrome)渲染页面,再获取完整的HTML。它们的优点是能处理任何JS渲染的内容,缺点是速度慢、资源消耗大。 - Scrapy框架:对于大型、复杂的爬虫项目,手动管理请求、解析、管道、去重会非常繁琐。Scrapy是一个专业的、异步的爬虫框架,它提供了项目结构、中间件、管道、调度器等一套完整机制,让开发大规模爬虫变得井井有条。它是企业级爬虫项目的首选。
- 数据清洗与分析:爬取数据只是第一步。你可以结合
pandas库对数据进行清洗、转换和分析,用matplotlib或seaborn进行可视化,真正从数据中挖掘出价值。 - 定时任务与部署:让爬虫自动定期运行。可以在本地使用系统的定时任务(如Linux的
cron,Windows的“任务计划程序”),或者部署到云服务器上。对于更复杂的调度,可以学习Celery等分布式任务队列。
从“零基础”到能写出一个稳定运行的小爬虫,你已经迈出了坚实的一步。爬虫技术的核心在于对网络协议(HTTP/HTTPS)和文档结构(HTML/DOM)的理解,以及耐心细致的调试。剩下的,就是在不断的实战中,去面对千变万化的网页结构,积累属于你自己的“解析经验库”。记住,遇到问题多查文档(requests,BeautifulSoup官方文档非常友好)、多利用开发者工具分析、善用搜索引擎,你解决问题的能力会在这个过程中飞速成长。
本文还有配套的精品资源,点击获取