Featured image of post 用 DuckDB 搭建可订阅的行业数据简报:从 0 到月入 2 万的自动化数据产品

用 DuckDB 搭建可订阅的行业数据简报:从 0 到月入 2 万的自动化数据产品

手把手教你用 DuckDB 构建多源数据整合管道,自动生成行业数据简报,打造可订阅的被动收入数据产品。从数据源采集到变现全链路实战。

行业数据简报产品架构

难度:⭐⭐⭐ | 预计投入:2小时搭建原型,之后每周仅需 2-3 小时维护


一个人,一台电脑,每个月收 3000-5000 订阅费,服务 50-80 个付费用户,月入 2-4 万。不需要团队,不需要办公室,不需要融资。

听起来像画饼?这是真实发生的。今天拆解一个用 DuckDB 构建的「行业数据简报」订阅产品的完整变现路径,从数据源选择到自动报表生成,再到多渠道分发变现。

一、这个产品解决的是什么痛点?

很多中小企业主、投资人、行业从业者,每天需要关注某个领域的最新数据动态——但没有人手去做。

具体场景:

  • 餐饮老板想知道「最近一个月奶茶品类的开店增速和头部品牌动态」
  • 创业者想知道「AI Agent 赛道最近半年融资数据和明星公司」
  • 投资者想知道「新能源电池领域每月的产能变化和价格趋势」

他们愿意付费,因为他们没有时间也没有能力自己做数据调研。一份「每周更新、数据可靠、直接给结论」的行业简报,市场价 ¥99-499/月。

二、核心优势:为什么选 DuckDB?

在做这个产品之前,你可能会考虑用 pandas 写 ETL + Excel 输出。但 DuckDB 带来了质的飞跃:

维度Pandas + Excel 方案DuckDB 方案
数据源接入需要下载再处理直接 read_csv_auto/read_json_auto 读取远程文件
类型推断需要手动指定 schema自动推断列类型
数据处理Python 代码 + SQL 混写纯 SQL 完成 90% 操作
执行速度内存受限,大数据慢向量化执行,GB 级数据秒级响应
部署成本需要服务器或本地环境单个 .db 文件,零基础设施
可移植性依赖环境配置随时随地运行

关键策略:数据源越多,简报的独特性越强。你的竞争对手可能只用 1-2 个数据源,你用 5-6 个交叉验证,结论更可靠。

三、第一步:搭建数据处理管道

假设你的简报主题是「中国新能源汽车市场周报」,你需要整合以下数据:

  1. 中汽协的月度销量数据(CSV)
  2. 各品牌的车型价格数据(爬虫)
  3. 充电基础设施数据(政府公开数据)
  4. 原材料锂价数据(金融数据接口)
import duckdb
from pathlib import Path

DB_PATH = "nev_industry.db"
RAW_DIR = Path("./data/raw")
PROCESS_DIR = Path("./data/processed")

con = duckdb.connect(DB_PATH)

# ── 建表:每月更新时清空 staging 再重新加载 ──
con.execute("""
CREATE TABLE IF NOT EXISTS sales_data (
    month VARCHAR,
    brand VARCHAR,
    model VARCHAR,
    sales_volume INTEGER,
    yoy_growth_pct DECIMAL(10,2),
    data_source VARCHAR
)
""")

con.execute("""
CREATE TABLE IF NOT EXISTS charging_stations (
    province VARCHAR,
    city VARCHAR,
    total_stations INTEGER,
    fast_chargers INTEGER,
    quarter VARCHAR
)
""")

con.execute("""
CREATE TABLE IF NOT EXISTS battery_prices (
    date VARCHAR,
    price_per_kwh DECIMAL(10,2),
    change_pct DECIMAL(10,2),
    source VARCHAR
)
""")

print("✅ 数据表结构已创建")

四、第二步:多源数据自动刷新

每月更新数据时,只需要调用对应的刷新函数即可:

def refresh_sales_data(csv_path):
    """每月更新销量数据"""
    con.execute("TRUNCATE TABLE sales_data")
    con.execute(f"""
        INSERT INTO sales_data
        SELECT * FROM read_csv_auto('{csv_path}', 
            headers=true, 
            autoparse=true,
            sep=','
        )
    """)
    print(f"✅ 销量数据已更新,共 {con.execute('SELECT COUNT(*) FROM sales_data').fetchone()[0]} 条")

def refresh_charging_data(json_path):
    """更新充电桩数据"""
    con.execute("TRUNCATE TABLE charging_stations")
    con.execute(f"""
        INSERT INTO charging_stations
        SELECT * FROM read_json_auto('{json_path}')
    """)
    print(f"✅ 充电桩数据已更新")

def refresh_battery_prices(csv_path):
    """更新电池价格数据"""
    con.execute("TRUNCATE TABLE battery_prices")
    con.execute(f"""
        INSERT INTO battery_prices
        SELECT * FROM read_csv_auto('{csv_path}', 
            headers=true, 
            timezone='Asia/Shanghai'
        )
    """)
    print(f"✅ 电池价格数据已更新")

核心要点:使用 TRUNCATE + INSERT 的模式实现幂等更新——每次运行都从原始数据重新加载,避免增量更新的复杂逻辑。对于日级别或周级别的简报,数据量不大,这种简单策略完全够用。

五、第三步:SQL 驱动的自动化简报生成

这是整个系统的核心。你需要写一套 SQL 查询,自动从多源数据中提取简报需要的关键指标:

def generate_weekly_report():
    """生成周报的核心数据"""
    
    # ── 指标 1:本周销量 TOP10 品牌 ──
    top_brands = con.execute("""
        SELECT 
            brand,
            SUM(sales_volume) AS total_sales,
            ROUND(AVG(yoy_growth_pct), 1) AS avg_yoy_growth,
            COUNT(DISTINCT model) AS model_count
        FROM sales_data
        WHERE month = (SELECT MAX(month) FROM sales_data)
        GROUP BY brand
        ORDER BY total_sales DESC
        LIMIT 10
    """).fetchdf()
    
    # ── 指标 2:各价位段市场份额变化 ──
    price_segment = con.execute("""
        WITH order_data AS (
            SELECT 
                model,
                brand,
                CASE 
                    WHEN avg_price < 10 THEN '<10万'
                    WHEN avg_price < 20 THEN '10-20万'
                    WHEN avg_price < 30 THEN '20-30万'
                    ELSE '30万+'
                END AS price_segment,
                SUM(sales_volume) AS sales
            FROM (
                SELECT 
                    s.model,
                    s.brand,
                    s.sales_volume,
                    CASE 
                        WHEN s.brand IN ('比亚迪海豚', '五菱缤果') THEN 8
                        WHEN s.brand IN ('比亚迪秦PLUS', '广汽埃安Y') THEN 12
                        WHEN s.brand IN ('比亚迪汉', '小鹏P7') THEN 20
                        ELSE 35
                    END AS avg_price
                FROM sales_data s
                WHERE s.month = (SELECT MAX(month) FROM sales_data)
            ) sub
            GROUP BY model, brand, price_segment
        )
        SELECT 
            price_segment,
            SUM(sales) AS total_sales,
            ROUND(SUM(sales) * 100.0 / NULLIF(SUM(SUM(sales)) OVER (), 0), 1) AS market_share_pct
        FROM order_data
        GROUP BY price_segment
        ORDER BY total_sales DESC
    """).fetchdf()
    
    # ── 指标 3:充电设施覆盖率变化 ──
    charging_coverage = con.execute("""
        SELECT 
            province,
            SUM(total_stations) AS total_stations,
            SUM(fast_chargers) AS total_fast_chargers,
            ROUND(SUM(fast_chargers) * 100.0 / NULLIF(SUM(total_stations), 0), 2) AS fast_ratio_pct
        FROM charging_stations
        WHERE quarter = (SELECT MAX(quarter) FROM charging_stations)
        GROUP BY province
        ORDER BY total_stations DESC
        LIMIT 10
    """).fetchdf()
    
    # ── 指标 4:电池成本趋势(影响整车利润的关键信号)──
    battery_trend = con.execute("""
        SELECT 
            date,
            price_per_kwh,
            change_pct,
            ROUND(AVG(price_per_kwh) OVER (
                ORDER BY date 
                ROWS BETWEEN 29 PRECEDING AND CURRENT ROW
            ), 2) AS ma_30d
        FROM battery_prices
        ORDER BY date DESC
        LIMIT 14
    """).fetchdf()
    
    return {
        'top_brands': top_brands,
        'price_segment': price_segment,
        'charging_coverage': charging_coverage,
        'battery_trend': battery_trend
    }

