数据存储(MySQL与MongoDB)

网络爬虫专题 · 掌握爬虫数据的数据库存储

专题:Python网络爬虫系统学习

关键词:Python, 网络爬虫, MySQL, MongoDB, PyMySQL, PyMongo, Redis, SQLAlchemy, 数据存储

一、关系型数据库MySQL

MySQL是最流行的开源关系型数据库管理系统,在爬虫项目中广泛用于存储结构化数据。Python通过PyMySQL等驱动可以方便地操作MySQL数据库,实现数据的持久化存储。

1.1 MySQL安装与配置

在Windows系统中,可以从MySQL官网下载MSI安装包进行安装;在Linux系统中,使用包管理器安装:Ubuntu/Debian执行 sudo apt install mysql-server,CentOS执行 sudo yum install mysql-server。安装完成后需启动MySQL服务并设置root密码,创建专用的数据库和用户用于爬虫项目。

# 安装PyMySQL pip install pymysql # 创建数据库(MySQL命令行) CREATE DATABASE crawler_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'crawler'@'localhost' IDENTIFIED BY 'password'; GRANT ALL PRIVILEGES ON crawler_db.* TO 'crawler'@'localhost'; FLUSH PRIVILEGES;

1.2 PyMySQL连接MySQL

PyMySQL是一个纯Python实现的MySQL客户端库,使用前需要先建立连接。连接时需要指定主机地址、端口、用户名、密码、数据库名和字符集等参数。建立连接后通过cursor对象执行SQL语句。

import pymysql # 建立数据库连接 connection = pymysql.connect( host='localhost', port=3306, user='crawler', password='password', database='crawler_db', charset='utf8mb4' ) # 创建游标 cursor = connection.cursor()

1.3 创建数据库和数据表

在爬虫项目中,需要根据爬取数据的结构提前设计数据表。以爬取新闻文章为例,通常需要包含标题、URL、来源、发布时间、正文内容、分类等字段。设计表结构时要为URL字段添加UNIQUE约束以确保数据唯一性,避免重复入库。

CREATE TABLE articles ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(500) NOT NULL, url VARCHAR(1000) NOT NULL UNIQUE, source VARCHAR(200), publish_time DATETIME, content TEXT, category VARCHAR(100), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

1.4 插入数据(INSERT INTO)

使用INSERT INTO语句将爬取到的数据写入数据库。为防止SQL注入攻击,推荐使用参数化查询(占位符方式),而不是直接拼接SQL字符串。PyMySQL使用 %s 作为占位符。

# 参数化插入(推荐,防SQL注入) sql = "INSERT INTO articles (title, url, source, content) VALUES (%s, %s, %s, %s)" data = (title, url, source, content) cursor.execute(sql, data) connection.commit()

1.5 批量插入(executemany)

当爬虫批量抓取多条数据时,逐条插入效率极低。使用 executemany 方法可以一次性批量插入多条记录,大幅提升写入性能。批量插入时如果某条数据违反唯一约束导致失败,可以结合INSERT IGNORE跳过错误。

# 批量插入多条数据 sql = "INSERT IGNORE INTO articles (title, url, source, content) VALUES (%s, %s, %s, %s)" data_list = [ (title1, url1, source1, content1), (title2, url2, source2, content2), # ... 更多数据 ] cursor.executemany(sql, data_list) connection.commit()

1.6 查询数据(SELECT)

爬虫系统不仅需要写入数据,有时也需要查询已存储的数据,例如检查URL是否已经爬取过。PyMySQL支持多种查询方式:fetchone获取单条、fetchall获取全部、fetchmany获取指定条数。

# 查询数据 sql = "SELECT * FROM articles WHERE category = %s LIMIT 10" cursor.execute(sql, ('科技',)) results = cursor.fetchall() for row in results: print(row[1], row[2]) # title, url

1.7 更新与删除

当爬取的内容需要更新或清理时,使用UPDATE和DELETE语句。更新操作通常需要配合WHERE条件精确定位要修改的记录。在实际爬虫中,更新操作常用于更新已有记录的状态(如更新抓取时间、修正内容等)。

# 更新数据 sql = "UPDATE articles SET content = %s WHERE url = %s" cursor.execute(sql, (new_content, url)) connection.commit() print(f"影响行数: {cursor.rowcount}") # 删除数据 sql = "DELETE FROM articles WHERE created_at < %s" cursor.execute(sql, (cutoff_date,)) connection.commit()

