☰
Python爬虫完整代码:从请求解析到数据落库的工程化实现
2026/10/3 18:16:06 网站建设 项目流程

前几天在某个技术群里看到有人问"有没有现成的Python爬虫代码能直接用",下面跟了一串"蹲一个""同求"。说实话这种问题我太理解了,很多人学爬虫卡在第一步——不是不知道要用requests,而是不知道一个完整项目该怎么串起来:请求要带什么头、解析用哪种方式、数据怎么存、报错了怎么处理。这篇文章干脆把我自己一直在用的那套"通用型"Python爬虫完整代码拿出来,从环境搭建到数据落库一条链路讲清楚,你可以直接复制改改就能用,也可以把它当成一个骨架来理解爬虫的工作原理。适合刚入门Python、想快速跑通第一个爬虫项目的朋友,也适合那些写过几次爬虫但总觉得代码不够工程化的老手,对照着看一眼自己漏了哪些细节。

因为内容比较多,我把整份代码拆成几个核心模块来讲解,每个模块都附上完整代码和必要的解释,最后再把我这些年踩过的坑集中整理一遍。你不需要一开始就全部看懂,先把代码跑起来,然后对着文章一步步理解为什么这么写,效果是最好的。

1. 项目整体设计与思路拆解

1.1 为什么爬虫项目必须做模块化设计

很多人写爬虫的习惯是打开编辑器,从头到尾一股脑写下来:发请求、解析、存文件全塞在一个脚本里。这种写法对付一次性小任务没问题,但只要你打算长期用、换数据源、或者给别人看代码,问题立刻就出来了——改一个解析规则可能要翻遍几百行代码,遇到反爬要调整请求参数时一不留神就把别的逻辑也动坏了。

我自己的经验是,一个能称作"完整"的爬虫项目,至少应该拆成四个独立的部分:下载器负责拿数据,解析器负责从拿到的内容里抽信息,存储模块负责把结果持久化,调度入口负责把前三者串起来并控制流程。这样做的好处很直接:网站改版了只需要改解析器,被封了只需要换下载器的请求策略,想从存CSV改成存数据库也只需动存储模块。代码之间通过函数签名和返回值对接,互不干扰。

这份代码我在设计的时候遵循了三个原则:第一,用最简单直接的第三方库实现,不引入过于复杂的框架,保证新手能看懂;第二,所有可变参数(请求头、目标URL、保存路径)都集中放顶部配置区,方便你替换成自己的目标网站;第三,异常处理尽量兜底,哪怕某个请求失败了也不会让整个程序崩溃退出。

1.2 通用爬虫框架的组成结构

如果把一个爬虫页面比作一个自动化处理文件的任务,那么整套流程就是:先把文件拿过来(下载),然后按规则翻到具体页(解析),再把有用的信息抄到表格里(存储),最后反复执行这个过程直到把所有文件处理完(调度)。

在这份代码里,我画了一条非常标准的流水线:

  • 配置区:定义目标URL、请求头、超时时间、重试次数、下载间隔等参数
  • 下载模块:基于requests封装,支持异常重试和超时控制,返回HTML文本或JSON内容
  • 解析模块:优先用BeautifulSoup定位HTML结构,遇到需要精确匹配的文本用正则表达式补充
  • 数据清洗模块:去掉无用字符、规范化日期和数字格式、去掉重复项
  • 存储模块:提供CSV和SQLite两种落库方式,按需要切换
  • 主控制模块:遍历URL列表、控制请求频率、记录日志、统计成功失败数量

这套结构不是我为某个特定网站设计的,而是一套通用的"骨架代码"。你拿到以后最需要改的就是解析模块——因为不同网站的HTML结构差异极大。其他模块百分之八九十的概率可以原封不动直接复用。

2. 核心代码模块拆解

2.1 环境准备与依赖安装