这里有几个关键技术点:

  • 窗口函数SUM(SUM(sales)) OVER () 计算总销售额用于份额百分比
  • CTE 嵌套:用 CTE 分层处理,先分价位段再聚合,逻辑清晰
  • 移动平均ROWS BETWEEN 29 PRECEDING AND CURRENT ROW 计算 30 天均线,检测趋势

六、第四步:数据翻译成「人话」

数据有了,下一步是把数据翻译成「人话」。这一步决定你的简报值不值钱:

def format_report_text(data):
    """将数据转化为自然语言简报"""
    import datetime
    lines = []
    lines.append("📊 新能源汽车行业周报 | " + datetime.date.today().strftime("%Y年%m月%d日"))
    lines.append("")
    
    # ── 头条:本周最大新闻 ──
    top_brand = data['top_brands'].iloc[0]
    lines.append(f"🏆 本周焦点:{top_brand['brand']}{int(top_brand['total_sales']):,} 辆的销量继续领跑,同比增长 {top_brand['avg_yoy_growth']}%")
    lines.append("")
    
    # ── 细分市场洞察 ──
    lines.append("📈 价格段竞争格局:")
    for _, row in data['price_segment'].iterrows():
        emoji = "🔥" if row['market_share_pct'] > 30 else "📌"
        lines.append(f"  {emoji} {row['price_segment']}{int(row['total_sales']):,} 辆(占比 {row['market_share_pct']}%)")
    lines.append("")
    
    # ── 基础设施进展 ──
    lines.append("🔋 充电设施进展(TOP 3 省份):")
    for _, row in data['charging_coverage'].head(3).iterrows():
        lines.append(f"  {row['province']}{int(row['total_stations']):,} 座充电桩,快充占比 {row['fast_ratio_pct']}%")
    lines.append("")
    
    # ── 成本信号 ──
    latest_battery = data['battery_trend'].iloc[0]
    prev_battery = data['battery_trend'].iloc[1] if len(data['battery_trend']) > 1 else latest_battery
    change_note = "📉 下降" if latest_battery['change_pct'] < 0 else "📈 上涨"
    lines.append(f"⚡ 电池成本信号:碳酸锂价格 {latest_battery['price_per_kwh']} 元/千瓦时({change_note} {abs(latest_battery['change_pct'])}%),30日均价 {latest_battery['ma_30d']} 元/千瓦时")
    lines.append("")
    
    # ── 本周结论(这是最值钱的部分)──
    lines.append("💡 本周核心结论:")
    
    # 自动生成的洞察逻辑
    if top_brand['avg_yoy_growth'] > 20:
        lines.append("  1. 头部品牌仍保持高速增长,行业集中度进一步提升")
    if len(data['price_segment']) > 0 and data['price_segment'].iloc[0]['price_segment'] == '10-20万':
        lines.append("  2. 10-20 万价格段仍是竞争主战场,性价比车型最受青睐")
    if latest_battery['change_pct'] < -2:
        lines.append("  3. 电池成本快速下降,下半年整车价格战压力缓解")
    
    lines.append("")
    lines.append("---")
    lines.append("数据来源:中汽协、工信部、各品牌官方公告、行业公开数据")
    lines.append("本报告由 DuckDB 自动化生成,数据更新时间:本周最新周期")
    
    return "\n".join(lines)