1.8 连接池使用(DBUtils)

在爬虫高频访问数据库的场景下,频繁创建和销毁数据库连接会带来较大的性能开销。连接池技术可以维护一组可复用的数据库连接,有效减少连接创建和销毁的开销。DBUtils是Python中常用的连接池实现。

from dbutils.pooled_db import PooledDB import pymysql # 创建连接池 pool = PooledDB( creator=pymysql, maxconnections=10, mincached=2, host='localhost', port=3306, user='crawler', password='password', database='crawler_db', charset='utf8mb4' ) # 从连接池获取连接 conn = pool.connection() cursor = conn.cursor() # ... 执行操作 cursor.close() conn.close() # 归还到连接池,非真正关闭

1.9 爬虫数据去重(UNIQUE约束、INSERT IGNORE)

爬虫运行过程中经常会重复抓取到相同的数据,去重是爬虫数据存储的关键环节。利用MySQL的UNIQUE约束配合INSERT IGNORE语句,可以在数据库层面实现高效去重:当插入的数据违反了唯一约束时,INSERT IGNORE会静默跳过该条数据而不会报错,从而保证数据表中不会出现重复记录。另外也可以使用 REPLACE INTOON DUPLICATE KEY UPDATE 实现更灵活的去重更新策略。

# 方法一:INSERT IGNORE(跳过重复) sql = "INSERT IGNORE INTO articles (title, url, source, content) VALUES (%s, %s, %s, %s)" # 方法二:ON DUPLICATE KEY UPDATE(重复时更新) sql = """INSERT INTO articles (title, url, source, content) VALUES (%s, %s, %s, %s) ON DUPLICATE KEY UPDATE content=VALUES(content), source=VALUES(source)"""

二、ORM框架SQLAlchemy

直接使用SQL语句操作数据库虽然灵活,但在大型项目中代码会变得难以维护。ORM(Object-Relational Mapping)框架将数据库表映射为Python类,使开发者可以用面向对象的方式操作数据库,大大提高了开发效率和代码可读性。

2.1 SQLAlchemy核心概念

SQLAlchemy是Python生态中最强大的ORM框架之一。其核心概念包括Engine(引擎,负责与数据库通信)、Session(会话,管理数据库操作的事务单元)、Model(模型,映射数据库表的Python类)和Query(查询器,构建查询语句)。通过声明式映射(Declarative Mapping),开发者可以用类定义的方式描述数据表结构。

2.2 创建引擎和会话

引擎是SQLAlchemy的入口点,通过 create_engine 函数创建并绑定数据库URL。会话(Session)是操作数据库的工作单元,通常使用 sessionmaker 工厂来创建会话类。

from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker # 创建引擎 engine = create_engine( 'mysql+pymysql://crawler:password@localhost:3306/crawler_db?charset=utf8mb4', echo=True, # 打印SQL日志 pool_size=5, # 连接池大小 max_overflow=10 # 最大溢出连接数 ) # 创建会话工厂 SessionLocal = sessionmaker(bind=engine)

2.3 定义模型(declarative_base)

通过declarative_base创建基类,然后定义继承该基类的Python类来映射数据库表。类属性对应表字段,通过Column类型定义字段的数据类型和约束。这种声明式方式让表结构与代码逻辑紧密耦合,便于维护。

from sqlalchemy import Column, Integer, String, Text, DateTime from sqlalchemy.ext.declarative import declarative_base from datetime import datetime Base = declarative_base() class Article(Base): __tablename__ = 'articles' id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(500), nullable=False) url = Column(String(1000), unique=True, nullable=False) source = Column(String(200)) content = Column(Text) category = Column(String(100)) created_at = Column(DateTime, default=datetime.now) # 创建所有表 Base.metadata.create_all(engine)

2.4 CRUD操作

使用SQLAlchemy执行增删改查操作非常直观。新增记录只需创建模型实例并添加到会话;查询使用Session.query方法结合过滤条件;更新直接修改对象属性后提交;删除使用Session.delete方法。

# 创建会话 session = SessionLocal() # 新增 article = Article(title='Python爬虫教程', url='https://example.com/1', source='示例站') session.add(article) session.commit() # 查询 articles = session.query(Article).filter(Article.category == '科技').limit(10).all() # 更新 article = session.query(Article).filter_by(url='https://example.com/1').first() article.content = '更新后的内容' session.commit() # 删除 old_articles = session.query(Article).filter(Article.created_at < cutoff_date).all() for a in old_articles: session.delete(a) session.commit() session.close()