在跑代码之前请确保电脑上已经装好了Python 3.8以上版本,并且装好了两个核心依赖库:requests和beautifulsoup4。很多人在第一步就栽跟头:用文本编辑器写完了代码,到命令行为说找不到模块,原因就是没装依赖或者装错了Python环境。

打开命令行执行下面的安装命令:

pip install requests beautifulsoup4 lxml

如果是在国内网络环境下装得慢,可以临时切换镜像源:

pip install requests beautifulsoup4 lxml -i https://pypi.tuna.tsinghua.edu.cn/simple

这里我额外推荐安装lxml,它是BeautifulSoup的底层解析器,比Python自带的html.parser速度快很多,尤其在处理复杂嵌套的页面时差距非常明显。装完以后打开Python交互环境输入import requests跑一下,不报错就说明环境正常了。

还有一个高频问题需要提前说明:如果你电脑上同时装了Python 2和Python 3,或者用了一些数据科学发行版,注意区分pip和pip3的区别。直接用python --version看一下当前默认解释器版本,再决定用哪个命令安装,能省去后面一连串"模块找不到"的麻烦。

2.2 请求模块:带重试机制的下载器

请求是整个爬虫项目中最容易出问题的环节。网站服务器从你的请求头部就能判断出你是不是爬虫,所以一个像样的请求头是必须具备的。这里我封装了一个下载函数,它做了三件看似基础但非常重要的事:伪装浏览器请求头、超时控制、自动重试。

