0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiitaのストック記事をPythonで自動取得してObsidianに保存する

0
Posted at

やりたいこと

Qiitaでストックした記事がどんどん溜まっていくのに、振り返る機会がない。
そこで、Pythonスクリプトで Qiitaのストック記事を自動取得 し、ObsidianのVaultにMarkdownファイルとして保存 する仕組みを作りました。

タスクスケジューラで定期実行すれば、勝手にリーディングリストが育っていきます。

完成イメージ

実行するとこんなログが流れて、

=== Qiita ストック取得中... === 
[SAVED] ClaudCode入門
[SAVED] Python便利帖
 ...  完了: 24 件処理

Obsidianにはこんなファイルが自動生成されます。

ReadingList/
└── Qiita/ 
├── 2026-03-16_ClaudCode入門.md
├── 2026-03-16_Python便利帖.md 
└── ...

各ファイルにはYAMLフロントマターが付いているので、Dataviewプラグインで未読一覧を表示したり、タグでフィルタしたりもできます。

title: "Claudcode入門"
source: Qiita
url: https://qiita.com/xxx/items/yyy
author: "@someone"
tags: ["Claude"]
saved_at: 2026-03-16
status: unread

事前準備

  1. Pythonのインストール
    https://www.python.org/downloads/ からダウンロード。
    インストール時に 「Add Python to PATH」にチェック を入れてください。

  2. requestsライブラリのインストール

Copypip install requests
  1. Qiitaアクセストークンの取得
    https://qiita.com/settings/applications にアクセス
    「新しくトークンを発行する」をクリック
    スコープは read_qiita のみにチェック
    「発行する」→ 表示されたトークンをコピー(※二度と表示されません)

スクリプト

以下のファイルを作成してください。

"""
Qiitaストック → Obsidian ReadingList 自動取得スクリプト
"""

import os
import re
import json
import time
import requests
from datetime import datetime
from pathlib import Path

# ============================================================
# 設定
# ============================================================

# Obsidian Vault の ReadingList フォルダ(自分の環境に合わせて変更)
VAULT_PATH = Path(r"C:\Users\yourname\Documents\Obsidian Vault\ReadingList")

# Qiita
QIITA_USER_ID = "your_qiita_id"       # ← 自分のQiitaユーザーID
QIITA_TOKEN = "your_qiita_token"       # ← 取得したアクセストークン

# ============================================================
# ユーティリティ
# ============================================================

def sanitize_filename(name: str) -> str:
    """ファイル名に使えない文字を除去"""
    name = re.sub(r'[\\/*?:"<>|]', "", name)
    name = name.strip()
    if len(name) > 100:
        name = name[:100]
    return name


def already_saved(folder: Path, url: str) -> bool:
    """同じURLの記事が既に保存されているかチェック"""
    if not folder.exists():
        return False
    for f in folder.glob("*.md"):
        try:
            content = f.read_text(encoding="utf-8")
            if url in content:
                return True
        except Exception:
            continue
    return False


def save_article(folder: Path, title: str, url: str, author: str,
                 tags: list, source: str):
    """Markdownファイルとして保存"""
    folder.mkdir(parents=True, exist_ok=True)

    if already_saved(folder, url):
        print(f"  [SKIP] 既に保存済み: {title}")
        return

    date_str = datetime.now().strftime("%Y-%m-%d")
    safe_title = sanitize_filename(title)
    filename = f"{date_str}_{safe_title}.md"
    filepath = folder / filename

    tags_yaml = json.dumps(tags, ensure_ascii=False) if tags else "[]"
    tags_md = " ".join([f"#{t}" for t in tags]) if tags else ""

    content = f"""---
title: "{title}"
source: {source}
url: {url}
author: "{author}"
tags: {tags_yaml}
saved_at: {date_str}
status: unread
---

# {title}

- **元記事**: [{source}]({url})
- **著者**: {author}
- **タグ**: {tags_md}

## メモ

"""

    filepath.write_text(content, encoding="utf-8")
    print(f"  [SAVED] {title}")


# ============================================================
# Qiita ストック取得
# ============================================================

def fetch_qiita_stocks():
    """Qiita APIでストック記事を取得"""
    print("\n=== Qiita ストック取得中... ===")
    folder = VAULT_PATH / "Qiita"

    headers = {}
    if QIITA_TOKEN:
        headers["Authorization"] = f"Bearer {QIITA_TOKEN}"

    page = 1
    total_saved = 0

    while True:
        url = f"https://qiita.com/api/v2/users/{QIITA_USER_ID}/stocks"
        params = {"page": page, "per_page": 100}

        try:
            resp = requests.get(url, headers=headers, params=params)
            resp.raise_for_status()
        except requests.RequestException as e:
            print(f"  [ERROR] Qiita API エラー: {e}")
            break

        items = resp.json()
        if not items:
            break

        for item in items:
            title = item.get("title", "無題")
            item_url = item.get("url", "")
            author = item.get("user", {}).get("id", "unknown")
            tags = [t.get("name", "") for t in item.get("tags", [])]

            save_article(folder, title, item_url, f"@{author}", tags, "Qiita")
            total_saved += 1

        if len(items) < 100:
            break

        page += 1
        time.sleep(1)

    print(f"  Qiita 完了: {total_saved} 件処理")


# ============================================================
# メイン
# ============================================================

def main():
    print(f"実行時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"保存先: {VAULT_PATH}")

    VAULT_PATH.mkdir(parents=True, exist_ok=True)
    (VAULT_PATH / "Qiita").mkdir(exist_ok=True)

    fetch_qiita_stocks()

    print("\n=== 完了! ===")


if __name__ == "__main__":
    main()

使い方

手動実行

Copypython fetch_qiita_stocks.py

タスクスケジューラで毎日自動実行(Windows)

Windowsの検索で「タスクスケジューラ」を開く
「基本タスクの作成」→ 名前を「Qiitaストック取得」
トリガー → 「毎日」→ 好きな時間(例: 朝9:00)
操作 → 「プログラムの開始」→ python を指定、引数にスクリプトのフルパス
Obsidianで未読一覧を表示(Dataviewプラグイン)
Dataviewプラグインをインストールしておけば、以下のクエリで未読記事だけ一覧表示できます。

TABLE author, tags, saved_at
FROM "ReadingList/Qiita"
WHERE status = "unread"
SORT saved_at DESC

仕組み

使っているのは Qiita API v2 の GET /api/v2/users/:user_id/stocks エンドポイントです。認証トークンありだと1時間に1000リクエスト可能なので、よほどの大量ストックでない限り問題なく全件取得できます。

重複チェックはファイル内のURLで判定しているため、2回目以降の実行では新規ストック分だけが追加されます。

まとめ

「あとで読む」でストックした記事は、外に出してあげないとなかなか読みにいきません。
Obsidianに集約しておけば、Dataviewで一覧化もできるし、読んだらメモを書き足すこともできます。
ストックを腐らせている方はぜひ試してみてください。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?