2.5 爬虫数据存储的ORM实现

在实际爬虫项目中,将ORM与爬虫框架(如Scrapy)结合,可以实现优雅的数据持久化。在Scrapy中,通常在pipelines模块中使用SQLAlchemy将爬取到的Item批量写入数据库。ORM的优势在于:当数据表结构发生变化时,只需修改模型类定义,而无需修改大量的SQL语句;同时ORM提供了更高级的查询API,支持关联查询、聚合函数等复杂操作,极大地简化了爬虫数据管理代码。

三、MongoDB非关系型数据库

MongoDB是最流行的NoSQL文档数据库之一,采用BSON(类JSON)格式存储数据,天然适合存储爬虫抓取的半结构化或非结构化数据。在爬虫场景中,不同网站返回的JSON数据结构往往不一致,MongoDB的Schema-less特性使其成为爬虫数据存储的理想选择。

3.1 MongoDB特点

3.2 安装与启动

从MongoDB官网下载Community Server版本安装。在Linux系统中可通过包管理器安装。安装完成后启动MongoDB服务,默认端口为27017。Python通过PyMongo驱动连接MongoDB。

# 安装PyMongo pip install pymongo

3.3 PyMongo连接MongoDB

使用MongoClient建立连接,可以指定连接字符串包含认证信息。默认连接本地MongoDB实例,也可连接远程服务器或MongoDB Atlas云服务。

from pymongo import MongoClient # 连接本地MongoDB client = MongoClient('mongodb://localhost:27017/') # 带认证的连接 client = MongoClient('mongodb://crawler:password@localhost:27017/crawler_db')

3.4 数据库、集合、文档

MongoDB中,数据库(Database)包含集合(Collection),集合包含文档(Document)。无需显式创建,第一次插入数据时MongoDB会自动创建对应的数据库和集合。

# 选择数据库(自动创建) db = client.crawler_db # 选择集合(自动创建) collection = db.articles # 一个文档示例(直接使用Python字典) article_doc = { 'title': 'MongoDB爬虫存储实践', 'url': 'https://example.com/mongodb', 'source': '技术博客', 'content': '本文介绍MongoDB在爬虫中的应用...', 'tags': ['MongoDB', '爬虫', 'Python'], 'crawl_time': '2026-05-05 23:50:00' }

3.5 插入数据(insert_one、insert_many)

MongoDB支持单条插入和批量插入。插入时无需预先定义字段,直接传入字典即可。每条文档自动生成一个唯一的 _id 字段作为主键。批量插入时如果某条文档的 _id 重复会报错,可以设置 ordered=False 跳过错误继续插入后续文档。

# 插入单条 result = collection.insert_one(article_doc) print(result.inserted_id) # 批量插入(ordered=False可跳过重复_id的错误) articles_list = [doc1, doc2, doc3] result = collection.insert_many(articles_list, ordered=False) print(result.inserted_ids)

3.6 查询数据(find、find_one、查询条件)

MongoDB提供丰富的查询语法。find_one返回匹配的第一条文档,find返回所有匹配文档的游标。查询条件使用Python字典描述,支持比较运算符($gt、$lt、$in)、逻辑运算符($and、$or、$not)、正则匹配($regex)等高级查询。

# 查询单条 doc = collection.find_one({'source': '技术博客'}) # 查询多条 cursor = collection.find( {'tags': {'$in': ['Python', '爬虫']}}, {'title': 1, 'url': 1} # 投影,只返回指定字段 ).limit(10).sort('crawl_time', -1) # 按时间倒序 for doc in cursor: print(doc['title'], doc['url']) # 复杂查询条件 query = { 'source': '技术博客', 'crawl_time': {'$gte': '2026-01-01'}, '$or': [ {'category': 'Python'}, {'category': '数据库'} ] } results = collection.find(query).count()

3.7 更新数据(update_one、update_many)

MongoDB提供原子性的更新操作。update_one更新匹配的第一条文档,update_many更新所有匹配的文档。更新操作使用 $set、$unset、$inc 等更新运算符,支持对文档的特定字段进行修改。

