0
0

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言語 SPIDERING メモ

Last updated at Posted at 2026-01-11

Go言語でのスパイダー/クロール

仕事でGo言語を使うことになったので少し調べた時のメモ。
こんなのが簡単に実装できる。 sokmil.com がターゲット。

ページ取得処理を作成

対象のURLのHTMLをパースできるようにページを取得する処理を作成します。
User-Agentとリファラを設定できるようにしておきます。

//共通設定
const (
	UA      = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
	AuthURL = "https://www.sokmil.com/member/ageauth/"
)

func doReq(cl *http.Client, url, ref string) *goquery.Document {
	// 新しいHTTP GETリクエストを作成
	req, _ := http.NewRequest("GET", url, nil)
	// ヘッダーにユーザーエージェントを設定
	req.Header.Set("User-Agent", UA)
	// もし参照元のURLが指定されている場合は、Refererヘッダーを設定
	if ref != "" {
		req.Header.Set("Referer", ref)
	}
	// リクエストを実行し、レスポンスを取得
	res, _ := cl.Do(req)
	// 関数終了時にレスポンスのボディを閉じるようにdeferを使う
	defer res.Body.Close()
	// レスポンスボディからgoquery用のドキュメントを作成
	doc, _ := goquery.NewDocumentFromReader(res.Body)
	// ドキュメントを返す
	return doc
}

年齢認証

まず年齢認証を突破する処理を作成します。
クッキーを設定して HTTPクライアントを作成します。

func getSession() (*http.Client, error) {
	// クッキージャーを新規に作成
	jar, _ := cookiejar.New(nil)
	// HTTPクライアントをクッキージャーと共に作成
	client := &http.Client{Jar: jar}

	// 認証ページ取得
	doc := doReq(client, AuthURL, "")

	// リンク抽出
	link, _ := doc.Find("a.btn-ageauth-yes").Attr("href")
	if link == "" {
		// リンクが見つからなかった場合はエラーを返す
		return nil, fmt.Errorf("認証ボタンが見つかりませんでした")
	}

	// 認証実行(Referer付き)
	doReq(client, link, AuthURL)

	// 認証完了したクライアントを返す
	return client, nil
}

商品詳細ページのURLを収集

商品詳細ページのURLは「https://www.sokmil.com/idol/_item/itemXXXXXX.htm」 なので
この形式のURLを収集する処理を作成

func getItemURLs(cl *http.Client, listURL string) []string {
	// HTTPリクエストを送信してレスポンスのHTMLドキュメントを取得
	doc := doReq(cl, listURL, "")
	// アイテムのURLを格納するスライスを宣言
	var urls []string

	// aタグのhref属性をすべてチェック
	doc.Find("a").Each(func(i int, s *goquery.Selection) {
		href, exists := s.Attr("href")
		// 条件: "/idol/_item/item" を含んでいるものだけを抽出
		if exists && strings.Contains(href, "/idol/_item/item") {
			// もし相対パス(/idol/..)なら絶対パスに変換
			if strings.HasPrefix(href, "/") {
				href = "https://www.sokmil.com" + href
			}
			// URLをスライスに追加
			urls = append(urls, href)
		}
	})

	// 重複を排除する(同じリンクが複数ある場合があるため)
	return unique(urls)
}

// 重複を消すための便利関数
func unique(slice []string) []string {
	m := make(map[string]bool)
	var result []string
	for _, s := range slice {
		if !m[s] {
			m[s] = true
			result = append(result, s)
		}
	}
	return result
}

商品詳細ページからサンプル動画をダウンロード

タイトルと動画URLを抽出し、ダウンロードする処理を作成
ファイル名は「作品名.mp4」で保存


func crawlAndDownload(cl *http.Client, pageURL string) {
	// 1. ページの中身を取得
	doc := doReq(cl, pageURL, "") // 以前作った共通関数を使用
	html, _ := doc.Html()

	// h1タグからタイトルを取得 
	title := doc.Find("h1").First().Text()
	title = strings.TrimSpace(title) // 前後の空白を消す

	// ファイル名に使えない禁止文字を置換する
	// WindowsやMacでトラブルになる / : * ? " < > | などを "_" に変える
	reg := regexp.MustCompile(`[\\/:*?"<>|]`)
	safeTitle := reg.ReplaceAllString(title, "_")

	// 正規表現で動画URL (video_url) を抜き出す
	re := regexp.MustCompile(`video_url:\s*'(https?://[^']+)'`)
	match := re.FindStringSubmatch(html)

	if len(match) < 2 {
		fmt.Printf("動画URLが見つかりませんでした: %s\n", pageURL)
		return
	}
	videoURL := match[1]

	// 保存先
	saveDir := "mp4"

	// mp4フォルダがなければ作成する (0755は一般的な権限設定)
	if _, err := os.Stat(saveDir); os.IsNotExist(err) {
		os.Mkdir(saveDir, 0755)
	}

	// タイトルに拡張子を付けて保存パスを作成
	fileName := safeTitle + ".mp4"
	savePath := filepath.Join(saveDir, fileName)

	// ダウンロード実行
	out, err := os.Create(savePath)
	if err != nil {
		return
	}
	defer out.Close()

	// HTTP GETリクエストを作成
	req, _ := http.NewRequest("GET", videoURL, nil)

	// リクエストヘッダーにUser-Agentを設定
	req.Header.Set("User-Agent", UA)

	// HTTPクライアント(cl)でリクエストを送信し、レスポンスを取得
	resp, err := cl.Do(req)
	if err != nil {
		// エラーが発生した場合は処理を中断
		return
	}
	// プログラムの終了時にレスポンスボディを必ず閉じるようにす
	defer resp.Body.Close()

	fmt.Printf("開始: %s\n", fileName)
	io.Copy(out, resp.Body)
	fmt.Printf("完了: %s\n", fileName)
}