import requests import time from requests.exceptions import RequestException 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", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } def fetch_page(url, timeout=10, max_retries=3, delay=1): """ 下载网页内容,带重试机制。 url: 目标地址 timeout: 单次请求超时时间(秒) max_retries: 最大重试次数 delay: 重试前的等待时间(秒) """ for attempt in range(max_retries): try: resp = requests.get(url, headers=HEADERS, timeout=timeout) if resp.status_code == 200: resp.encoding = resp.apparent_encoding or "utf-8" return resp.text else: print(f"[警告] 状态码异常: {resp.status_code}, URL: {url}") except RequestException as e: print(f"[错误] 第 {attempt + 1} 次请求失败: {e}, URL: {url}") if attempt < max_retries - 1: time.sleep(delay) return None

有几个容易被忽略的细节值得专门强调。第一是resp.encoding这行,很多网站返回的响应头里没写清楚编码,或者写了charset但实际内容和它不符,拿到的文本就会变成乱码。用apparent_encoding可以自动从页面内容推断编码,这是解决中文乱码最省事的方式。第二是delay参数在重试等待时有大用,如果连续请求被服务器拒绝,加一个短暂等待再试,成功率能提升不少。第三,status_code的判断要留意,有些网站会返回200但内容是验证码跳转页,那种情况纯靠状态码判断不了,需要后续在解析层再来校验内容的有效性。

这里我特别建议把HEADERS作为一个全局变量而不是在函数里每次构造,原因是当你需要维护多个请求头时,可以直接在配置区统一管理,请求头被哪个函数用都不会乱。

2.3 解析模块:BeautifulSoup和正则双剑合璧

拿到HTML文本以后,最关键的事情是从乱糟糟的标签中精准抠出你想要的字段。BeautifulSoup的核心用法就一句话:用选择器定位元素,然后提取文本或属性。下面这段代码我写了两种最常见的解析方式,分别用find系列方法和CSS选择器实现,你按目标网站的结构挑着用:

from bs4 import BeautifulSoup import re def parse_html(html): """ 假设目标页面的结构是: <div class="item"> <h2 class="title">某标题</h2> <span class="date">2024-01-15</span> <div class="content">正文内容...</div> </div> """ soup = BeautifulSoup(html, "lxml") results = [] items = soup.select("div.item") # CSS选择器:选出所有 class=item 的div for item in items: title_tag = item.find("h2", class_="title") date_tag = item.find("span", class_="date") content_tag = item.select_one("div.content") # 注意:如果标签不存在,直接访问 .text 会报 AttributeError title = title_tag.text.strip() if title_tag else "未获取到" date = date_tag.text.strip() if date_tag else "未获取到" content = content_tag.text.strip() if content_tag else "未获取到" # 用正则做二次清洗:去掉多余空白字符 content = re.sub(r"\s+", " ", content) results.append({ "title": title, "date": date, "content": content, }) return results

这个代码里的核心思想是"先定位容器,再提取字段"。你先找到每个重复区块的外层标签(比如每条数据都在一个div.item里),然后在区块内用find定位各个字段。千万避免直接在整页用find_all("h2"),那样会把导航栏、侧边栏里无关的h2标题也抓进来。

正则表达式的应用场景和BeautifulSoup不太一样,它更适合处理那些没有明显标签结构、隐藏在文本或JS变量里的数据。比如页面里有一段var list = ["苹果", "香蕉"],你用BeautifulSoup根本定位不到,因为它在script标签内部,这时候正则一行就搞定了:

import re # 从script标签中提取JSON数组内容 pattern = r'var list = (\[.*?\])' matched = re.search(pattern, html, re.S) if matched: import json data = json.loads(matched.group(1))

关于解析器选择,我个人的习惯是这样的:结构清晰、标准HTML优先用BeautifulSoup,它容错率高,语法直观;需要从字符串中匹配特定模式、或者数据嵌套在HTML属性中时用正则。两种手段配合使用,基本能覆盖日常百分之九十五以上的需求。

2.4 数据清洗与存储模块

数据抓下来只是第一步,直接存进文件再用的时候你就会发现一堆问题:标题前后带着空格、日期格式混乱、正文里有大量换行和缩进、重复数据不知道已经存了没有。所以我在解析之后专门加了一层清洗逻辑,这层虽然代码量不大,但实际体验提升巨大。

清洗的常见操作包括:strip去首尾空白、re.sub把连续空白字符压缩成单个空格、统一日期格式、把纯数字字符串转成int或float、过滤掉空值和明显无意义的内容。下面这个函数演示了基本的清洗流程:

def clean_data(raw_items): cleaned = [] seen_titles = set() # 用于去重 for item in raw_items: title = item["title"].strip() date = item["date"].strip() content = re.sub(r"\s+", " ", item["content"]).strip() # 去重:一些列表页会包含重复推荐条目 if title in seen_titles: continue seen_titles.add(title) if not title and not content: continue # 完全空数据直接丢弃 cleaned.append({ "title": title, "date": date, "content": content, }) return cleaned

存储部分我给了两个选择:CSV格式适合数据量不大、想用Excel打开查看的场景;SQLite适合需要频繁查询、数据量上万条的场景。两者我都在项目里实测跑过,直接把函数贴出来:

import csv import sqlite3 def save_to_csv(data, filename="output.csv"): if not data: print("[提示] 没有数据可保存") return keys = data[0].keys() with open(filename, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=keys) writer.writeheader() writer.writerows(data) print(f"[成功] 数据已保存至 {filename},共 {len(data)} 条") def save_to_sqlite(data, db_path="output.db", table_name="items"): conn = sqlite3.connect(db_path) cur = conn.cursor() # 简单的建表语句,如果表已存在则保留原表 cur.execute(f""" CREATE TABLE IF NOT EXISTS {table_name} ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, date TEXT, content TEXT ) """) for item in data: cur.execute( f"INSERT INTO {table_name} (title, date, content) VALUES (?, ?, ?)", (item["title"], item["date"], item["content"]) ) conn.commit() conn.close() print(f"[成功] 数据已写入 {db_path},共 {len(data)} 条")

用CSV保存时encoding建议选择utf-8-sig,因为这个编码会在文件开头加一个BOM头,Excel打开的时候不会乱码。如果用纯utf-8直接写,Excel双击打开中文大概率是乱码的,这是新手反馈给我最多的问题。SQLite这边要注意表名不要直接用字符串拼接太随意,如果表名是写死的就可以,如果是变量就需要做合法性校验,防止SQL注入。

3. 实操过程与完整落地

3.1 从零跑通一个真实示例

光看代码片段肯定不够,我拿一个具体的例子走一遍完整流程。假设我们要抓取一个简单的新闻列表页,页面结构就是我上面解析模块里写的那种div.item布局,总共要翻5页列表页。

主控制模块的代码如下,它做的事情是构造分页URL、逐页下载、解析、清洗、存储,并且在每页之间强制休眠随机秒数:

import time import random def crawl(base_url_template, start_page=1, end_page=5): all_data = [] for page in range(start_page, end_page + 1): url = base_url_template.format(page) print(f"[信息] 正在爬取第 {page} 页: {url}") html = fetch_page(url) if html is None: print(f"[错误] 第 {page} 页获取失败,跳过") continue parsed = parse_html(html) cleaned = clean_data(parsed) all_data.extend(cleaned) print(f"[信息] 第 {page} 页解析到 {len(cleaned)} 条有效数据") # 随机休眠,模拟人的浏览节奏 sleep_time = random.uniform(1, 3) time.sleep(sleep_time) save_to_csv(all_data, "news.csv") # 如果你想存数据库,把下一行注释去掉 # save_to_sqlite(all_data, "news.db", "news") if __name__ == "__main__": BASE_URL_TEMPLATE = "https://example.com/news?page={}" # 先把 example.com 换成你的目标网址 crawl(BASE_URL_TEMPLATE)

执行的时候直接在终端运行python main.py,控制台会逐行打印进度。第一次跑通以后,你会看到程序自动完成了多页抓取和存储的全部工作,这时候再回去看代码结构,理解起来会顺畅得多。

这里必须提醒一句:上面代码中的example.com是我演示用的占位地址,真实爬取时你需要先手动打开目标网站,用浏览器开发者工具(F12)查看列表页的URL规律。大多数网址确实都是这种?page=1、?page=2的递进结构,但也有很多网站用的是别的分页方式,比如/page/2/或者POST请求翻页。看清楚URL规律再改模板,不要想当然。

3.2 数据落地与异常处理策略

数据落地的环节并不只是把数据写进文件就结束,实际的工程场景里还会出现各种"意外"。最常见的有三种:一是中途某个请求失败,整个程序中断,之前爬到的数据白干;二是目标网站改版后解析规则失效,程序不报错但存下来的都是空字段;三是同一条数据反复爬取多次,库里有大量重复记录。

针对这三种情况,我的做法在代码里其实已经埋了伏笔。下载模块里加了重试机制,但重试三次还失败的时候,前面的实现是直接返回None跳过。对于不重要的数据这样处理没问题,但如果是关键数据,更稳的做法是把它记录到一个专门的"待补抓列表"文件中,等整轮爬取结束后手动或者二次运行补抓。异常处理的粒度不必细到每个字段,但至少要做到:程序不轻易崩、失败了有日志可查、重跑时能跳过已有数据。把这三条做到位,你的爬虫基本就具备"可用"的门槛了。

另外一个很多人忽略的点是"幂等性"。通俗讲就是同一个URL重复爬几遍,得到的结果应该是一致或者可以被安全覆盖的。为了达到这个效果,我在清洗模块里加了seen_titles去重,在SQLite里可以用CREATE TABLE IF NOT EXISTS保证表结构不会重复创建。如果你做一个增量爬虫,还可以在库里加一个UNIQUE约束的字段,比如URL本身,遇到重复就直接忽略,这样即使程序中断重跑,也不会插入大量重复数据。

3.3 代码封装与工程化改造

如果你的爬虫只是自己用一次两次,那写到上面那一步就可以停了。但值得提醒的是,把代码稍微工程化改造一下,后面维护起来会舒服很多。这里说的工程化不是像大厂那样搞复杂框架,而是做两个简单调整:把配置项集中到一个地方管理,把采集结果按日期分组存放。

我经常用的做法是新建一个config.py,把所有可变参数都放在里面,爬虫主文件直接from config import *。这样换目标网站的时候,只需要改配置,完全不用动逻辑代码。配置文件里除了URL、请求头,还可以把爬取的启止页码、请求间隔范围、存储路径都放进去。另一个实用小技巧是存储时自动创建文件夹,按当天日期生成子目录,这样每天的爬取结果互不覆盖,后面做数据对比时非常方便。

4. 常见问题与排查技巧实录

4.1 高频报错与对策速查表

我整理了这几年被问得最多的故障类型,每一类我都实际踩过。这里做成一个表格,方便你对照排查:

报错现象根本原因解决方案
requests抛SSLError目标网站证书不信任或加密方式特殊在请求中加verify=False参数,并用urllib3.disable_warnings()关闭警告
中文全部变成乱码响应编码推断错误或未设置encoding使用resp.encoding = resp.apparent_encoding
AttributeError: 'NoneType' object has no attribute 'text'解析时标签定位失败,说明页面结构变了先打印HTML片段确认标签,再调整选择器
请求返回403服务器识别出是爬虫请求更新User-Agent,增加Cookie头,降低请求频率
数据只爬到一半就停了中途触发反爬或网络波动加异常捕获和断点续爬机制,每页结果即时存储
存到CSV后Excel打开乱码CSv编码不是utf-8-sig保存时指定encoding="utf-8-sig"
程序运行内存越来越大把所有数据都累积在内存列表中改为每页解析完就增量写入文件或数据库

表格里有一项我要特别展开讲:403错误。这几乎是爬虫路上必遇的坎。大部分情况下,你的User-Agent暴露了身份——比如默认的python-requests/2.31.0这种字符串一眼假。解决方式就是在HEADERS里伪装成一个真实浏览器的UA。有些更严的网站会校验Referer,你在 headers 里加上目标页面的域名作为Referer往往也能解决。但如果网站上了更高级的验证码或浏览器指纹校验,那靠纯requests就很难绕过了,需要考虑切换到模拟浏览器方案,比如用Selenium或者Playwright驱动真实浏览器去操作页面。

另外还有一类隐蔽问题:代码不报错,但爬下来的字段全是空值。这通常意味着你的选择器匹配不到元素,但又没有报错,因为代码里做了if tag else "未获取到"的兜底。遇到这种情况,我建议你在解析模块里临时加一行print(soup.prettify()[:2000]),把HTML前两千个字符打出来,肉眼看结构再调整选择器,这是排查最快的方式。兜底逻辑虽然能让程序稳着跑下去,但同时也可能把真正的解析错误藏起来,所以开发调试阶段最好把兜底值改成明显的标记如"MISSING",方便一眼看出哪些字段没抓到。

4.2 断点续爬与日志记录

数据量一旦大了,爬虫跑几个小时是很正常的事情。最难受的莫过于凌晨爬起来发现程序在第三分钟就崩了,前面爬的都白白丢弃。我用了一个非常朴素但有效的方案:每页数据解析完立刻增量写入存储,同时在日志文件里记录当前爬到的页码。程序重启后,读取日志文件里记录的页码,从下一页继续爬,完美做到断点续爬。

日志记录不建议自己格式化字符串print一下就完事,推荐用Python标准库logging,它可以同时输出到控制台和文件,还能自动带上时间戳和级别。写日志不是形式主义,当你需要排查"到底哪一步出了问题"的时候,一份完整日志的价值会完全体现出来。

4.3 爬虫应用的合规建议与个人心得

聊到爬虫,这个话题避不开,我直接说我的观点。写爬虫本身是正常的编程技术,用它做什么、怎么用,才是决定风险的关键。我自己的原则是:只爬取公开可访问的数据,不做突破登录鉴权、绕过验证码等规避平台访问控制的行为;遵守robots.txt的约定;控制请求频率,不对目标服务器造成明显压力;爬取的数据不用于商业牟利或者侵犯个人隐私。简单说,技术可以合法学习,但别把爬虫用到灰色地带里去。

另外我个人还有个习惯:即使是在做技术demo,也会在代码里保留合理的下载延迟,比如每两次请求之间sleep 1到2秒。这不仅是为了合规,更是为了实际效果。高频请求往往导致IP被临时封禁,反而让任务无法完成。把请求频率控制在一个"比较像人"的节奏上,整体稳定性会提升很多,这是经验的差距所在。

5. 进阶扩展方向与性能优化

5.1 从单线程到并发加速

上面这套代码是同步请求的,也就是说发出一个请求、等待响应、处理完毕,然后才开始下一个请求。这个模式在页面数量少的时候没有任何问题,但当你要爬成千上万个页面时,同步等待的时间会非常可观。一个可行的优化思路是用concurrent.futures的ThreadPoolExecutor来并发请求,将网络等待时间重叠,大幅提升效率。

但注意:并发提升的代价是更容易触发反爬。服务器看到某个IP突然产生高频请求,很容易直接封禁。我自己的做法是控制并发数不大于5,并且请求之间保留随机间隔。如果目标网站数据量大,更推荐用异步方案或者分布式方案,而不是开几十个线程玩命请求。

5.2 可视化界面与定时调度

项目跑到后期,很多人会想做一个简单的可视化界面,不想每次都在命令行里敲参数。Python里有一个非常轻量的方案是使用tkinter做一个小窗口,上面放几个输入框:起始页、结束页、保存路径,加一个"开始爬取"按钮。后台线程跑爬虫逻辑,主线程更新进度信息。这个方案技术门槛很低,但能极大提升工具的易用性,尤其是给非技术同事使用的时候。

还有人需要定时抓取,这时候最简单的方式不是自己在Python里写调度逻辑,而是用操作系统的计划任务。Windows上用任务计划程序,Linux上用crontab,每天定时跑一次爬虫脚本,实现完全自动化的数据采集。我认识的很多朋友做商品信息监控、天气预报采集、行业资讯聚合,都是靠这套"爬虫脚本+定时调度"的组合来实现的,成本和维护难度都很低。

5.3 如何把这个骨架扩展成分布式爬虫

当目标网站的数据量达到百万级别,单机爬虫怎么优化都有瓶颈,分布式是必然选择。这里我不展开太深,只说一种最容易理解的过渡方案:把任务队列从代码里挪到一个中间存储上,比如Redis。多个爬虫节点从Redis里循环取URL来爬,爬到的结果统一写入同一个数据库。这样不需要改核心的下载解析逻辑,只需要把主控制模块里的循环改成消费Redis队列。

这个扩展方向适合已经能把单机爬虫写得很稳的人再考虑。如果现阶段还在追着报错跑,建议先把单机项目的健壮性打磨好。我在分布式爬虫上踩过的最大的坑不是技术本身,而是没有充分测试就仓促上马,结果每个节点各自为战,产生了海量重复数据。做分布式前,先保证单机的去重、断点续爬、日志都是可用的,再去横向扩容才不会翻车。

写到这里,这整套代码和思路就完整交付了。我个人在实际使用中的体会是:爬虫项目真正难的不是代码本身,而是面对各种突发状况时有一套稳定排查的思路。你把上面这套骨架跑通了、调好了,之后再遇到任何网站,无非是改改解析规则、调调请求参数的事。别总想着到处复制别人的完整代码,自己动手把这段代码敲一遍,再对照着文章搞懂每一处设计,比什么都强。最后再分享一个小技巧:把这份代码里所有print打出来,下一步优化的方向自然就清楚了——先看哪一步永远在报错,再看哪一步耗时最长,对症下药,往往比搜索引擎更管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询