# 更新单条 collection.update_one( {'url': 'https://example.com/old'}, {'$set': {'content': '更新后的内容', 'updated_at': '2026-05-05'}} ) # 批量更新 collection.update_many( {'source': '过时站点'}, {'$set': {'status': 'deprecated'}} ) # 自增字段($inc) collection.update_one( {'_id': article_id}, {'$inc': {'view_count': 1}} )

3.8 删除数据

MongoDB支持单条删除和批量删除。delete_one删除匹配的第一条文档,delete_many删除所有匹配的文档。删除操作不可恢复,建议在删除前确认条件准确无误。

# 删除单条 collection.delete_one({'url': 'https://example.com/delete'}) # 批量删除 collection.delete_many({'status': 'deprecated'}) # 删除整个集合 collection.drop()

3.9 索引创建(ensure_index/create_index)

在MongoDB中,合理的索引对于查询性能至关重要。特别是在爬虫场景下,经常需要根据URL查询文档判断是否已爬取,因此为URL字段创建唯一索引可以同时提升查询性能和数据唯一性保障。

# 创建唯一索引(去重) collection.create_index('url', unique=True) # 创建复合索引 collection.create_index([('source', 1), ('crawl_time', -1)]) # 创建TTL索引(自动过期删除) collection.create_index('crawl_time', expireAfterSeconds=604800) # 7天后自动删除

四、MySQL vs MongoDB选择

在实际爬虫项目中,选择合适的数据库直接影响开发效率和运行稳定性。以下从几个关键维度对比MySQL和MongoDB,帮助开发者做出合理的技术选型。

对比维度MySQLMongoDB
数据模型结构化(表、行、列),需预定义Schema文档型(JSON/BSON),Schema-less灵活
事务支持完整ACID事务支持支持多文档事务(4.0+),性能开销较大
扩展性垂直扩展为主,读写分离,分库分表复杂原生支持水平扩展(分片),自动负载均衡
查询能力SQL标准查询,JOIN关联查询强大类JSON查询语法,聚合管道灵活
数据一致性强一致性最终一致性(可配置)
适合场景数据结构固定、关系复杂、需要事务数据结构多变、文档类数据、快速迭代

爬虫数据存储选型建议

根据爬虫项目的不同需求,给出以下选型建议:

五、Redis在爬虫中的应用

Redis是一种基于内存的高性能键值数据库,在爬虫系统中扮演着不可或缺的角色。虽然Redis不用于持久化存储爬取数据,但它在爬虫调度、去重、队列管理等环节发挥着关键作用。

5.1 Redis连接

Python通过redis-py库连接Redis服务器。连接时可以指定主机、端口、密码和数据库编号(Redis默认支持16个数据库,编号0-15)。

