1. 项目缘起与核心思路最近在做一个数据分析相关的个人项目需要一批特定领域的文本数据。公开的数据集要么不够新要么维度不符合我的需求。于是一个念头冒了出来为什么不自己动手从互联网这个最大的“数据矿场”里挖一挖呢当然这里的“你懂得的网站”并非指那些灰色或违规的站点而是泛指那些内容丰富、结构相对规整但数据获取不便的各类信息平台比如一些垂直领域的论坛、知识库、商品列表页等。我的目标很明确使用 Python 生态里久经考验的爬虫框架 Scrapy高效、稳定地抓取目标网站的结构化数据并存入本地数据库构建一个专属于我自己的、可随时查询和分析的数据仓库。选择 Scrapy 而非 Requests BeautifulSoup 的组合主要基于几个考量。首先Scrapy 是一个完整的、异步的爬虫框架它内置了请求调度、去重、管道处理等机制在抓取大规模、多层级页面时性能和健壮性远胜于手动编写的脚本。其次它的项目结构清晰Spider、Item、Pipeline、Middleware 各司其职非常适合将爬取、解析、清洗、存储的流程模块化代码可维护性极高。最后Scrapy 的扩展性很强可以方便地集成代理池、分布式爬取等高级功能为未来的需求升级留足了空间。这个项目的核心价值在于“自建”。它意味着数据获取的自主权、数据格式的定制化以及数据更新的灵活性。无论是用于市场分析、内容研究还是模型训练一个量身定制的数据库都是无价之宝。接下来我将详细拆解从零开始使用 Scrapy 构建一个完整数据爬取与存储系统的全过程包括环境搭建、爬虫编写、数据清洗、入库存储以及那些只有踩过坑才知道的注意事项。2. 环境准备与项目初始化工欲善其事必先利其器。第一步是搭建一个干净、可复现的开发环境。我强烈建议使用虚拟环境来管理项目依赖这能避免不同项目间的包版本冲突。2.1 创建虚拟环境与安装 Scrapy我习惯使用conda或venv。这里以venv为例在项目目录下执行# 创建虚拟环境命名为 scrapy_env python -m venv scrapy_env # 激活虚拟环境 # Windows: scrapy_env\Scripts\activate # Linux/Mac: source scrapy_env/bin/activate环境激活后命令行提示符前会出现(scrapy_env)标识。接下来安装 Scrapy 框架pip install scrapyScrapy 会一并安装其核心依赖如 Twisted异步网络引擎、lxml解析库等。为了后续的数据处理我们通常还会安装pandas用于初步的数据探查和清洗以及数据库驱动例如操作 SQLite 或 MySQL 的库。pip install pandas pymysql # 如果使用 SQLitePython 已内置支持无需额外安装驱动。2.2 创建 Scrapy 项目Scrapy 提供了一套标准的项目模板。在合适的目录下运行以下命令来创建一个名为my_data_crawler的项目scrapy startproject my_data_crawler这个命令会生成一个标准的项目结构my_data_crawler/ scrapy.cfg my_data_crawler/ __init__.py items.py middlewares.py pipelines.py settings.py spiders/ __init__.pyscrapy.cfg: 项目部署配置文件。items.py: 定义要爬取的数据结构类似于模型或表单。pipelines.py: 定义数据清洗、验证和存储的流水线。settings.py: 项目的全局设置如并发数、下载延迟、User-Agent 等。spiders/: 存放爬虫文件的目录每个爬虫文件对应一个特定的抓取任务。2.3 关键配置调整在编写爬虫前需要先调整settings.py中的一些关键配置这能有效提高爬虫的友好度和稳定性。# my_data_crawler/settings.py # 1. 遵守 Robots 协议建议设为 True 以示尊重但可根据目标站点调整 ROBOTSTXT_OBEY False # 对于某些限制严格的站点可能需要设置为False但请务必谨慎评估法律和道德风险。 # 2. 设置并发请求数太大会对目标服务器造成压力可能被屏蔽。 CONCURRENT_REQUESTS 16 # 3. 下载延迟两次请求之间的最小时间间隔秒降低请求频率。 DOWNLOAD_DELAY 0.5 # 4. 配置 User-Agent模拟真实浏览器这是最基本的反反爬措施。 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 # 5. 启用或配置 Item Pipelines用于数据存储。 ITEM_PIPELINES { my_data_crawler.pipelines.MyDataCrawlerPipeline: 300, } # 数字 300 代表管道优先级数字越小优先级越高。 # 6. 启用中间件如需要更换IP代理、处理Cookie等 # DOWNLOADER_MIDDLEWARES { # my_data_crawler.middlewares.MyProxyMiddleware: 543, # }注意ROBOTSTXT_OBEY设置为False意味着爬虫将忽略目标网站的robots.txt文件。这仅应在你完全确认该行为符合目标网站的服务条款且不会对其造成不当负担时使用。始终将网络礼仪和法律合规性放在首位。3. 定义数据模型与编写核心爬虫一切准备就绪现在进入核心环节定义我们要抓取什么以及如何去抓取。3.1 在items.py中定义数据结构假设我们要从一个电影信息网站爬取电影数据我们需要的数据项可能包括标题、评分、导演、主演、简介、上映日期等。在items.py中我们使用 Scrapy 的Item类来定义这个结构。# my_data_crawler/items.py import scrapy class MovieItem(scrapy.Item): # define the fields for your item here like: title scrapy.Field() # 电影标题 rating scrapy.Field() # 评分 directors scrapy.Field() # 导演可能是列表 actors scrapy.Field() # 主演可能是列表 summary scrapy.Field() # 简介 release_date scrapy.Field() # 上映日期 url scrapy.Field() # 详情页URL用于唯一标识和溯源 crawl_time scrapy.Field() # 爬取时间scrapy.Field()对象可以存储任何类型的数据。定义清晰的 Item 就像为数据库设计表结构是后续数据处理的基础。3.2 编写第一个 SpiderSpider 是定义爬取行为和解析规则的类。我们在spiders目录下创建一个新文件比如movie_spider.py。一个典型的 Spider 需要继承scrapy.Spider并至少定义name爬虫唯一标识、start_urls起始URL列表和parse方法用于解析响应。# my_data_crawler/spiders/movie_spider.py import scrapy from my_data_crawler.items import MovieItem from urllib.parse import urljoin # 用于拼接相对URL class MovieSpider(scrapy.Spider): name movie # 爬虫名运行爬虫时使用scrapy crawl movie allowed_domains [example-movie-site.com] # 限制爬取的域名 start_urls [https://example-movie-site.com/list?page1] # 起始页面列表页 def parse(self, response): 解析电影列表页提取每个电影详情页的链接并跟进。 # 使用XPath或CSS选择器定位每个电影条目 movie_links response.css(div.movie-item a::attr(href)).getall() for link in movie_links: # 构建完整的详情页URL detail_url urljoin(response.url, link) # 生成一个到详情页的请求并指定回调函数为 parse_detail yield scrapy.Request(detail_url, callbackself.parse_detail) # 处理翻页查找“下一页”的链接 next_page response.css(a.next-page::attr(href)).get() if next_page: next_page_url urljoin(response.url, next_page) # 将下一页的请求返回给引擎继续爬取 yield scrapy.Request(next_page_url, callbackself.parse) def parse_detail(self, response): 解析电影详情页提取具体信息并填充到Item中。 item MovieItem() # 使用CSS选择器提取数据并做好异常处理 item[title] response.css(h1.title::text).get(default).strip() # .get() 方法在找不到元素时返回None我们可以提供默认值。 item[rating] response.css(span.rating::text).get() # 有些字段可能是多个元素使用 .getall() 获取列表 item[directors] response.css(div.directors a::text).getall() item[actors] response.css(div.actors a::text).getall() item[summary] .join(response.css(div.summary p::text).getall()).strip() item[release_date] response.css(meta[propertyrelease_date]::attr(content)).get() item[url] response.url # 记录当前页面的URL item[crawl_time] datetime.datetime.now().isoformat() # 记录爬取时间 # 将填充好的Item返回交给Pipeline处理 yield item实操心得一选择器的选择与容错XPath vs CSSCSS 选择器通常更简洁易读适合大多数静态页面。XPath 功能更强大可以基于文本内容、位置等复杂条件定位在结构不规整的页面上更有优势。我通常先用 CSS搞不定再换 XPath。健壮性网页结构可能变动选择器可能失效。务必使用.get()返回第一个匹配或None和.getall()返回列表方法并为关键字段设置合理的默认值如default。避免直接使用extract_first()旧版方法或索引[0]这可能导致程序崩溃。数据清洗前置在解析阶段就进行简单的清洗如.strip()去除首尾空格能减轻后续 Pipeline 的压力。4. 数据处理与入库管道Spider 产出的是原始的Item对象下一步需要在Pipeline中进行清洗、验证和持久化存储。4.1 编写数据清洗 Pipeline我们在pipelines.py中定义一个MoviePipeline类。Scrapy 的process_item方法会对每一个Item调用。# my_data_crawler/pipelines.py import pymysql from itemadapter import ItemAdapter from scrapy.exceptions import DropItem import logging class MoviePipeline: def __init__(self): # 可以在这里初始化数据库连接但更推荐在 open_spider 中初始化 self.seen_titles set() # 用于内存中去重 def open_spider(self, spider): 当爬虫开启时调用用于建立数据库连接等资源初始化。 # 示例连接MySQL数据库 self.connection pymysql.connect( hostlocalhost, useryour_username, passwordyour_password, databasemovie_db, charsetutf8mb4 # 重要支持存储Emoji等四字节字符 ) self.cursor self.connection.cursor() # 创建表如果不存在 create_table_sql CREATE TABLE IF NOT EXISTS movies ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, rating FLOAT, directors TEXT, actors TEXT, summary TEXT, release_date DATE, url VARCHAR(500) UNIQUE, -- 设置唯一约束避免重复 crawl_time DATETIME ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; self.cursor.execute(create_table_sql) self.connection.commit() logging.info(Database connection established and table checked.) def close_spider(self, spider): 当爬虫关闭时调用用于关闭数据库连接等资源清理。 self.cursor.close() self.connection.close() logging.info(Database connection closed.) def process_item(self, item, spider): 处理每个Item的核心方法。 adapter ItemAdapter(item) # 1. 数据清洗 # 检查必填字段 if not adapter.get(title) or not adapter.get(url): raise DropItem(fMissing title or url in {item}) # 标题去重内存级适用于单机爬虫 title adapter[title] if title in self.seen_titles: raise DropItem(fDuplicate title found: {title}) self.seen_titles.add(title) # 处理评分确保是数字或转换为None rating adapter.get(rating) if rating: try: adapter[rating] float(rating) except ValueError: adapter[rating] None else: adapter[rating] None # 处理列表字段存储为逗号分隔的字符串或JSON字符串 if adapter.get(directors): adapter[directors] , .join([d.strip() for d in adapter[directors] if d.strip()]) if adapter.get(actors): adapter[actors] , .join([a.strip() for a in adapter[actors] if a.strip()]) # 2. 数据入库 try: insert_sql INSERT INTO movies (title, rating, directors, actors, summary, release_date, url, crawl_time) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE ratingVALUES(rating), summaryVALUES(summary), crawl_timeVALUES(crawl_time) # 使用 ON DUPLICATE KEY UPDATE 实现“有则更新无则插入” self.cursor.execute(insert_sql, ( adapter[title], adapter[rating], adapter.get(directors), adapter.get(actors), adapter.get(summary), adapter.get(release_date), adapter[url], adapter.get(crawl_time) )) self.connection.commit() logging.debug(fItem saved to database: {adapter[title]}) except pymysql.Error as e: logging.error(fError inserting item {adapter[title]}: {e}) self.connection.rollback() # 发生错误时回滚 # 可以选择不抛出DropItem而是记录错误让Item继续进入后续Pipeline如果有的话 # raise DropItem(fDatabase error for {adapter[title]}: {e}) return item # 必须返回item以便被后续的Pipeline处理如果定义了多个实操心得二数据库操作的最佳实践连接管理在open_spider和close_spider中管理数据库连接的生命周期避免为每个Item都建立/关闭连接这是巨大的性能开销。异常处理数据库操作必须用try...except包裹并做好错误日志记录。发生错误时进行回滚 (rollback)保证数据一致性。字符编码对于中文互联网内容数据库和连接字符集务必使用utf8mb4而非utf8以完整支持所有Unicode字符包括Emoji。去重策略除了在内存中用集合去重更可靠的方式是利用数据库的唯一约束如UNIQUE索引。ON DUPLICATE KEY UPDATE语句能优雅地处理重复插入实现数据更新。对于大规模爬虫可以考虑使用 Scrapy 内置的RFPDupeFilter或布隆过滤器进行请求级别的去重。4.2 启用并配置 Pipeline确保在settings.py中已经启用了这个 Pipeline并设置了合适的优先级。ITEM_PIPELINES { my_data_crawler.pipelines.MoviePipeline: 300, }5. 应对反爬策略与高级技巧真实的网站通常会有反爬机制。一个鲁棒的爬虫必须能妥善处理这些问题。5.1 基础反反爬措施User-Agent 轮换在settings.py中配置一个 User-Agent 列表并通过下载中间件随机选择。# settings.py USER_AGENT_LIST [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ..., # ... 更多浏览器标识 ]然后在middlewares.py中编写一个中间件import random from my_data_crawler.settings import USER_AGENT_LIST class RandomUserAgentMiddleware: def process_request(self, request, spider): ua random.choice(USER_AGENT_LIST) request.headers[User-Agent] ua并在settings.py中启用它DOWNLOADER_MIDDLEWARES {my_data_crawler.middlewares.RandomUserAgentMiddleware: 543}。请求延迟与并发控制合理设置DOWNLOAD_DELAY和CONCURRENT_REQUESTS模拟人类浏览速度。AUTOTHROTTLE_ENABLED True可以启用自动限速扩展它能根据服务器响应情况动态调整请求速率。处理 Cookies 与 Session对于需要登录的网站可以使用scrapy.FormRequest模拟登录Scrapy 会自动管理 Session Cookie。在 Spider 的start_requests方法中可以先发送登录请求。5.2 处理动态加载内容许多现代网站使用 JavaScript 动态加载数据。Scrapy 本身只能抓取初始 HTML 响应。对此有几种解决方案分析 API 请求这是最推荐的方法。使用浏览器的开发者工具F12切换到“网络”(Network) 标签过滤 XHR/Fetch 请求找到数据真正的来源通常是 JSON 接口。然后让 Scrapy 直接去请求这些 API 接口效率远高于渲染页面。集成 Splash 或 Selenium当数据必须通过执行 JS 才能生成时可以考虑集成 Splash一个带 HTTP API 的轻量级浏览器渲染服务或直接使用scrapy-selenium中间件。但这会显著增加资源消耗和爬取时间应作为最后手段。示例使用 scrapy-selenium (简化)# middlewares.py from scrapy_selenium import SeleniumRequest class MovieSpider(scrapy.Spider): def start_requests(self): url https://example-js-site.com yield SeleniumRequest(urlurl, callbackself.parse, wait_time3) # 等待JS执行 def parse(self, response): # 此时 response 包含渲染后的 HTML # 注意response 是 HtmlResponse 的子类你可以从中提取数据 data response.css(div.js-rendered-content::text).get()实操心得三动态内容抓取取舍优先寻找隐藏的 API。这不仅能绕过反爬还能获得结构更清晰、体积更小的数据。如果必须用浏览器渲染Splash 比 Selenium 更轻量更适合集成到 Scrapy 的异步架构中。Selenium 更适合小规模、交互复杂的场景。无论哪种方式都要大幅增加请求延迟并做好请求失败的重试机制。6. 运行、监控与数据导出6.1 运行爬虫在项目根目录有scrapy.cfg的目录下运行以下命令scrapy crawl movie -o movies.json -s LOG_LEVELINFOcrawl movie: 运行名为movie的爬虫。-o movies.json: 将爬取的 Item 同时输出到movies.json文件作为备份或快速检查。-s LOG_LEVELINFO: 设置日志级别为 INFO减少调试信息输出更清晰。调试时可使用DEBUG。6.2 日志监控与错误排查Scrapy 的日志非常详细。关注以下几种日志DEBUG: 详细的请求、响应信息用于调试选择器、解析逻辑。INFO: 爬虫启动、关闭、Item 统计信息。WARNING: 非关键性错误如忽略的重复请求。ERROR: 需要关注的错误如 HTTP 错误码403 429 500、数据库插入失败。CRITICAL: 严重错误通常会导致爬虫停止。可以在settings.py中配置日志输出到文件LOG_FILE crawl.log LOG_LEVEL INFO6.3 数据导出与后续处理虽然数据已存入数据库但有时我们需要导出进行分析。可以使用简单的 Python 脚本连接数据库并导出为 CSV 或 Excel。import pandas as pd import pymysql # 连接数据库 connection pymysql.connect(hostlocalhost, userroot, password, databasemovie_db) # 读取数据到 DataFrame df pd.read_sql(SELECT * FROM movies, connection) connection.close() # 导出为 CSV df.to_csv(movies_export.csv, indexFalse, encodingutf-8-sig) # utf-8-sig 解决Excel打开中文乱码 # 导出为 Excel df.to_excel(movies_export.xlsx, indexFalse)7. 常见问题与排查技巧实录在实际爬取过程中你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方法。问题一返回 HTTP 403 Forbidden 错误。可能原因1请求头被识别为爬虫。排查检查请求头特别是User-Agent。是否使用了 Scrapy 默认的User-Agent是否缺少Referer、Accept-Language等常见头信息解决在settings.py中设置一个常见的浏览器User-Agent并通过中间件添加完整的请求头字典。可能原因2IP 地址被暂时封禁。排查用浏览器直接访问同一URL是否正常如果浏览器正常而爬虫不行很可能是IP被限。解决增加DOWNLOAD_DELAY如从0.5秒增加到2-5秒。考虑使用付费或免费的代理IP池并在中间件中实现IP轮换。对于非常重要的任务可以考虑使用云函数等分布式、IP不固定的服务来运行爬虫关键部分。问题二解析不到数据选择器返回空列表。可能原因1网页结构发生变化。排查用浏览器查看页面源代码CtrlU确认你使用的CSS或XPath路径是否还能定位到目标元素。解决更新选择器表达式。使用更通用的、容错性更强的路径避免依赖过于具体和易变的id或class。可能原因2数据是动态加载的。排查在浏览器中禁用JavaScript刷新页面看看数据是否还在。或者查看“网络”选项卡寻找包含数据的XHR请求。解决转为请求API接口或集成Splash/Selenium。问题三数据库插入速度慢。可能原因每条数据都单独提交commit。解决使用批量插入。可以在 Pipeline 中积累一定数量的 Item比如100个然后一次性执行批量插入操作最后再提交事务。这能极大减少数据库的I/O次数。class MoviePipeline: def __init__(self): self.items_buffer [] # 缓冲区 def process_item(self, item, spider): self.items_buffer.append(item) if len(self.items_buffer) 100: self._flush_buffer() return item def _flush_buffer(self): if not self.items_buffer: return # 构建批量插入SQL sql INSERT INTO ... VALUES (%s, %s, ...) values [(i[title], i[rating], ...) for i in self.items_buffer] self.cursor.executemany(sql, values) # 使用 executemany self.connection.commit() self.items_buffer.clear() def close_spider(self, spider): self._flush_buffer() # 爬虫关闭时清空缓冲区 # ... 关闭连接问题四爬虫运行一段时间后意外停止或内存飙升。可能原因1未处理的异常导致爬虫崩溃。解决在 Spider 的解析函数和 Pipeline 的process_item中加强异常捕获和日志记录。使用try...except包裹可能出错的代码块记录错误但不要轻易让整个爬虫停止除非是致命错误。Scrapy 自身有重试机制可以在settings.py中配置RETRY_TIMES。可能原因2内存泄漏。排查是否在全局变量或类属性中不断追加数据而没有清理例如在 Pipeline 中去重用的集合seen_titles是否会无限增长解决对于需要长期运行的爬虫应将去重状态持久化到数据库或 Redis 中而不是内存。定期清理不必要的缓存。构建一个健壮、高效的数据爬取系统远不止写对选择器那么简单。它涉及网络请求的调度、反爬策略的对抗、数据清洗的严谨以及资源管理的优化。每一次调试和解决问题的过程都是对目标网站结构、网络协议和数据流理解的深化。当你最终看到成千上万条规整的数据安静地躺在自己的数据库里随时等待被分析和挖掘时那种满足感是无可替代的。记住能力越大责任越大在享受技术带来的便利时务必遵守法律法规和网站的使用条款合理控制爬取频率做一个有道德的“数据矿工”。