从零到接单 11:数据存储——CSV、Excel、JSON 与 SQLite
系列目录:本文是「从零到接单:Python 自动化与爬虫实战」系列的第 11 篇。前面的文章我们已经能把网页数据抓下来了,但数据散在内存里,关了程序就没了。这篇我们系统学习如何把数据存储为 CSV、Excel、JSON 和 SQLite 数据库——这些是接单交付时客户最常要的格式。
一、选择哪种存储?
| 格式 | 适合场景 | 优点 | 缺点 | |------|----------|------|------| | CSV | 表格数据、数据交换 | 通用、简单、Excel 能打开 | 不支持多 sheet、嵌套数据 | | Excel | 需要格式、多 sheet、图表 | 所见即所得、客户最爱 | 库较重、大文件慢 | | JSON | API 返回数据、配置、嵌套数据 | 灵活、支持嵌套 | 非技术人员不熟悉 | | SQLite | 大量数据、需要查询 | 零配置、支持 SQL | 需要 SQL 知识 |
二、CSV:最通用的表格格式
基础写入
import csv
data = [
{"姓名": "张三", "年龄": 25, "城市": "北京", "薪资": 15000},
{"姓名": "李四", "年龄": 30, "城市": "上海", "薪资": 20000},
{"姓名": "王五", "年龄": 28, "城市": "广州", "薪资": 18000},
]
# 写入
with open("employees.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["姓名", "年龄", "城市", "薪资"])
writer.writeheader()
writer.writerows(data)
# 逐行写入
with open("employees.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["姓名", "年龄", "城市", "薪资"])
writer.writeheader()
for row in data:
writer.writerow(row)
print(f"写入:{row['姓名']}")
# 追加写入
with open("employees.csv", "a", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["姓名", "年龄", "城市", "薪资"])
writer.writerow({"姓名": "赵六", "年龄": 35, "城市": "深圳", "薪资": 25000})
💡
encoding="utf-8-sig"让 Excel 打开 CSV 时不乱码(加 BOM 头)。
读取
# 字典方式读取
with open("employees.csv", "r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
for row in reader:
print(f"{row['姓名']} - {row['薪资']}元")
# 转为列表
with open("employees.csv", "r", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
data = list(reader)
# 数据分析
salaries = [int(row["薪资"]) for row in data]
print(f"平均薪资:{sum(salaries) / len(salaries):.0f}元")
print(f"最高薪资:{max(salaries)}元")
print(f"最低薪资:{min(salaries)}元")
原生 csv 模块 vs pandas
# 如果没有 pandas 或数据量小,用原生 csv
import csv
# 如果装了 pandas 且数据量大,用 pandas
# import pandas as pd
# df = pd.read_csv("employees.csv")
# df.to_excel("employees.xlsx", index=False)
三、Excel:客户最爱的格式
openpyxl:读写 .xlsx
pip install openpyxl
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
# 创建 Excel 文件
wb = Workbook()
ws = wb.active
ws.title = "员工信息"
# 表头
headers = ["姓名", "年龄", "城市", "薪资"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = Font(bold=True, color="FFFFFF")
cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
cell.alignment = Alignment(horizontal="center")
# 写入数据
data = [
["张三", 25, "北京", 15000],
["李四", 30, "上海", 20000],
["王五", 28, "广州", 18000],
["赵六", 35, "深圳", 25000],
]
for row_idx, row_data in enumerate(data, 2):
for col_idx, value in enumerate(row_data, 1):
ws.cell(row=row_idx, column=col_idx, value=value)
# 添加公式
ws.cell(row=len(data) + 3, column=1, value="平均薪资")
ws.cell(row=len(data) + 3, column=4, value=f"=AVERAGE(D2:D{len(data)+1})")
# 调整列宽
for col in range(1, len(headers) + 1):
ws.column_dimensions[get_column_letter(col)].width = 15
# 保存
wb.save("employees.xlsx")
print("Excel 文件已生成 ✅")
读取 Excel
from openpyxl import load_workbook
wb = load_workbook("employees.xlsx")
ws = wb.active
# 逐行读取
for row in ws.iter_rows(min_row=2, values_only=True):
name, age, city, salary = row
print(f"{name} | {age}岁 | {city} | {salary}元")
# 转为字典列表
headers = [cell.value for cell in ws[1]]
data = []
for row in ws.iter_rows(min_row=2, values_only=True):
data.append(dict(zip(headers, row)))
多 Sheet 工作簿
wb = Workbook()
# Sheet 1:汇总
ws1 = wb.active
ws1.title = "汇总"
ws1.append(["日期", "抓取数量", "成功", "失败"])
ws1.append(["2026-07-16", 500, 485, 15])
# Sheet 2:详细数据
ws2 = wb.create_sheet("详细数据")
ws2.append(["标题", "价格", "链接", "抓取时间"])
# Web 开发 Sheet
ws3 = wb.create_sheet("Web 开发")
ws4 = wb.create_sheet("移动开发")
wb.save("report.xlsx")
四、JSON:最灵活的数据交换格式
前面第 06 篇已经介绍了 JSON 基础操作,这里补充进阶用法:
import json
# ── 处理复杂嵌套数据 ──
crawl_result = {
"meta": {
"source": "example.com",
"crawl_time": "2026-07-16 15:30:00",
"total": 1200
},
"items": [
{
"title": "Python 爬虫入门",
"url": "https://example.com/1",
"tags": ["python", "爬虫", "入门"],
"price": None
},
# ... 更多条目
]
}
# 美化写入
with open("result.json", "w", encoding="utf-8") as f:
json.dump(crawl_result, f, ensure_ascii=False, indent=2)
# ── 自定义 JSON 编码器 ──
from datetime import datetime
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return super().default(obj)
data = {"time": datetime.now(), "value": 42}
json_str = json.dumps(data, cls=CustomEncoder)
print(json_str) # {"time": "2026-07-16T15:30:00", "value": 42}
# ── 大 JSON 流式读取 ──
# 逐条读取(适合非常大的 JSON 数组文件)
def read_json_lines(filepath):
"""读取 JSON Lines 格式(每行一条 JSON)"""
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
yield json.loads(line)
# 写入 JSON Lines
with open("data.jsonl", "w", encoding="utf-8") as f:
for item in crawl_result["items"]:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
五、SQLite:零配置数据库
SQLite 不需要安装服务器,一个文件就是一个数据库。适合中小型数据。
基础操作
import sqlite3
# 连接数据库(文件不存在则自动创建)
conn = sqlite3.connect("crawler.db")
cursor = conn.cursor()
# 创建表
cursor.execute("""
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT UNIQUE,
author TEXT,
publish_date TEXT,
views INTEGER DEFAULT 0,
crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 插入数据
cursor.execute("""
INSERT INTO articles (title, url, author, publish_date, views)
VALUES (?, ?, ?, ?, ?)
""", ("Python 爬虫入门", "https://example.com/1", "张三", "2026-07-01", 1280))
# 批量插入
articles = [
("自动化脚本实战", "https://example.com/2", "李四", "2026-07-05", 2560),
("RPA 入门指南", "https://example.com/3", "张三", "2026-07-10", 960),
]
cursor.executemany("""
INSERT OR IGNORE INTO articles (title, url, author, publish_date, views)
VALUES (?, ?, ?, ?, ?)
""", articles)
conn.commit()
查询
# 查询所有
cursor.execute("SELECT * FROM articles")
for row in cursor.fetchall():
print(row)
# 条件查询
cursor.execute("SELECT title, views FROM articles WHERE views > ?", (1000,))
for row in cursor.fetchall():
print(f"{row[0]} - {row[1]}阅读")
# 聚合查询
cursor.execute("""
SELECT author, COUNT(*) as count, SUM(views) as total_views
FROM articles
GROUP BY author
""")
for author, count, total in cursor.fetchall():
print(f"{author}: {count}篇, 总阅读{total}")
# 分页查询
page = 1
page_size = 10
offset = (page - 1) * page_size
cursor.execute(f"SELECT * FROM articles LIMIT {page_size} OFFSET {offset}")
# 字典形式读取(推荐)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM articles")
for row in cursor.fetchall():
print(dict(row))
conn.close()
爬虫专用存储类
import sqlite3
from datetime import datetime
class CrawlerDB:
"""爬虫数据库工具"""
def __init__(self, db_path="crawler.db"):
self.conn = sqlite3.connect(db_path)
self.conn.row_factory = sqlite3.Row
self._init_tables()
def _init_tables(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_url TEXT,
title TEXT,
content TEXT,
price REAL,
category TEXT,
crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_url, title)
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS crawl_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT,
status TEXT,
items_count INTEGER,
error TEXT,
start_time TIMESTAMP,
end_time TIMESTAMP
)
""")
self.conn.commit()
def insert_items(self, items):
cursor = self.conn.cursor()
count = 0
for item in items:
try:
cursor.execute("""
INSERT OR IGNORE INTO items (source_url, title, content, price, category)
VALUES (?, ?, ?, ?, ?)
""", (item.get("source_url"), item.get("title"),
item.get("content"), item.get("price"), item.get("category")))
if cursor.rowcount > 0:
count += 1
except sqlite3.Error as e:
print(f"插入失败:{e}")
self.conn.commit()
return count
def log_crawl(self, url, status, items_count, error=None):
self.conn.execute("""
INSERT INTO crawl_log (url, status, items_count, error, start_time, end_time)
VALUES (?, ?, ?, ?, ?, ?)
""", (url, status, items_count, error, datetime.now(), datetime.now()))
self.conn.commit()
def get_stats(self):
cursor = self.conn.cursor()
cursor.execute("SELECT COUNT(*) FROM items")
total_items = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(DISTINCT source_url) FROM items")
total_sources = cursor.fetchone()[0]
return {"total_items": total_items, "total_sources": total_sources}
def export_to_csv(self, filename="export.csv"):
import csv
cursor = self.conn.cursor()
cursor.execute("SELECT * FROM items")
rows = cursor.fetchall()
if not rows:
return 0
headers = rows[0].keys()
with open(filename, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
writer.writerows([dict(r) for r in rows])
return len(rows)
def close(self):
self.conn.close()
六、数据存储选择决策流程图
数据量 < 1 万条?
├─ 是 → 客户要求 Excel?
│ ├─ 是 → openpyxl 写 .xlsx
│ └─ 否 → CSV(够简单、通用)
└─ 否 → SQLite(支持 SQL 查询)
└─ 数据量 > 100 万?→ MySQL / PostgreSQL
总结
| 存储方式 | 核心库 | 写操作 | 读操作 |
|---------|--------|--------|--------|
| CSV | csv | csv.DictWriter | csv.DictReader |
| Excel | openpyxl | Workbook() → cell.value = | load_workbook() → iter_rows() |
| JSON | json | json.dump() | json.load() |
| SQLite | sqlite3 | cursor.execute("INSERT...") | cursor.execute("SELECT...") |
数据存储是交付的"最后一公里"。下一篇我们进入办公自动化——用 openpyxl、python-docx 等库处理 Word/Excel/PDF,这是自动化接单的核心利润点。
练习:改造第 09 篇的 BlogCrawler,让它在爬取文章列表后,同时保存为 CSV 和 SQLite 两种格式。SQLite 中记录爬取日志(URL、抓取时间、成功条数)。
Comments
Sign in to leave a comment.