掲題を行うスクリプトを作成したのでお使いください。
使い方 #
go run main.go -u my-user-name -t ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- Go 言語で作成したため、
go run
で実行します。 -u
で自分の GitHub ユーザ名を、-t
で自分の GitHub アクセストークンを指定してください。- 使用するアクセストークンには
user:follow
の権限が付与されている必要があります。
コード(Go 言語) #
main.go
のファイルを作成し、そこにコピペしてください。なおファイル名は任意です。
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
)
const githubAPI = "https://api.github.com"
func main() {
// ---------------------------------------------------------------------------
// コマンドライン引数を受け取る
// ---------------------------------------------------------------------------
username := flag.String("u", "", "GitHub のユーザー名")
token := flag.String("t", "", "GitHub のアクセストークン")
flag.Parse()
if *username == "" || *token == "" {
fmt.Println("使い方: go run main.go -u <username> -t <token>")
os.Exit(1)
}
// ---------------------------------------------------------------------------
// フォロー・フォロワーを取得する
// ---------------------------------------------------------------------------
githubFollowingUrl := fmt.Sprintf("%s/users/%s/following", githubAPI, *username)
following, err := fetchUsers(githubFollowingUrl, *token)
if err != nil {
log.Fatalf("following の取得に失敗: %v", err)
}
githubFollowersUrl := fmt.Sprintf("%s/users/%s/followers", githubAPI, *username)
followers, err := fetchUsers(githubFollowersUrl, *token)
if err != nil {
log.Fatalf("followers の取得に失敗: %v", err)
}
// ---------------------------------------------------------------------------
// 片思いリストを作成する
// ---------------------------------------------------------------------------
onlyFollowing := []string{} // 自分はフォローしているが相手はしていない
onlyFollowers := []string{} // 相手はフォローしているが自分はしていない
followingSet := make(map[string]struct{})
for _, user := range following {
followingSet[user] = struct{}{}
}
followersSet := make(map[string]struct{})
for _, user := range followers {
followersSet[user] = struct{}{}
}
for _, user := range following {
if _, ok := followersSet[user]; !ok {
onlyFollowing = append(onlyFollowing, user)
}
}
for _, user := range followers {
if _, ok := followingSet[user]; !ok {
onlyFollowers = append(onlyFollowers, user)
}
}
// ---------------------------------------------------------------------------
// 出力する
// ---------------------------------------------------------------------------
fmt.Println("---------------------------------------------------------------")
fmt.Println("✅ 自分は相手をフォローしているが、その逆はないユーザー")
fmt.Println("---------------------------------------------------------------")
for _, user := range onlyFollowing {
fmt.Println(user)
}
fmt.Println("---------------------------------------------------------------")
fmt.Println("✅ 相手は自分をフォローしているが、その逆はないユーザー")
fmt.Println("---------------------------------------------------------------")
for _, user := range onlyFollowers {
fmt.Println(user)
}
}
func fetchUsers(url, token string) ([]string, error) {
var users []string
client := &http.Client{}
for {
// 新しいHTTPリクエストを作成
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// 認証ヘッダーを設定
req.Header.Set("Authorization", "token "+token)
req.Header.Set("Accept", "application/vnd.github+json")
// リクエストを送信
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// ステータスコードが 200 でなければエラー扱い
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("GitHub API エラー: %s", string(body))
}
// レスポンスボディをデコードしてユーザー名(Login)だけ取り出す
var data []struct {
Login string `json:"login"`
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return nil, err
}
// 取得したユーザー名をリストに追加
for _, user := range data {
users = append(users, user.Login)
}
// ページネーションがあるか確認(Link ヘッダーを見る)
linkHeader := resp.Header.Get("Link")
nextURL := parseNextURL(linkHeader)
if nextURL == "" {
break // 次ページがなければループを終了する
}
url = nextURL // 次のページの URL をセットしてループ継続する
}
return users, nil
}
func parseNextURL(linkHeader string) string {
if linkHeader == "" {
return ""
}
// Link ヘッダーをカンマ区切りで分割する
parts := strings.Split(linkHeader, ",")
for _, part := range parts {
// 各パートをセミコロンで分割して URL 部分と rel 部分に分ける
section := strings.Split(part, ";")
if len(section) < 2 {
continue
}
urlPart := strings.Trim(section[0], " <>") // URL 部分の <> を除去する
relPart := strings.Trim(section[1], " ") // rel 情報を取得する
// rel="next"(次ページリンク)ならその URL を返す
if relPart == `rel="next"` {
return urlPart
}
}
// 次ページが見つからない場合は空文字を返す
return ""
}
補足 #
最近流行りのバイブコーディングで作成しました。