13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go言語】Qiitaの投稿をGitHubのプロフィールに反映させてみた

Last updated at Posted at 2025-01-24

はじめに

最近、GitHubのプロフィールをもっと魅力的にしようといろいろカスタマイズしていました。その中で、「自分が投稿したQiitaの記事もプロフィール上に表示できたらいいな」と思いました👍

そこで今回は、Goを使ってQiitaのRSSから投稿記事を取得し、GitHubのプロフィールに表示させるまでのプロセスをまとめてみようと思います。
ぜひ最後までお付き合いください!

記事表示の完成イメージ

スクリーンショット 2025-01-24 18.44.17.png

このような感じでGitHubのプロフィール(README.md)に掲載します。

ソースコード

早速コードの紹介をしていきます。

main.go
package main

import (
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sort"
	"time"
)

type QiitaAtom struct {
	Entries []struct {
		Title     string `xml:"title"`
		Link      string `xml:"link"`
		Published string `xml:"published"`
	} `xml:"entry"`
}

type Post struct {
	Title string
	Date  time.Time
	URL   string
}

func fetchQiitaFeed(feedURL string) ([]Post, error) {
	resp, err := http.Get(feedURL)
	if err != nil {
		return nil, fmt.Errorf("Qiitaフィード取得エラー: %w", err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("フィードデータ読み込みエラー: %w", err)
	}

	var feed QiitaAtom
	if err := xml.Unmarshal(data, &feed); err != nil {
		return nil, fmt.Errorf("フィード解析エラー: %w", err)
	}

	var posts []Post
	for _, entry := range feed.Entries {
		date, err := time.Parse(time.RFC3339, entry.Published)
		if err != nil {
			continue
		}
		posts = append(posts, Post{
			Title: entry.Title,
			Date:  date,
			URL:   entry.Link,
		})
	}

	sort.Slice(posts, func(i, j int) bool {
		return posts[i].Date.After(posts[j].Date)
	})

	return posts, nil
}

func main() {
	const feedURL = "https://qiita.com/fujifuji1414/feed.atom"

	posts, err := fetchQiitaFeed(feedURL)
	if err != nil {
		log.Fatalf("フィード取得エラー: %v", err)
	}

	distMD := "**Recent Qiita Articles**\n"
	for i, post := range posts {
		if i >= 5 {
			break
		}
		distMD += fmt.Sprintf("- ![](img/qiita.png) [%s](%s)\n", post.Title, post.URL)
	}

	readme, err := os.ReadFile("README.md")
	if err != nil {
		log.Fatalf("README読み込みエラー: %v", err)
	}

	newReadme := `<!--[START POSTS]-->` + "\n" + distMD + `<!--[END POSTS]-->`
	readmeContent := string(readme)
	readmeContent = replaceBetween(readmeContent, "<!--[START POSTS]-->", "<!--[END POSTS]-->", newReadme)

	if err := os.WriteFile("README.md", []byte(readmeContent), 0644); err != nil {
		log.Fatalf("README書き込みエラー: %v", err)
	}

	fmt.Println("README.md が更新されました!")
}

// 指定したプレースホルダー間の文字列を置換
func replaceBetween(content, start, end, newContent string) string {
	startIdx := indexOf(content, start) + len(start)
	endIdx := indexOf(content, end)
	if startIdx == -1 || endIdx == -1 || startIdx >= endIdx {
		return content // プレースホルダーが見つからない場合はそのまま返す
	}
	return content[:startIdx] + "\n" + newContent + "\n" + content[endIdx:]
}

// 部分文字列の開始インデックスを取得
func indexOf(content, substr string) int {
	return findIndex(content, substr)
}

// 部分文字列を検索して最初に見つかった位置を返す
func findIndex(content, substr string) int {
	idx := -1
	for i := 0; i+len(substr) <= len(content); i++ {
		if content[i:i+len(substr)] == substr {
			idx = i
			break
		}
	}
	return idx
}

QiitaのRSSフィードを取得し、記事データをMarkdown形式で整形し、README.md を自動更新するGoプログラムとなっています。

それでは、セクションごとに見ていきましょう!

インポートセクション

main.go
import (
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sort"
	"time"
)
  • encoding/xmlはXMLを構造体にマッピングしてくれるパッケージ
  • osはファイルの読み書きに使用
  • その他は必要な標準パッケージとなります

構造体の定義

main.go
type QiitaAtom struct {
	Entries []struct {
		Title     string `xml:"title"`
		Link      struct {
			Href string `xml:"href,attr"`
		} `xml:"link"`
		Published string `xml:"published"`
	} `xml:"entry"`
}
  • QiitaのAtomフィードに対応した構造体です
  • Entriesは記事ごとのデータを保持します
    • Title, Link フィールドで記事のタイトルとリンクを保持
    • 各記事の情報として以下を取得
      • Title: 記事のタイトル
      • Link: 記事のURL
      • Published: 記事の公開日時
    • タグ (xml:"title", xml:"link") はXMLの特定フィールドと対応付けるために使用(RSSはxml形式で記述されています)

fetchQiitaFeed関数

main.go
func fetchQiitaFeed(feedURL string) ([]Post, error) {
	resp, err := http.Get(feedURL)
	if err != nil {
		return nil, fmt.Errorf("Qiitaフィード取得エラー: %w", err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("フィードデータ読み込みエラー: %w", err)
	}

	var feed QiitaAtom
	if err := xml.Unmarshal(data, &feed); err != nil {
		return nil, fmt.Errorf("フィード解析エラー: %w", err)
	}

	var posts []Post
	for _, entry := range feed.Entries {
		date, err := time.Parse(time.RFC3339, entry.Published)
		if err != nil {
			continue
		}
		posts = append(posts, Post{
			Title: entry.Title,
			Date:  date,
			URL:   entry.Link,
		})
	}

	// 日付順にソート
	sort.Slice(posts, func(i, j int) bool {
		return posts[i].Date.After(posts[j].Date)
	})

	return posts, nil
}

こちらはRSSデータを取得する関数になっています。
まず、指定したURL(feedURL)からフィードデータを取得します。
次に、Unmarshalio.ReadAll(resp.Body)で取得したXMLデータをGoの構造体に変換しています。
この際、構造体のフィールドとXMLタグを対応させるために、タグマッピング(xml:"entry")を利用します。

おそらくUnmarshalの部分がイメージしにくいと思うので以下の動作例と一緒に処理を見ていきましょう。
例えば、以下のXMLデータを処理するとします

xml
<feed>
  <entry>
    <title>記事タイトル</title>
    <link href="記事のリンク" />
    <published>2023-01-01T00:00:00Z</published>
  </entry>
</feed>

こちらをUnmarshalで構造体に変換させると、、、

QiitaAtom{
    Entries: []struct{
        Title string
        Link struct{ Href string }
        Published string
    }{
        {Title: "記事タイトル", Link: {Href: "記事のリンク"}, Published: "2023-01-01T00:00:00Z"},
    },
}

取得したデータがEntriesに格納されていることが確認できると思います。

データの整形

for _, entry := range feed.Entries {
    date, err := time.Parse(time.RFC3339, entry.Published)
    if err != nil {
        continue
    }
    posts = append(posts, Post{
        Title: entry.Title,
        Date:  date,
        URL:   entry.Link.Href,
    })
}
  • 処理された記事がPost構造体に変換します
  • time.Time型はソート処理に使用します

main関数

main.go
func main() {
	const feedURL = "https://qiita.com/Qiita_username/feed.atom"

	// Qiitaフィードを取得
	posts, err := fetchQiitaFeed(feedURL)
	if err != nil {
		log.Fatalf("フィード取得エラー: %v", err)
	}

	// 上位5件をMarkdown形式で整形
	distMD := "**Recent Qiita Articles**\n"
	for i, post := range posts {
		if i >= 5 {
			break
		}
		distMD += fmt.Sprintf("- [%s](%s)\n", post.Title, post.URL)
	}

	// README.mdの更新
	readme, err := os.ReadFile("README.md")
	if err != nil {
		log.Fatalf("README読み込みエラー: %v", err)
	}

	newReadme := replaceBetween(string(readme), "<!--[START POSTS]-->", "<!--[END POSTS]-->", distMD)

	if err := os.WriteFile("README.md", []byte(newReadme), 0644); err != nil {
		log.Fatalf("README書き込みエラー: %v", err)
	}

	fmt.Println("README.md が更新されました!")
}

まず、QiitaのAフィードURLを feedURL 定数として指定します。
次にfetchQiitaFeed関数を呼び出し、記事リストの取得をします。
最終的に、取得した記事をREADME.mdに反映させます。

こちらはご自身のQiitaのアカウント名に変更してください。
https://qiita.com/Qiita_username/feed.atom

例) 私の場合はfujifuji1414なので
https://qiita.com/fujifuji1414/feed.atom

記事セクションの更新

newReadme := replaceBetween(string(readme), "<!--[START POSTS]-->", "<!--[END POSTS]-->", distMD)

README.mdへの書き込み

if err := os.WriteFile("README.md", []byte(newReadme), 0644); err != nil {
    log.Fatalf("README書き込みエラー: %v", err)
}

更新された内容をREADME.mdに書き込み、ファイルのパーミッションは0644に設定しておきます。
※ 事前にREADME.mdにプレースホルダー<!--[START POSTS]--><!--[END POSTS]--> を事前に設定しておいてください。

それでは実際にGoを実行して以下のような出力が得られたらOKです!

terminal
$ go run main.go
README.md が更新されました!

README.mdは以下のように更新されます。

README.md
**Recent Qiita Articles**
- [記事タイトル1](リンク1)
- [記事タイトル2](リンク2)
- [記事タイトル3](リンク3)
- [記事タイトル4](リンク4)
- [記事タイトル5](リンク5)

更新を自動化する

このままだと手動でコマンドを叩く必要があるのでGitHub Actionsで自動化していきましょう。

posts.yml
name: Posts Updater

on:
  schedule:
    - cron: '0 0 * * *' # 必要に応じて変更可能

jobs:
  update-posts:
    runs-on: ubuntu-latest

    steps:
      # リポジトリをチェックアウト
      - name: Checkout
        uses: actions/checkout@v3

      # Gitの設定
      - name: Git setting
        run: |
          git config --local user.email "your_username@users.noreply.github.com"
          git config --local user.name "your_username"

      # Goのセットアップ
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.20' # 必要に応じてGoのバージョンを変更

      # 必要な依存関係のインストール(もしある場合)
      - name: Install dependencies
        run: go mod tidy

      # Goプログラムを実行してREADME.mdを更新
      - name: Run updater
        run: go run main.go

      # README.mdをコミットしてプッシュ
      - name: Commit and push changes
        run: |
          git add README.md
          git commit -m "update posts"
          git push origin main

  • 毎時実行する設定です。必要に応じてCRON形式でスケジュールを調整してください
  • 例) 毎日午前9時に実行する場合は 0 9 * * * と指定します(詳しくはこちら)

注意

  • メールアドレスとユーザー名
    • your_username@users.noreply.github.comyour_username を適切な値に置き換えてください
    • メールアドレスは、リポジトリに関連付けられたものを使用します。
  • Goプログラムの依存
    • main.goの依存関係が正しく管理されているか(go.modが正しく設定されているか)確認してください
  • ブランチについて
    • デフォルトのブランチ名がmain以外の場合、git push origin mainを適切なブランチ名に変更してください

こういう感じで設定しておくと1日に1回差分があるときだけ更新されます

今回作成にあたり大変参考にさせていただいた方々のGitHubになります!(ありがとうございます!!!!🥹)
mikkameさん

ikawahaさん

さいごに

今回は、RSSを使ってQiitaの記事を取得し、それをGitHubのプロフィールREADME.mdに表示する仕組みを作成しました。GitHub Actionsを利用した自動化も今回のポイントとなっています!🎉

13
8
1

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
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?