実行部分

WaitGroupを使って動画のダウンロードを並列処理で実行
WaitGroupで覚えるべき操作は3つだけ

操作 意味 イメージ
wg.Add(1) カウンターを +1 する 「今から1人、現場に向かわせます!」と報告。
wg.Done() カウンターを -1 する 作業員が「仕事終わりました!」と無線で報告。
wg.Wait() カウンターが 0 になるまで待つ 「全員からの完了報告が届くまで、ここで待機する」

並列処理で行う処理を go func で定義

func main() {
    client, err := getSession()
    if err != nil {
        fmt.Println("エラー:", err)
        return
    }

    fmt.Println("✅ 認証完了。データを取得します...")

    // 一覧ページを解析するメッセージの表示
    fmt.Println("一覧ページを解析中...")
    // アイテムのURLを取得し、pageURLsに格納
    pageURLs := getItemURLs(client, "https://www.sokmil.com/idol/")
    // 見つかったアイテム数を出力
    fmt.Printf("%d 個のアイテムが見つかりました\n", len(pageURLs))

    // 並行処理で使用するWaitGroupの準備
    var wg sync.WaitGroup
    // 同時に実行するgoroutineの数を3つに制限
    limit := make(chan struct{}, 3)

    // ページURLごとに並行処理を設定
    for i, url := range pageURLs {
        wg.Add(1) // WaitGroupに1追加
        go func(target string, index int) {
            // goroutine終了時にWaitGroupから1減少
            defer wg.Done()

            limit <- struct{}{} // チャンネルに空構造体を送信(枠に入る)
            // 処理中のメッセージを出力
            fmt.Printf("[%d / %d] 処理中...\n", index+1, len(pageURLs))
            // ページを解析してダウンロード
            crawlAndDownload(client, target) // ページ解析〜保存まで一気に実行
            <-limit                          // チャンネルから値を取り出し(枠を空ける)
        }(url, i)
    }

    wg.Wait() // すべてのgoroutineが終了するのを待つ
    
    // 全ての処理完了のメッセージを表示
    fmt.Println("すべての処理が完了しました!")
}
Ask a question...

実行結果

いつものコマンドで実行

go run main.go 
go run main.go 
 認証完了。データを取得します...
一覧ページを解析中...
269 個のアイテムが見つかりました
[54 / 269] 処理中...
[36 / 269] 処理中...
[18 / 269] 処理中...
動画URLが見つかりませんでした: https://www.sokmil.com/idol/_item/item499694.htm?ref=top_sale
[45 / 269] 処理中...
開始: 月虹(げっこう) 宮越虹海.mp4
開始: 大好き、だよ。 藤乃ゆりあ.mp4
開始: 昼下がりの誘惑 遠野千夏.mp4
完了: 月虹(げっこう) 宮越虹海.mp4
[37 / 269] 処理中...

# 

[21 / 269] 処理中...
開始: 僕のあおい 藤乃あおい.mp4
動画URLが見つかりませんでした: https://www.sokmil.com/idol/_item/item334120.htm?ref=top_sale
[22 / 269] 処理中...
動画URLが見つかりませんでした: https://www.sokmil.com/idol/_item/item499692.htm?ref=top_sale
[27 / 269] 処理中...
完了: 真面目な大家さんが実はとってもエッチだった件! 藤田あずさ.mp4
[28 / 269] 処理中...
開始: imagine 井川遥.mp4
動画URLが見つかりませんでした: https://www.sokmil.com/idol/_item/item064664.htm?ref=top_sale
[29 / 269] 処理中...
動画URLが見つかりませんでした: https://www.sokmil.com/idol/_item/item499691.htm?ref=top_sale
[30 / 269] 処理中...
完了: 僕のあおい 藤乃あおい.mp4
[31 / 269] 処理中...

動画がダウンロードできていることを確認してください。
藤乃あおいさんのご冥福をお祈りいたします。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?