前几天在某个技术群里看到有人问有没有现成的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/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } def fetch_page(url, timeout10, max_retries3, delay1): 下载网页内容带重试机制。 url: 目标地址 timeout: 单次请求超时时间秒 max_retries: 最大重试次数 delay: 重试前的等待时间秒 for attempt in range(max_retries): try: resp requests.get(url, headersHEADERS, timeouttimeout) 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 classitem h2 classtitle某标题/h2 span classdate2024-01-15/span div classcontent正文内容.../div /div soup BeautifulSoup(html, lxml) results [] items soup.select(div.item) # CSS选择器选出所有 classitem 的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 rvar 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, filenameoutput.csv): if not data: print([提示] 没有数据可保存) return keys data[0].keys() with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnameskeys) writer.writeheader() writer.writerows(data) print(f[成功] 数据已保存至 {filename}共 {len(data)} 条) def save_to_sqlite(data, db_pathoutput.db, table_nameitems): 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( fINSERT 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_page1, end_page5): 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规律。大多数网址确实都是这种?page1、?page2的递进结构但也有很多网站用的是别的分页方式比如/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目标网站证书不信任或加密方式特殊在请求中加verifyFalse参数并用urllib3.disable_warnings()关闭警告中文全部变成乱码响应编码推断错误或未设置encoding使用resp.encoding resp.apparent_encodingAttributeError: NoneType object has no attribute text解析时标签定位失败说明页面结构变了先打印HTML片段确认标签再调整选择器请求返回403服务器识别出是爬虫请求更新User-Agent增加Cookie头降低请求频率数据只爬到一半就停了中途触发反爬或网络波动加异常捕获和断点续爬机制每页结果即时存储存到CSV后Excel打开乱码CSv编码不是utf-8-sig保存时指定encodingutf-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打出来下一步优化的方向自然就清楚了——先看哪一步永远在报错再看哪一步耗时最长对症下药往往比搜索引擎更管用。