Python爬虫实战:从requests到Scrapy的抓取、解析与存储全解析
1. 项目概述为什么Python爬虫是数据时代的“瑞士军刀”如果你刚接触Python或者已经写过一些脚本但总觉得知识零散尤其是对“爬虫”这个听起来很酷的词既向往又有点发怵那这篇总结就是为你准备的。我干了十多年数据抓取和自动化从最初用urllib一行行抠数据到后来用Scrapy框架管理千万级页面踩过的坑比写过的代码还多。今天我不讲那些教科书上照搬的理论就从一个一线开发者的视角跟你聊聊Python爬虫那些真正核心、能让你少走弯路的重点知识。简单说Python爬虫就是用程序模拟浏览器自动从互联网上获取你感兴趣的数据。它为什么重要因为数据就是新时代的石油。无论是分析市场趋势、监控竞品价格、聚合新闻资讯还是为你的机器学习模型准备训练数据爬虫都是获取第一手数据最直接、最高效的手段。而Python凭借其简洁的语法、丰富的库生态尤其是requests,BeautifulSoup,Scrapy和强大的社区几乎成了爬虫领域的“官方语言”。但别以为会写个requests.get()就叫会爬虫了真正的挑战在于如何稳定、高效、合法地拿到数据这背后涉及网络协议、反爬对抗、数据解析、任务调度等一系列知识。接下来我就把这些年积累的实战经验掰开揉碎了讲给你听。2. 爬虫核心三要素抓取、解析、存储的深度拆解很多人学爬虫一上来就找代码复制粘贴结果遇到点问题就卡住。其实无论多复杂的爬虫项目都逃不出“抓取-解析-存储”这个核心流程。理解透这三步你就掌握了爬虫的骨架。2.1 网络请求从urllib到requests的进化与选择抓取的第一步是发起网络请求。Python内置的urllib/urllib2Python 2时代或urllib.requestPython 3是官方库功能完备但API设计略显繁琐。对于现代爬虫开发我强烈推荐使用第三方库requests。它几乎成了行业标准原因很简单人性化。一个简单的GET请求对比# 使用 urllib.request (Python 3) import urllib.request import urllib.parse url https://httpbin.org/get req urllib.request.Request(url) response urllib.request.urlopen(req) content response.read().decode(utf-8) print(content) # 使用 requests import requests url https://httpbin.org/get response requests.get(url) print(response.text) # 自动解码 print(response.status_code) # 状态码 print(response.headers) # 响应头高下立判。requests自动处理了编码、连接池、会话保持等底层细节让你专注于业务逻辑。对于需要传递查询参数Query Parameters的GET请求requests也优雅得多import requests params {key1: value1, key2: value2} response requests.get(https://httpbin.org/get, paramsparams) # 实际请求的URL会是https://httpbin.org/get?key1value1key2value2POST请求与表单提交当需要登录或提交数据时就需要用到POST请求。这里的关键是区分data和json参数。data发送表单数据application/x-www-form-urlencoded通常是键值对。json发送JSON格式的数据application/json。# 模拟表单登录 login_data {username: your_name, password: your_pass} session requests.Session() # 使用会话保持Cookie login_response session.post(https://example.com/login, datalogin_data) # 调用JSON API api_data {query: Python, page: 1} api_response requests.post(https://api.example.com/search, jsonapi_data)实操心得遇到登录问题第一件事就是用浏览器的开发者工具F12查看Network标签。找到登录请求查看它的Request Headers特别是Content-Type和Form Data然后用requests原样模拟。Session对象至关重要它会自动管理Cookies让你在后续请求中保持登录状态。2.2 数据解析正则、BeautifulSoup与lxml的“兵器谱”拿到网页源码通常是HTML或接口返回的数据通常是JSON后下一步就是从中提取出有价值的信息。这里有三大主流工具各有适用场景。1. 正则表达式re库正则表达式功能强大适合处理结构简单的文本或提取有固定模式的字符串如邮箱、电话、特定格式的数字。但对于复杂的HTML文档正则表达式难以应对嵌套的标签结构容易写出脆弱且难以维护的代码。import re html div classprice199.00/div pattern r(\d\.?\d*) # 匹配价格数字 price re.search(pattern, html) if price: print(price.group(1)) # 输出199.002. BeautifulSoup这是我最推荐给新手的解析库。它像一把“瑞士军刀”能很好地处理“糟糕”的HTML比如标签不闭合API极其友好。它支持多种解析器最常用的是lxml需要额外安装速度快和html.parserPython内置速度慢但无需安装。from bs4 import BeautifulSoup html_doc html body h1商品列表/h1 ul classitems li classitemPython书 span classprice89元/span/li li classitem鼠标 span classprice129元/span/li /ul /body /html soup BeautifulSoup(html_doc, lxml) # 指定使用lxml解析器 # 查找第一个h1标签的文本 title soup.h1.text print(title) # 输出商品列表 # 查找所有class为‘item’的li标签 items soup.find_all(li, class_item) for item in items: name item.contents[0].strip() # 获取标签内的文本节点 price item.find(span, class_price).text print(f商品{name}, 价格{price})BeautifulSoup的find和find_all方法支持CSS选择器非常直观。select方法更是可以直接使用CSS选择器语法对于前端熟悉的开发者来说上手极快。3. lxml这是一个高性能的解析库底层是C语言实现速度远快于BeautifulSoup。它的XPath语法非常强大和精确适合处理大型文档或对性能要求极高的场景。但XPath语法有一定学习成本。from lxml import etree html ulliItem 1/liliItem 2/li/ul tree etree.HTML(html) # 使用XPath选取所有li标签的文本 items tree.xpath(//li/text()) print(items) # 输出[Item 1, Item 2]工具选型建议快速上手、处理中小型项目、HTML不规范首选BeautifulSoup。追求极致性能、处理海量数据、需要复杂节点定位选择lxml XPath。提取简单、有固定模式的文本片段可以用正则表达式作为补充。 在实际项目中我经常混合使用用requests获取用BeautifulSoup做初步清洗和定位对于复杂的嵌套数据再用XPath精确提取。2.3 数据存储从文本文件到数据库的演进提取出的数据需要持久化保存。选择哪种存储方式取决于数据量、结构化和后续使用需求。1. 文本文件CSV/JSON适合小规模、一次性的数据抓取。csv和json是Python标准库使用方便。import csv import json data [{name: 商品A, price: 100}, {name: 商品B, price: 200}] # 存储为CSV with open(products.csv, w, newline, encodingutf-8-sig) as f: # utf-8-sig解决Excel中文乱码 writer csv.DictWriter(f, fieldnames[name, price]) writer.writeheader() writer.writerows(data) # 存储为JSON with open(products.json, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) # ensure_asciiFalse确保中文正常显示2. 关系型数据库如SQLite/MySQL适合数据结构规整、需要复杂查询和关联分析的场景。SQLite无需安装服务器单个文件即数据库非常适合小型爬虫或原型开发。import sqlite3 conn sqlite3.connect(products.db) cursor conn.cursor() # 创建表 cursor.execute( CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL) ) # 插入数据 cursor.executemany(INSERT INTO products (name, price) VALUES (?, ?), [(商品A, 100), (商品B, 200)]) conn.commit() conn.close()3. 非关系型数据库如MongoDB适合数据结构多变、字段不固定例如爬取不同网站的商品信息字段可能不同的场景。MongoDB以文档形式存储类似JSON非常灵活。from pymongo import MongoClient client MongoClient(localhost, 27017) # 连接本地MongoDB db client[spider_db] # 选择数据库 collection db[products] # 选择集合类似表 product {name: 商品A, price: 100, tags: [电子, 数码]} # 插入数据 collection.insert_one(product) # 查询数据 for item in collection.find({price: {$gt: 50}}): print(item)存储策略心得增量爬取这是工业级爬虫的必备技能。避免重复爬取已更新的数据。通常的做法是在数据库表中增加一个update_time字段每次爬取时比较该字段。或者更常见的对每条数据的唯一标识如商品ID、文章URL的MD5值建立索引插入前先查询是否存在。去重大规模爬取时URL去重能节省大量资源。可以使用Python的set内存有限或者使用**布隆过滤器Bloom Filter**这种概率型数据结构用很小的内存实现海量URL的去重。scrapy-redis等分布式爬虫组件就内置了去重机制。异步存储当爬取速度很快时同步写入数据库或文件可能成为瓶颈。可以考虑使用异步IO库如asyncioaiomysql/motor或将数据先放入内存队列如queue.Queue由单独的消费者线程/进程负责写入实现生产-消费模式。3. 进阶实战应对反爬虫与提升爬虫效率如果你写的爬虫只能抓取一些没有防护的静态页面那还只是“玩具”。真实的网络环境充满挑战IP封锁、验证码、动态加载是家常便饭。同时效率也是必须考虑的问题。3.1 反爬虫策略与应对之道网站为了防止被过度抓取影响正常服务会设置各种反爬机制。作为开发者我们需要在遵守Robots协议和法律的前提下合理规避这些限制。1. 请求头Headers伪装这是最基本的防护。服务器会检查请求头中的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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://www.google.com/, # 模拟从谷歌跳转而来 Connection: keep-alive } response requests.get(https://www.example.com, headersheaders)注意User-Agent池是个好习惯。准备一个列表每次请求随机选择一个能有效降低被单一特征封锁的风险。2. IP代理与请求频率控制这是应对IP封锁的核心手段。当服务器检测到某个IP在短时间内请求过于频繁就会将其暂时或永久封禁。代理IP池从免费的或付费的代理服务商获取IP并在爬虫中使用。requests使用代理非常简单proxies { http: http://10.10.1.10:3128, https: http://10.10.1.10:1080, } requests.get(http://example.org, proxiesproxies)但免费代理质量极不稳定需要写检测程序定期验证其可用性和匿名度。生产环境建议使用付费的优质代理服务。请求延迟在请求之间加入随机等待时间模拟人类操作。import time import random for url in url_list: response requests.get(url, headersheaders) # 处理数据... time.sleep(random.uniform(1, 3)) # 随机等待1-3秒3. 处理Cookie与Session对于需要登录的网站Cookie是维持会话状态的关键。务必使用requests.Session()对象它会自动管理Cookies。session requests.Session() # 首先访问登录页可能获取初始Cookie或token login_page session.get(login_url) # 解析登录页获取必要的token如CSRF token # ... # 提交登录表单 login_data {username: ..., password: ..., csrf_token: ...} session.post(login_url, datalogin_data) # 登录后session会自动携带登录成功的Cookie访问其他页面 profile_page session.get(profile_url)4. 破解JavaScript渲染与动态内容现代网站大量使用Ajax或前端框架如React, Vue动态加载内容。直接抓取初始HTML是看不到这些数据的。解决方法有二分析网络请求打开浏览器开发者工具的Network面板过滤XHR/Fetch请求找到真正获取数据的API接口然后用requests直接调用这个接口。这是最高效的方法。使用无头浏览器当接口参数加密复杂或无法直接找到时就只能模拟浏览器执行JavaScript来渲染页面了。Selenium或Playwright是这类工具的代表。from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options webdriver.ChromeOptions() options.add_argument(--headless) # 无头模式不显示浏览器窗口 driver webdriver.Chrome(optionsoptions) driver.get(https://example.com/dynamic-page) # 等待某个动态加载的元素出现 element WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, dynamic-content)) ) # 获取渲染后的页面源码 html driver.page_source driver.quit() # 再用BeautifulSoup解析html踩坑实录无头浏览器虽然强大但资源消耗巨大内存、CPU速度慢。它应该是你最后的选择。优先尝试分析API接口。3.2 提升爬虫效率从同步到异步与分布式当需要抓取成千上万个页面时同步的、一次一个请求的方式会慢得无法忍受。1. 多线程/多进程Python的threading多线程和multiprocessing多进程模块可以并发执行任务。但由于GIL全局解释器锁的存在多线程在CPU密集型任务上提升有限但在I/O密集型如网络请求任务上效果显著。多进程则可以充分利用多核CPU。from concurrent.futures import ThreadPoolExecutor, as_completed import requests def fetch_url(url): try: resp requests.get(url, timeout5) return url, resp.status_code, len(resp.text) except Exception as e: return url, None, str(e) urls [http://example.com/1, http://example.com/2, ...] # 大量URL # 使用线程池例如10个线程 with ThreadPoolExecutor(max_workers10) as executor: future_to_url {executor.submit(fetch_url, url): url for url in urls} for future in as_completed(future_to_url): url future_to_url[future] try: data future.result() print(f{url}: {data}) except Exception as exc: print(f{url} generated an exception: {exc})2. 异步IOasyncio aiohttp这是处理高并发I/O的现代解决方案。与多线程相比异步IO在单线程内通过事件循环实现并发开销更小能轻松管理成千上万个并发连接。import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(): urls [http://example.com/1, http://example.com/2] async with aiohttp.ClientSession() as session: tasks [fetch(session, url) for url in urls] htmls await asyncio.gather(*tasks) # 并发执行所有任务 for html in htmls: print(len(html)) asyncio.run(main())性能对比心得对于成百上千的URLasyncio aiohttp的组合在性能上通常碾压多线程代码也更清晰。但异步编程模型有一定心智负担调试也相对复杂。3. 分布式爬虫当数据量达到亿级单机在带宽、存储和计算上都会成为瓶颈。这时就需要分布式爬虫。核心思想是将爬取任务分发到多台机器上执行。任务队列使用Redis作为中央任务队列。主节点负责生成待爬取的URL种子放入Redis队列。多个爬虫节点从队列中领取URL进行爬取并将解析出的新URL再放回队列。去重在Redis中使用Set或布隆过滤器实现全局URL去重。状态共享爬取进度、统计信息等可以存储在Redis中供所有节点查看。Scrapy框架配合scrapy-redis组件可以非常方便地搭建分布式爬虫系统。4. 工业级框架Scrapy深度解析对于中型及以上规模的爬虫项目使用框架是明智的选择。Scrapy是一个为爬取网站数据、提取结构性数据而编写的应用框架它封装了网络请求、数据解析、管道处理、中间件扩展等通用功能让你能专注于爬取规则的编写。4.1 Scrapy核心架构与工作流程Scrapy采用经典的“引擎-调度器-下载器-爬虫-管道”架构数据流清晰。引擎Engine整个框架的大脑控制数据流。调度器Scheduler接收引擎发来的请求将其入队并在引擎请求时返回。下载器Downloader负责下载网页内容并将结果返回给引擎。爬虫Spider你编写的主要逻辑所在。定义如何跟踪链接、如何解析页面。项目管道Item Pipeline处理爬虫提取出的Item数据。典型用途包括清洗数据、验证数据、去重、存储到数据库。下载器中间件Downloader Middleware位于引擎和下载器之间可以全局修改请求和响应。常用于设置代理、更换User-Agent、处理Cookie等。爬虫中间件Spider Middleware位于引擎和爬虫之间处理爬虫的输入响应和输出Items/Requests。创建一个Scrapy项目非常简单scrapy startproject myproject cd myproject scrapy genspider example example.com这会生成一个标准的项目结构其中spiders/目录下的example.py就是你的爬虫文件。4.2 编写一个健壮的Spider一个典型的Spider需要定义起始URL和parse回调方法。# spiders/example_spider.py import scrapy from myproject.items import ProductItem # 从items.py导入定义的数据结构 class ExampleSpider(scrapy.Spider): name example # 爬虫的唯一标识 allowed_domains [example.com] # 允许爬取的域名 start_urls [http://www.example.com/products] # 起始URL def parse(self, response): # 解析列表页提取商品详情页链接 product_links response.css(div.product a::attr(href)).getall() for link in product_links: # 构造绝对URL full_url response.urljoin(link) # 生成新的Request对象指定回调函数为parse_product yield scrapy.Request(full_url, callbackself.parse_product) # 处理分页查找“下一页”链接 next_page response.css(a.next-page::attr(href)).get() if next_page: yield response.follow(next_page, callbackself.parse) def parse_product(self, response): # 解析商品详情页提取数据并封装成Item item ProductItem() item[name] response.css(h1.product-title::text).get().strip() # 使用XPath或CSS选择器提取更复杂的数据 item[price] response.xpath(//span[classprice]/text()).get() item[description] .join(response.css(.description ::text).getall()).strip() # 提取其他字段... yield item # 将Item传递给Pipeline核心技巧使用response.follow相比scrapy.Requestresponse.follow能自动处理相对URL更简洁。选择器Scrapy内置了基于parsel库的CSS和XPath选择器性能很好。.get()获取第一个结果.getall()获取所有结果。Item定义在items.py中定义数据结构这有助于数据管理和Pipeline处理。日志使用self.logger记录信息便于调试和监控。4.3 配置与中间件定制你的爬虫行为Scrapy的强大之处在于其高度的可配置性。settings.py文件是控制中心。关键配置项# settings.py BOT_NAME myproject USER_AGENT Mozilla/5.0 ... # 设置默认User-Agent ROBOTSTXT_OBEY True # 是否遵守Robots协议生产环境建议为True CONCURRENT_REQUESTS 16 # 并发请求数根据目标网站和自身网络调整 DOWNLOAD_DELAY 0.5 # 下载延迟秒避免请求过快 COOKIES_ENABLED False # 是否启用Cookies某些网站需要开启 DEFAULT_REQUEST_HEADERS {...} # 默认请求头 ITEM_PIPELINES { myproject.pipelines.MyProjectPipeline: 300, # 数字代表优先级越小越先执行 } DOWNLOADER_MIDDLEWARES { myproject.middlewares.RandomUserAgentMiddleware: 543, # 自定义中间件 scrapy.downloadermiddlewares.useragent.UserAgentMiddleware: None, # 禁用默认中间件 }编写自定义中间件以随机User-Agent为例# middlewares.py import random from scrapy import signals class RandomUserAgentMiddleware: def __init__(self, user_agent_list): self.user_agents user_agent_list classmethod def from_crawler(cls, crawler): # 从settings中读取USER_AGENT_LIST配置 s crawler.settings ua_list s.get(USER_AGENT_LIST, []) return cls(ua_list) def process_request(self, request, spider): if self.user_agents: ua random.choice(self.user_agents) request.headers[User-Agent] ua # 不返回任何值表示继续处理该请求编写Item Pipeline进行数据清洗和存储# pipelines.py import pymongo class MongoPipeline: def __init__(self, mongo_uri, mongo_db): self.mongo_uri mongo_uri self.mongo_db mongo_db classmethod def from_crawler(cls, crawler): # 从settings中读取MongoDB配置 return cls( mongo_uricrawler.settings.get(MONGO_URI), mongo_dbcrawler.settings.get(MONGO_DATABASE, items) ) def open_spider(self, spider): # 爬虫启动时连接数据库 self.client pymongo.MongoClient(self.mongo_uri) self.db self.client[self.mongo_db] def close_spider(self, spider): # 爬虫关闭时断开连接 self.client.close() def process_item(self, item, spider): # 处理每一个Item这里插入MongoDB collection_name item.__class__.__name__ # 用Item类名作为集合名 self.db[collection_name].insert_one(dict(item)) return item # 必须返回item以便后续Pipeline处理4.4 部署与调度让爬虫自动化运行开发完成的爬虫需要部署到服务器上定期执行。Scrapyd是一个用于部署和运行Scrapy爬虫的应用服务器。安装Scrapydpip install scrapyd启动Scrapyd服务在服务器上运行scrapyd它会启动一个Web服务默认端口6800。部署项目使用scrapy-client或curl将你的Scrapy项目打包成egg文件部署到Scrapyd。定时调度结合crontabLinux或APScheduler等定时任务库定时向Scrapyd的API接口发送运行爬虫的请求。对于更复杂的分布式和监控需求可以使用Scrapy Cluster或Gerapy等开源管理平台。5. 法律、伦理与最佳实践避坑指南技术之外爬虫开发者必须紧绷法律和伦理这根弦。不当的爬取行为可能引发法律纠纷甚至导致公司面临巨额索赔。1. 严格遵守Robots协议Robots.txt是网站放在根目录下的一个文本文件用于告知爬虫哪些页面可以抓取哪些不可以。使用Scrapy时将ROBOTSTXT_OBEY设置为True是基本道德。手动爬取时也应先检查https://目标网站/robots.txt。例如淘宝的robots.txt部分内容如下User-agent: * Disallow: /这明确禁止了所有非合作爬虫。无视此协议强行爬取在法律和道义上都站不住脚。2. 尊重网站的服务条款Terms of Service很多网站的用户协议中明确禁止未经授权的自动化数据抓取。在爬取前务必阅读相关条款。3. 控制访问频率避免对目标网站造成压力这是最重要的实操准则。你的爬虫不应该影响网站的正常服务。在Scrapy中合理设置DOWNLOAD_DELAY、CONCURRENT_REQUESTS_PER_DOMAIN。添加随机延迟模拟人类浏览的随机性。监控目标网站的响应速度如果变慢应主动降低爬取频率。避免在对方服务器高峰时段如购物网站促销时进行高强度爬取。4. 识别并处理“反爬”的合理边界很多“反爬”措施如要求登录、验证码是网站保护自身数据和服务的正当权利。破解这些措施可能涉及法律风险。一个通行的原则是只为个人学习、研究目的且爬取的是公开、非敏感数据。如果用于商业目的或爬取用户隐私、版权内容务必寻求法律意见或与网站方合作。5. 数据使用与版权即使你合法获取了数据其使用也可能受到限制。直接复制网站内容发布可能侵犯版权。对数据进行聚合、分析、生成摘要或报告通常风险较低但最好注明数据来源。6. 常见技术性“坑”与解决方案连接超时与重试网络不稳定是常态。务必为请求设置超时并实现重试机制。Scrapy有内置的重试中间件。数据格式异常网页结构可能变动你的解析规则可能会失效。代码中要有充分的异常处理try...except并记录解析失败的页面以便后续排查和更新规则。编码问题网页编码千奇百怪GBK, GB2312, UTF-8等。requests和Scrapy通常能自动检测但有时需要手动指定response.encoding gbk。增量爬取与去重如前所述这是生产级爬虫的必备功能。设计数据表时就要考虑唯一键和更新时间戳。爬虫技术是一把双刃剑。用它来赋能研究、创造价值你会乐在其中用它来作恶或蛮干则会麻烦不断。我的经验是保持对技术的敬畏对规则的尊重你的爬虫之路才能走得又远又稳。最后再分享一个小心得对于重要的爬虫任务一定要写监控脚本定期检查爬取数量、成功率、数据质量并设置报警这样才能在出问题时第一时间响应。