简介:这是一套面向编程零基础学习者的Python网络爬虫实战入门资源,聚焦HTML解析驱动的数据采集与多格式存储全流程,解决初学者从环境搭建、网页请求、结构化提取到本地持久化的完整链路问题。资源包共30个文件,含17个可直接运行的Python脚本(覆盖requests抓取、BeautifulSoup/lxml解析、维基百科词条递归采集、链接去重、数据库写入等典型场景)、6份PDF技术文档(含《Web Scraping with Python》核心章节、Beautiful Soup权威指南及Scrapy入门教程)、5个XML配置与项目元数据文件,以及README说明和IDE配置文件,总大小96.04MB。内容预览显示项目已结构化组织为crawler-master主模块,包含互联网采集、新闻抓取、图片正则提取、六度空间链接分析等进阶实践案例。已有39人下载学习,配套代码即学即用,注释清晰,涵盖反爬应对思路、动态内容处理提示及数据合规采集原则,是构建扎实爬虫工程能力的高实操性入门素材。
1. 从零开始的爬虫之旅:为什么是Python和HTML解析?
如果你是一个对数据感兴趣,但又觉得编程门槛高不可攀的初学者,那么“Python + HTML解析”这个组合,可能就是为你量身定做的第一把钥匙。我见过太多人,一提到“爬虫”就觉得是黑客行为,或者认为需要高深的计算机知识才能入门。其实不然,爬虫的本质,就是模拟一个普通用户去访问网页,然后把网页上你感兴趣的信息,用一种程序化的方式“拿”下来。这个过程,和你用浏览器打开一个网页,然后用眼睛看、用手复制粘贴到Excel里,在逻辑上没有任何区别,只是前者更快、更准、更不知疲倦。
为什么是Python?答案很简单:它足够简单,也足够强大。Python的语法接近自然英语,一个完全没有编程基础的人,花上半天时间就能看懂基础的变量、循环和判断语句。更重要的是,围绕Python有一个极其繁荣的“生态”,这意味着有无数前辈已经为你可能遇到的各种问题写好了现成的工具包(库),你只需要像搭积木一样调用它们。对于爬虫来说,最核心的几块“积木”——requests用于网络请求,BeautifulSoup或lxml用于解析HTML——都成熟得不能再成熟,文档齐全,社区活跃,你遇到的99%的问题都能在网上找到答案。
而HTML,则是这一切的基石。你可以把整个互联网想象成一个由无数个HTML文档链接起来的巨大网络。每一个网页,本质上就是一个HTML文件。你的浏览器收到这个文件后,会按照HTML的规则把它渲染成你看到的图文并茂的页面。我们做爬虫,就是直接去获取这个最原始的HTML文件,然后从中提取我们需要的数据。所以,学习爬虫,第一步不是学多么复杂的编程技巧,而是要学会看懂HTML的结构。这就像你要在一本书里找一段话,你得先知道这本书的目录和章节是怎么排版的。HTML通过一系列的“标签”(如<div>,<p>,<a>,<span>)来定义内容的结构和样式,我们的任务就是找到目标数据被哪个标签“包裹”着,然后告诉程序把它“拆”出来。
很多人卡在第一步,觉得看HTML源码像看天书。其实你完全不需要成为前端开发专家。你只需要掌握最核心的一点:HTML是一种嵌套的、有层次的结构。一个标签里面可以包含另一个标签,形成父子关系。我们提取数据,绝大多数时候就是在利用这种层次关系进行定位。比如,你想抓取一个新闻列表,你会发现每条新闻的标题都放在一个结构类似的<div class="news-item">里,里面又分别有<h2>放标题,<span class="time">放时间。你的爬虫程序要做的,就是先找到所有class为"news-item"的<div>标签(父级),然后分别进入每一个这样的<div>,去提取里面的<h2>和<span>的内容(子级)。理解了这一点,你就掌握了基于HTML解析的爬虫最核心的思维模型。
2. 环境搭建与核心工具库:打造你的数据采集工具箱
在开始写第一行爬虫代码之前,我们需要先把“战场”布置好。这个过程可能会让一些新手感到畏惧,但请放心,跟着步骤一步步来,绝对可以搞定。我个人的体会是,一次性地把环境配置妥当,远比每次运行时被各种报错折磨要高效得多。
2.1 Python安装与虚拟环境管理
首先,你需要安装Python。访问Python官网,下载最新稳定版本(比如3.8以上)的安装包。安装时,务必勾选“Add Python to PATH”这个选项,这能让你在系统的任何地方都能通过命令行调用Python。安装完成后,打开命令行(Windows上是CMD或PowerShell,Mac/Linux上是Terminal),输入python --version,如果能看到版本号,恭喜你,第一步成功了。
接下来是至关重要的一步:创建虚拟环境。这是很多教程会忽略,但实际开发中必不可少的好习惯。虚拟环境相当于为你当前的项目创建一个独立的、干净的Python运行空间,里面安装的库只对这个项目生效,不会影响系统里其他的Python项目。这样可以完美解决不同项目依赖不同版本库的冲突问题。创建方法很简单,在你打算存放项目的文件夹里,打开命令行,执行:
python -m venv spider_env这条命令会创建一个名为spider_env的文件夹,里面就是独立的Python环境。然后,你需要激活它:
- Windows:
spider_env\Scripts\activate - Mac/Linux:
source spider_env/bin/activate
激活后,你的命令行提示符前面通常会显示(spider_env),表示你已经在这个虚拟环境里了。之后所有pip install的操作,库都会被安装到这个环境里。
2.2 爬虫核心三件套:Requests, BeautifulSoup, lxml
环境准备好后,我们来安装爬虫最核心的三个库。在激活的虚拟环境下,执行以下命令:
pip install requests beautifulsoup4 lxml我来解释一下为什么是这三个,以及它们各自扮演什么角色:
Requests: 这是我们的“网络浏览器”。它的唯一任务就是向指定的网址(URL)发送HTTP请求,并把服务器返回的响应(通常是HTML文本)拿回来。它比用浏览器手动访问更高效,而且可以自定义请求头、携带参数、处理Cookie等。你可以把它想象成一个只会“要东西”和“收东西”的机器人。
BeautifulSoup (bs4): 这是我们的“信息提取器”。
Requests拿回来的是一大坨混杂着标签、样式、脚本的HTML字符串,人类很难直接阅读。BeautifulSoup的作用就是解析这坨字符串,把它变成一个结构清晰的、可以让我们方便地查询和遍历的“树状”对象。它提供了一系列像find(),find_all(),select()这样直观的方法,让我们能根据标签名、属性(如class,id)来定位元素。它本身不负责解析,需要指定一个“解析器”,这就是为什么我们要安装lxml。lxml: 这是
BeautifulSoup推荐使用的解析器,速度非常快,容错能力也强。虽然Python标准库里有html.parser,但在处理复杂或不规范的HTML时,lxml的表现要稳定得多。安装它相当于给BeautifulSoup配了一把锋利的刀。
这里有一个非常重要的实操心得:很多网站会对请求头进行简单的检查,如果发现你不是一个正常的浏览器(比如缺少User-Agent字段),可能会拒绝你的请求或返回错误页面。因此,在使用requests时,养成习惯,总是带上一个合理的请求头。一个最简单的例子:
import requests 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' } url = 'https://example.com' response = requests.get(url, headers=headers)这个User-Agent字符串是模仿Chrome浏览器的,能帮你绕过很多基础的反爬机制。记住,requests.get()返回的response对象,其.text属性就是我们需要的HTML字符串。
3. 实战解析:手把手拆解一个网页并提取数据
理论说再多,不如动手做一遍。我们找一个结构清晰、内容公开的网站作为练习目标。为了避免法律和伦理风险,我们选择各大高校或机构提供的公开数据集页面,或者像“豆瓣读书Top250”这类经典的、对爬虫相对友好的练习场(实际操作时请注意频率,避免对服务器造成压力)。这里,我们以解析一个简单的静态新闻列表页为例,讲解完整的流程。
假设我们有一个目标页面,它的HTML结构大致如下(这是简化后的核心部分):
<html> <body> <div class="news-list"> <div class="news-item"> <h2><a href="/news/1">Python 3.12 发布,性能提升显著</a></h2> <p class="summary">最新版本的Python在解释器性能方面有重大优化...</p> <span class="meta">发布于:2023-10-02 | 来源:官方</span> </div> <div class="news-item"> <h2><a href="/news/2">人工智能辅助编程工具展望</a></h2> <p class="summary">AI正在改变开发者编写代码的方式...</p> <span class="meta">发布于:2023-10-01 | 来源:科技媒体</span> </div> </div> </body> </html>我们的目标是提取每条新闻的标题、链接、摘要和发布日期。
3.1 第一步:获取页面与创建Soup对象
首先,我们用requests获取页面内容,并用BeautifulSoup进行解析。
import requests from bs4 import BeautifulSoup url = '你的目标网址' headers = {'User-Agent': '你的浏览器UA'} response = requests.get(url, headers=headers) # 检查请求是否成功,状态码200表示成功 if response.status_code == 200: # 使用 lxml 解析器来解析 HTML 内容 soup = BeautifulSoup(response.text, 'lxml') else: print(f'请求失败,状态码:{response.status_code}')这里有个关键细节:一定要检查response.status_code。如果不是200,可能是页面不存在、需要登录、触发了反爬等。直接对非200响应的.text进行操作可能会导致后续解析出错。
3.2 第二步:定位与提取数据
现在,soup对象就是整个页面的结构化代表。我们需要找到所有class为"news-item"的<div>标签。
# 使用 find_all 方法找到所有符合条件的新闻条目 news_items = soup.find_all('div', class_='news-item') for item in news_items: # 在每个 news-item 内部进行提取接下来,我们在每个item(即一个<div class="news-item">)内部进行精细提取。
提取标题和链接:标题在<h2>标签内的<a>标签里。
title_tag = item.find('h2').find('a') # 先找到h2,再找到里面的a if title_tag: title = title_tag.get_text(strip=True) # 获取标签内的文本,并去除首尾空格 link = title_tag.get('href') # 获取a标签的href属性,即链接 else: title, link = None, None为什么用.get_text(strip=True)?因为标签内的文本可能包含换行和多余空格,strip=True参数能自动清理,让数据更干净。
提取摘要:摘要在class为summary的<p>标签里。
summary_tag = item.find('p', class_='summary') summary = summary_tag.get_text(strip=True) if summary_tag else None提取发布日期:日期信息在<span class="meta">里,但和其他信息混在一起。我们需要用字符串处理的方法把它分离出来。
meta_tag = item.find('span', class_='meta') if meta_tag: meta_text = meta_tag.get_text() # 假设格式固定,可以用 split 方法分割字符串 # 例如:“发布于:2023-10-02 | 来源:官方” parts = meta_text.split('|') for part in parts: if '发布于:' in part: publish_date = part.replace('发布于:', '').strip() break else: publish_date = None else: publish_date = None这个过程清晰地展示了爬虫数据提取的典型模式:先定位大容器(父节点),再逐层深入,定位具体的子元素,最后提取文本或属性。find()和find_all()是最常用的方法,find()返回第一个匹配的元素,find_all()返回一个列表。
3.3 第三步:应对HTML结构变化与异常处理
网页结构不是一成不变的,网站改版是常有的事。一个健壮的爬虫必须考虑这种可能性。上面的代码有一个潜在问题:如果某个news-item里缺少<h2>或<p class=”summary”>,直接调用.find()再链式调用会抛出AttributeError(因为None对象没有.find()方法)。
更安全的写法是每一步都做判断,或者使用try...except。此外,BeautifulSoup的select()方法支持CSS选择器,有时写起来更简洁直观。例如,提取所有标题链接可以写成:
title_links = soup.select('div.news-item h2 a') for link in title_links: print(link.get_text(), link['href'])select('div.news-item h2 a')的意思就是:选择所有在<div class=”news-item”>内部的<h2>内部的<a>标签。这种写法在复杂嵌套结构中尤其好用。
一个重要的经验:在编写提取逻辑时,不要一次性写死。应该先用浏览器开发者工具(F12)仔细分析目标页面的HTML结构,把可能的变化点(比如某个类名、某个包裹层级)考虑进去。写好代码后,先用一小部分数据测试,打印出中间结果,确保每一步都按预期工作,再处理大量数据。
4. 数据的归宿:从内存到硬盘的存储策略
费了九牛二虎之力把数据从网上“扒”下来,如果只是打印在屏幕上或者留在程序的内存里,程序一关就全没了,这显然不是我们想要的结果。数据存储是爬虫项目的“最后一公里”,也是体现项目价值的关键。根据数据量、后续用途和个人习惯,有几种常见的存储方式。
4.1 CSV文件:轻量级结构化存储的首选
对于大多数初学者和小规模项目,CSV(逗号分隔值)文件是最友好、最通用的选择。它可以用Excel、Numbers或文本编辑器直接打开查看,也可以轻松导入到数据库或其他分析工具中。Python标准库中的csv模块让读写CSV变得非常简单。
接续上面的例子,我们将提取到的数据存储到CSV文件中:
import csv # 假设我们已经有了一个包含多条新闻数据的列表 news_data # 每条数据是一个字典,例如:{'title': '...', 'link': '...', 'summary': '...', 'date': '...'} filename = 'news_data.csv' # 定义CSV文件的列名(表头) fieldnames = ['title', 'link', 'summary', 'publish_date'] with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() # 写入表头 for news in news_data: writer.writerow(news) # 逐行写入数据这里有三个关键细节:
newline='':这个参数在Windows系统下尤为重要,可以避免写入的行之间出现空行。encoding='utf-8-sig':使用UTF-8编码并写入BOM(签名)。这对于包含中文等非ASCII字符的数据非常友好,能确保用Excel打开时不会出现乱码。如果确定只用其他工具处理,使用utf-8也可以。DictWriter:它允许我们使用字典来写入数据,字典的键对应CSV的列名,这样代码可读性更高,也不容易搞错列的顺序。
4.2 JSON文件:嵌套结构数据的天然载体
如果采集的数据本身结构就比较复杂,比如每条数据里又包含一个列表或字典(例如,一条商品信息里包含多个颜色尺码的库存),那么JSON格式比CSV更合适。JSON保持了数据的层次结构,并且被几乎所有编程语言支持。Python的json模块使用起来更是直观。
import json filename = 'news_data.json' # 直接将整个 news_data 列表(列表内是字典)保存为JSON文件 with open(filename, 'w', encoding='utf-8') as f: # indent 参数让JSON文件格式化输出,更易于阅读 json.dump(news_data, f, ensure_ascii=False, indent=2)ensure_ascii=False这个参数至关重要,它保证中文字符能以其原本的形式(而不是\uXXXX的Unicode转义序列)存储到文件中,方便直接阅读。
4.3 数据库入门:SQLite的便捷之道
当数据量变大,或者你需要频繁地查询、更新数据时,文件存储就显得力不从心了。这时就该数据库登场了。对于个人项目或中小型爬虫,SQLite是一个完美的起点。它不需要安装独立的数据库服务器,整个数据库就是一个文件,用Python标准库sqlite3就能直接操作,功能却一点也不弱。
首先,我们需要设计一张表来存储新闻数据:
import sqlite3 # 连接到数据库文件(如果不存在则会自动创建) conn = sqlite3.connect('spider_data.db') cursor = conn.cursor() # 创建表 create_table_sql = ''' CREATE TABLE IF NOT EXISTS news ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, link TEXT UNIQUE, -- 链接设为唯一,避免重复存储 summary TEXT, publish_date TEXT, created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- 记录爬取时间 ) ''' cursor.execute(create_table_sql) # 插入数据,假设 news 是一个字典 insert_sql = ''' INSERT OR IGNORE INTO news (title, link, summary, publish_date) VALUES (?, ?, ?, ?) ''' data_tuple = (news['title'], news['link'], news['summary'], news['publish_date']) cursor.execute(insert_sql, data_tuple) # 提交事务,使更改生效 conn.commit() # 关闭连接 cursor.close() conn.close()这里有几个必须注意的要点:
- 使用参数化查询(?占位符):绝对不要用字符串拼接的方式将变量直接放入SQL语句(如
f”INSERT … VALUES (‘{title}’)”),这会导致严重的SQL注入安全风险。使用?占位符和元组传参是既安全又规范的做法。 INSERT OR IGNORE:这是一个非常实用的技巧。我们为link字段设置了UNIQUE约束,当尝试插入一条链接已存在的记录时,OR IGNORE会忽略这次插入,避免数据重复。对于爬虫来说,这能有效避免因重复爬取相同页面导致的数据冗余。- 事务提交:对数据库的增删改操作,需要在最后执行
conn.commit()才会真正保存。养成好习惯,在完成一批操作后统一提交,效率更高。 - 关闭连接:操作完成后,显式地关闭
cursor和connection是一个好习惯,虽然在某些情况下解释器退出时会自动关闭,但显式关闭能确保资源立即释放。
对于初学者,从CSV开始,过渡到JSON处理复杂数据,再在项目需要时引入SQLite,是一个平滑且实用的学习路径。存储方式的选择没有绝对的好坏,只有是否适合当前的项目阶段和需求。
5. 避坑指南与效率提升:新手常犯的五个错误
掌握了基本流程后,想要让爬虫跑得更稳、更快、更长久,就需要了解一些常见的“坑”和优化技巧。这些都是我在早期爬虫实践中用时间和报错信息换来的经验。
5.1 错误一:忽视请求头与基础反爬
这是新手最常踩的坑。你的代码在本机运行得好好的,一放到服务器上或者多跑几次就返回403错误或者跳转到验证页面。很大概率是因为你的请求头太“假”了。除了之前提到的User-Agent,一个看起来更真实的请求头还应该包含Referer(你从哪个页面跳转过来的)、Accept-Language等字段。你可以用浏览器开发者工具的“网络(Network)”选项卡,查看一次真实的网页请求所携带的所有头部信息,然后模仿着构建你的headers字典。
进阶技巧:有些网站会检查Cookie来维持会话状态。对于这类网站,你可以先用浏览器手动登录,然后从开发者工具中复制出Cookie字符串,添加到你的requests请求头中。但请注意,Cookie通常有有效期,且涉及隐私,此方法仅用于学习测试。
5.2 错误二:缺乏异常处理与重试机制
网络是不稳定的,目标服务器也可能临时出问题。如果你的代码里没有try...except,那么一次网络波动就可能导致整个程序崩溃。对于网络请求,最基本的异常处理是必要的。
import time from requests.exceptions import RequestException def safe_request(url, headers, retries=3): for i in range(retries): try: resp = requests.get(url, headers=headers, timeout=10) # 设置超时 resp.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 return resp except RequestException as e: print(f'第{i+1}次请求失败: {e}') if i < retries - 1: wait_time = 2 ** i # 指数退避,等待1, 2, 4秒... print(f'等待{wait_time}秒后重试...') time.sleep(wait_time) else: print('重试次数用尽,放弃请求。') return None这个safe_request函数实现了简单的重试机制和指数退避等待,能显著提升程序在恶劣网络环境下的健壮性。timeout参数也至关重要,它防止程序因为某个请求没有响应而永远卡住。
5.3 错误三:暴力请求,触发反爬被封IP
如果你编写一个循环,一秒钟内请求同一个网站几十次,服务器很可能会把你的IP地址暂时甚至永久封禁。这是最直接的反爬手段。尊重网站,控制频率是爬虫工程师的基本素养。
最朴素也最有效的方法是,在每次请求之间随机休眠一段时间。
import time import random for url in url_list: response = requests.get(url, headers=headers) # ... 处理数据 ... # 随机等待1到3秒 time.sleep(random.uniform(1, 3))对于严肃的项目,可以考虑使用更智能的调度,或者通过代理IP池来分散请求。但对于初学者和道德爬取,time.sleep()就是你的好朋友。
5.4 错误四:解析逻辑过于脆弱,依赖固定结构
你的解析代码严重依赖于<div class=”news-item”>这样的特定标签和类名。一旦网站前端改版,类名变了,或者HTML结构微调了,你的爬虫立刻失效。为了增加鲁棒性:
- 多用相对定位,少用绝对定位:不要依赖像
soup.find_all(‘div’)[7]这样的绝对索引。 - 组合使用多种属性:如果一个
class不稳定,可以尝试结合标签名、其他属性(如id、>response.encoding = 'gbk' # 或者 ‘utf-8’, ‘gb2312’ html_text = response.text更稳妥的方法是使用
response.content(它是原始的字节流),然后用chardet这样的库去检测编码:import chardet raw_data = response.content detected_encoding = chardet.detect(raw_data)['encoding'] html_text = raw_data.decode(detected_encoding, errors='ignore') # errors参数决定如何处理解码错误处理编码问题虽然繁琐,但却是保证数据质量不可跳过的一环。看到乱码不要慌,检查并修正编码设置,十有八九能解决问题。
6. 项目实战:构建一个完整的、可复用的爬虫脚本
现在,我们把前面所有的知识点串联起来,构建一个结构清晰、功能完整、具有一定容错能力的爬虫脚本。这个脚本将包含配置、请求、解析、存储和日志等模块,虽然简单,但骨架是专业的。
6.1 项目结构设计
一个好的项目应该易于理解和维护。我们为这个爬虫项目设计一个简单的结构:
my_spider_project/ ├── config.py # 配置文件,存放URL、请求头、数据库路径等 ├── spider.py # 主爬虫逻辑 ├── storage.py # 数据存储相关函数(CSV、数据库) ├── utils.py # 工具函数,如安全请求、日志设置 └── main.py # 程序入口,调度整个流程config.py:集中管理配置,方便修改。# config.py BASE_URL = 'https://example-news-site.com/list' HEADERS = { 'User-Agent': 'Mozilla/5.0 ...', 'Accept-Language': 'zh-CN,zh;q=0.9', } DB_PATH = 'data/news.db' CSV_PATH = 'data/news.csv' REQUEST_TIMEOUT = 10 RETRY_TIMES = 3utils.py:封装工具函数。# utils.py import time import random import logging from requests.exceptions import RequestException import requests def setup_logger(): """配置日志,方便追踪程序运行状态""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('spider.log'), logging.StreamHandler() ] ) return logging.getLogger(__name__) def safe_request(url, headers, timeout, retries): """带重试和异常处理的请求函数""" logger = logging.getLogger(__name__) for i in range(retries): try: resp = requests.get(url, headers=headers, timeout=timeout) resp.raise_for_status() # 随机延迟,模拟人类操作,避免请求过快 time.sleep(random.uniform(1, 2)) return resp except RequestException as e: logger.warning(f'请求 {url} 失败 (尝试 {i+1}/{retries}): {e}') if i < retries - 1: wait = (2 ** i) + random.random() time.sleep(wait) else: logger.error(f'请求 {url} 最终失败。') return None6.2 主爬虫逻辑 (
spider.py)这是核心,负责抓取和解析。
# spider.py from bs4 import BeautifulSoup from config import BASE_URL, HEADERS, REQUEST_TIMEOUT, RETRY_TIMES from utils import safe_request, setup_logger logger = setup_logger() def fetch_page(page_num): """获取指定页码的列表页""" url = f'{BASE_URL}?page={page_num}' logger.info(f'开始抓取: {url}') response = safe_request(url, HEADERS, REQUEST_TIMEOUT, RETRY_TIMES) if response and response.status_code == 200: return response.text else: return None def parse_list_page(html): """解析列表页,提取详情页链接和基础信息""" soup = BeautifulSoup(html, 'lxml') news_list = [] # 假设列表项容器是 article.news-card items = soup.select('article.news-card') for item in items: try: title_elem = item.select_one('h2 a') title = title_elem.get_text(strip=True) if title_elem else 'N/A' link = title_elem['href'] if title_elem else '#' # 补全可能为相对路径的链接 if link.startswith('/'): link = 'https://example-news-site.com' + link summary_elem = item.select_one('p.description') summary = summary_elem.get_text(strip=True) if summary_elem else '' date_elem = item.select_one('time') publish_date = date_elem['datetime'] if date_elem and date_elem.has_attr('datetime') else (date_elem.get_text(strip=True) if date_elem else '') news_list.append({ 'title': title, 'link': link, 'summary': summary, 'publish_date': publish_date }) except Exception as e: logger.error(f'解析列表项时出错: {e}', exc_info=True) continue # 跳过此项,继续解析下一个 logger.info(f'本页解析到 {len(news_list)} 条新闻。') return news_list6.3 数据存储 (
storage.py)提供不同的存储方式。
# storage.py import csv import json import sqlite3 from config import DB_PATH, CSV_PATH def save_to_csv(data_list, filename=CSV_PATH): """保存数据到CSV文件""" if not data_list: return fieldnames = data_list[0].keys() with open(filename, 'a', newline='', encoding='utf-8-sig') as f: # 使用追加模式 ‘a’ writer = csv.DictWriter(f, fieldnames=fieldnames) if f.tell() == 0: # 如果文件是空的,写入表头 writer.writeheader() writer.writerows(data_list) def save_to_db(data_list, db_path=DB_PATH): """保存数据到SQLite数据库""" conn = sqlite3.connect(db_path) cursor = conn.cursor() # 确保表存在(同前文) create_table_sql = '''CREATE TABLE IF NOT EXISTS news (...)''' cursor.execute(create_table_sql) insert_sql = '''INSERT OR IGNORE INTO news (title, link, summary, publish_date) VALUES (?, ?, ?, ?)''' for data in data_list: cursor.execute(insert_sql, (data['title'], data['link'], data['summary'], data['publish_date'])) conn.commit() conn.close()6.4 程序入口 (
main.py)负责调度整个流程,比如爬取前5页。
# main.py from spider import fetch_page, parse_list_page from storage import save_to_csv, save_to_db from utils import setup_logger logger = setup_logger() def main(): all_news = [] for page in range(1, 6): # 爬取1到5页 html = fetch_page(page) if html: news_on_page = parse_list_page(html) all_news.extend(news_on_page) else: logger.error(f'第 {page} 页抓取失败,停止后续抓取。') break if all_news: logger.info(f'共抓取到 {len(all_news)} 条数据,开始存储。') # 可以选择存储到CSV或数据库,或两者都存 save_to_csv(all_news) save_to_db(all_news) logger.info('数据存储完毕。') else: logger.warning('未抓取到任何数据。') if __name__ == '__main__': main()这个项目结构虽然基础,但已经具备了模块化、配置化、日志化和错误处理等良好实践的雏形。你可以在此基础上,轻松地扩展功能,比如增加代理支持、解析详情页、定时任务调度等。记住,爬虫项目不是一蹴而就的,而是需要根据目标网站的变化和自身需求的增长不断迭代和维护的。从这样一个清晰的结构开始,会让未来的维护工作轻松很多。
本文还有配套的精品资源,点击获取