import redis r = redis.Redis( host='localhost', port=6379, password='password', db=0, decode_responses=True # 自动解码为字符串 ) # 测试连接 print(r.ping()) # True

5.2 数据结构使用(String、Set、List、Hash)

Redis提供了丰富的数据结构,每种结构在爬虫中都有典型应用场景:

5.3 爬虫URL去重(Set)

URL去重是爬虫系统的核心功能之一。利用Redis Set的自动去重特性,可以高效地实现大规模URL去重。每次抓取前先检查URL是否已存在于Set中,不存在则将URL加入Set并开始抓取。Redis Set的SISMEMBER操作时间复杂度为O(1),性能极高。当URL数量达到千万级别时,内存占用会显著增加,此时可考虑使用布隆过滤器。

# URL去重 def is_url_crawled(url): return r.sismember('crawled_urls', url) def mark_url_crawled(url): r.sadd('crawled_urls', url) # 使用示例 url = 'https://example.com/page/1' if not is_url_crawled(url): # 执行爬取 mark_url_crawled(url) # ... 处理数据

5.4 请求队列(List)

使用Redis List可以实现一个高性能的分布式爬虫队列。爬虫主进程将待爬取的URL通过LPUSH推入队列左端,多个爬虫工作进程从队列右端使用RPOP取出URL进行爬取,实现了生产者-消费者模式。这种架构天然支持分布式部署——多个爬虫节点共享同一个Redis队列,协同完成爬取任务。

# 生产者:添加URL到队列 r.lpush('request_queue', url) # 消费者:从队列取出URL(阻塞式) while True: url = r.brpop('request_queue', timeout=5) if url: url = url[1] # brpop返回(key, value)元组 # 执行爬取 crawl(url) else: break # 队列为空,退出

5.5 布隆过滤器去重

当爬虫需要处理的URL数量达到亿级时,Redis Set的内存消耗变得不可忽视。布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,它使用多个哈希函数将元素映射到一个位数组中。布隆过滤器判断"不存在"是绝对准确的,判断"存在"有一定误判率。当内存成为瓶颈时,布隆过滤器是理想的去重方案。

from pybloom_live import BloomFilter # 创建布隆过滤器(容量1000万,误判率0.1%) bloom = BloomFilter(capacity=10000000, error_rate=0.001) # 使用示例 url = 'https://example.com' if url not in bloom: bloom.add(url) # 执行爬取 # Redis 4.0+ 原生支持布隆过滤器模块 # r.bf().add('bloom_filter', url) # r.bf().exists('bloom_filter', url)

六、数据存储最佳实践

在实际爬虫项目中,数据存储不仅仅是简单的插入操作,还需要考虑数据质量、稳定性、效率和可恢复性等多方面因素。以下是爬虫数据存储的一些关键最佳实践。

6.1 避免重复数据(去重策略)

爬虫产生重复数据的原因多样:网页URL存在多种形式(带尾随斜杠、带参数顺序不同、HTTP/HTTPS混用等)、爬虫意外中断重启、多线程同时抓取等。推荐采用多层去重策略:在Redis层使用Set或布隆过滤器做第一层快速去重,在数据库层使用UNIQUE约束或唯一索引做第二层兜底去重。同时要对URL进行标准化处理(去除尾随斜杠、排序查询参数、统一协议等),提高去重命中率。

6.2 断点续爬(保存爬取状态)

大规模爬虫可能运行数小时甚至数天,期间任何意外中断都会造成已爬取数据的浪费。实现断点续爬需要在爬虫运行过程中定期持久化爬取状态:记录已爬取的URL集合、请求队列中剩余的URL、当前爬取的页码、爬虫的配置参数等。将这些状态信息定期写入Redis或本地文件,当爬虫重启时先从持久化存储中恢复状态,从中断处继续爬取,避免重复劳动。

# 保存爬虫状态 def save_crawler_state(state): r.hset('crawler:state', mapping={ 'current_page': state.page, 'crawled_count': state.crawled_count, 'last_crawl_time': state.last_time, 'status': 'paused' }) # 恢复爬虫状态 def restore_crawler_state(): state = r.hgetall('crawler:state') if state: return { 'current_page': int(state.get('current_page', 1)), 'crawled_count': int(state.get('crawled_count', 0)), } return None

6.3 数据一致性

在爬虫系统中,数据一致性主要体现在以下几个方面:一是爬取的数据完整性,确保每条记录的必填字段都有值(如URL、标题不能为空);二是爬取数据与源站数据的一致性,对于频繁更新的网站需要设计增量更新策略;三是多线程/分布式爬虫的数据写入顺序一致性。实践中建议在数据入库前进行完整性校验,对关键字段设置NOT NULL约束,对于分布式场景可使用数据库事务或分布式锁保证数据一致性。

6.4 大数据量分批存储

当爬虫需要抓取海量数据时,一次性将所有数据写入单个数据库或单张表会导致性能瓶颈。合理的数据分片策略至关重要:可以按时间分区(如按月分表)、按数据来源分库或按数据ID范围分片。MongoDB天然支持分片和自动负载均衡,MySQL则可以通过分库分表中间件(如ShardingSphere、MyCat)实现水平扩展。同时,写入操作应采用批量提交而不是逐条插入,每次批量写入100-1000条记录可获得较好的写入性能与内存消耗的平衡。

核心要点总结:

1. MySQL适合结构化数据存储,PyMySQL提供原生SQL操作,SQLAlchemy提供ORM封装,各有适用场景。

2. MongoDB适合半结构化/非结构化爬虫数据,Schema-less特性降低了数据结构变更的维护成本。

3. Redis在爬虫系统中"一身多役":URL去重(Set)、请求队列(List)、爬虫状态管理(Hash/Hash)均可用Redis高效实现。

4. 数据库选型应根据数据类型灵活选择:结构化数据用MySQL,非结构化数据用MongoDB,混合架构取长补短。

5. 生产级别的爬虫必须考虑去重、断点续爬、数据一致性和大数据量分批存储等工程实践,缺一不可。