# 生成并保存简报
report_data = generate_weekly_report()
report_text = format_report_text(report_data)

# 保存为 Markdown 文件
output_path = f"./reports/report_{datetime.date.today().strftime('%Y%m%d')}.md"
with open(output_path, 'w', encoding='utf-8') as f:
    f.write(report_text)

print(f"✅ 简报已生成:{output_path}")

七、第五步:多渠道分发与变现

简报做好了,怎么让别人看到并付费?

方案 A:微信公众号/知识星球订阅

每周自动发送到公众号,付费用户看完整版。免费版:头条 + 结论;付费版:全量数据 + 深度分析。

方案 B:付费 Newsletter(Substack/小报童)

在 Substack 或小报童上开设付费订阅。月费 ¥99,提供独家数据解读 + 可下载的原始数据。

方案 C:企业定制版

为特定企业提供定制简报(如某车企需要竞品深度分析)。月费 ¥2000-5000,按客户需求调整数据维度。

方案 D:数据 API

把核心指标封装成 API,供其他开发者调用。按调用次数收费,如 ¥0.01/次。

def publish_report(report_text, report_data):
    """多渠道分发"""
    PROCESS_DIR.mkdir(parents=True, exist_ok=True)
    
    # 保存原始数据(供付费用户使用)
    report_data['top_brands'].to_csv(
        PROCESS_DIR / f"top_brands_{datetime.date.today().strftime('%Y%m%d')}.csv",
        index=False, encoding='utf-8-sig'
    )
    report_data['price_segment'].to_csv(
        PROCESS_DIR / f"price_segment_{datetime.date.today().strftime('%Y%m%d')}.csv",
        index=False, encoding='utf-8-sig'
    )
    
    print("✅ 数据文件已保存,可供付费用户下载")
    print("📤 简报文本已准备好,可发送到各分发渠道")

八、变现金额测算

以「新能源汽车周报」为例:

订阅档位月费目标用户数月收入
个人订阅¥99100 人¥9,900
企业订阅¥49910 家¥4,990
定制报告¥2,0003 家¥6,000
合计¥20,890

成本结构

  • DuckDB 运行成本:几乎为零(本地运行)
  • 数据源成本:大部分免费,部分付费数据源约 ¥500/月
  • 时间成本:每周 2-3 小时(数据更新 + 内容审核)
  • 服务器成本:¥100/月(用于存储和分发)

净利润:约 ¥18,000-20,000/月

九、与传统方案对比

环节传统方案(Pandas + Excel)DuckDB 方案
数据获取手动下载 CSV → 本地处理read_csv_auto 直接读取远程 URL
数据处理Python 代码编写 ETL 流程纯 SQL 完成聚合、计算、格式化
类型处理手动指定 dtype 和 schemaautoparse 自动推断
输出格式Excel + 手动整理SQL 结果直接转为 Markdown
调度方式手动运行或 cron + 复杂脚本DuckDB + Python 一键生成
部署需要依赖环境和库单文件 .db,零依赖

十、今晚行动清单

  1. 选一个你熟悉的行业(你本身就是这个行业的从业者最好)
  2. 找到 3-5 个公开数据源(政府网站、行业协会、开源数据集)
  3. 用 DuckDB 搭一个最小可行版本:能自动从数据源读取 → 生成简单简报
  4. 先发给 10 个潜在用户免费试用,收集反馈
  5. 迭代优化后,开始收费

记住:最好的数据产品不是「数据最全的」,而是「最能帮人做决策的」。DuckDB 让你把精力放在「提炼洞察」上,而不是「搬运数据」上。


📖 完整的行业简报项目模板(含数据源清单、SQL 模板、自动化调度脚本)已发布在 duckdblab.org,你可以直接基于模板替换数据源,快速启动自己的数据订阅产品。

💡 想系统学习如何用 DuckDB 搭建可商业化的数据产品?→ duckdblab.org 上有从 0 到 1 的完整教程系列

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计