Go

goqueryの使い方|title取得・文字化け対策まで実行済みコードで解説

goqueryは、Go言語(golang)でHTMLをCSSセレクタから解析するライブラリです。jQueryに似た記法で要素を選び、テキストや属性を取り出せます。2026年8月時点の最新版はv1.12.0(2026年3月15日公開)で、Go 1.25以上が必要です。この記事に載せたコードは Go 1.26.5 と goquery v1.12.0 で実行し、出力を確認したものです。紙面の都合で一部は抜粋しているため、動かす際はimport文の補完が必要になります。

まとめ:goquery導入と実装の要点

  • 導入は go get github.com/PuerkitoBio/goquery。v1.12.0はGo 1.25以上、v1.11.0はGo 1.24以上を要求します。
  • HTMLの取得は net/httpNewDocumentFromReader の組み合わせが公式の推奨形です。NewDocumentNewDocumentFromResponse はソース上で非推奨と明記されています。
  • titleは doc.Find("title").Text() で取れます。複数一致するセレクタで Text() を呼ぶと文字列が連結されるため、一覧の抽出は Each を使います。
  • Shift_JISやEUC-JPの日本語サイトは、charset.NewReader を挟まないと文字化けします。UTF-8のページでも透過するので、日本語サイトを相手にするなら常時挟んで構いません。
  • 複数ページをまとめて取るときは errgroupSetLimit で同時接続数を固定します。goroutineを無制限に起動する実装は採用しないでください。

以下、インストールの要件から順に、実行して出力を確認したコードで説明します。

goqueryのインストールとGoバージョン要件

go getによる導入とモジュールパスの表記

モジュールを初期化したうえで go get を実行します。依存はCSSセレクタを実装した cascadia と golang.org/x/net の2つ。v1.12.0では cascadia v1.3.3 と golang.org/x/net v0.52.0 が同時に入ります。

$ go mod init example.com/scraper
$ go get github.com/PuerkitoBio/goquery

go: downloading golang.org/x/net v0.52.0
go: added github.com/PuerkitoBio/goquery v1.12.0
go: added github.com/andybalholm/cascadia v1.3.3
go: added golang.org/x/net v0.52.0

import文のパスは github.com/PuerkitoBio/goquery で、作者名の P と B は大文字です。Goのモジュールパスは大文字小文字を区別するため、小文字の puerkitobio で書くとビルドが通りません。検索結果やドキュメントのURLが小文字で表示されていても、コードにはこの表記をそのまま書き写してください。

バージョン別に必要なGoの最低版

goqueryはメジャーな更新のたびに要求Goバージョンを引き上げています。CIのGoが古いまま最新版を入れるとビルドが落ちるため、先に対応表を確認します。

goqueryのバージョン 必要なGo 公開日
v1.12.0 1.25以上 2026-03-15
v1.11.0 1.24以上 2025-11-16
v1.10.3 1.23以上 2025-04-11

要求バージョンはリリースノートに明記されています。v1.12.0には「Note that Go1.25 is now required」、v1.11.0には「Note that Go1.24 is now required」との一文が入りました。表の値はいずれも各タグの go.mod にある go ディレクティブと一致します。Goを上げられない環境では、最新版を入れずにバージョンを固定してください。Go 1.24で止まっているなら go get github.com/PuerkitoBio/[email protected] と明示します。

net/httpとNewDocumentFromReaderによるHTML取得

NewDocumentが非推奨になった経緯と代替

URLを1つ渡すだけでドキュメントを返す goquery.NewDocument(url) は、v1.12.0のソース(type.go)で非推奨と明記されています。コメントの全文は「Deprecated: Use the net/http standard library package to make the request and validate the response before calling goquery.NewDocumentFromReader with the response’s body.」です。レスポンスを受け取る NewDocumentFromResponse も同じく非推奨扱いになりました。

理由は制御できる範囲の狭さにあります。NewDocument はUser-Agentもタイムアウトもcontextも指定できません。公式コメントが「リクエストを作り、レスポンスを検証してから」と書いているとおり、標準の net/http でリクエストを組み立て、ステータスを確かめてからボディを NewDocumentFromReader に渡す形が現在の正しい書き方です。タイムアウトとUser-Agentまで含めると次のようになります。

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/PuerkitoBio/goquery"
)

func main() {
	client := &http.Client{Timeout: 10 * time.Second}

	req, err := http.NewRequest(http.MethodGet, "https://example.com/", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("User-Agent", "example-scraper/1.0 (+https://example.com/)")

	res, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		log.Fatalf("status %d", res.StatusCode)
	}

	doc, err := goquery.NewDocumentFromReader(res.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(doc.Find("title").Text())
}

http.DefaultClienthttp.Get はタイムアウトが0、つまり無制限です。応答を返さないサーバーに当たるとgoroutineが解放されないため、クライアントは必ず自前で組み立ててください。User-Agentは連絡先を含む文字列にしておくと、相手のサーバー管理者が異常なアクセスに気づいたときに問い合わせ先が分かります。

ステータスコードとエラーの扱い方

上のコードで res.StatusCode を判定している点が要になります。NewDocumentFromReader はHTTPのステータスを一切見ません。404のエラーページを渡してもパースは成功し、err はnilのまま返ります。ステータス判定を省くと「エラーは出ていないのに抽出結果だけ空」という状態になり、原因の特定に時間を取られます。

セレクタ側の失敗も同じく静かです。手元の環境で確認したところ、存在しないセレクタ #nopeLength() が0、Text() が空文字を返すだけでpanicは起きません。閉じ括弧を落とした不正なセレクタ li[ も同様に0件を返します。セレクタのtypoは例外にならないため、抽出件数を Length() でログに出し、0件なら異常として扱う作りにしておくと早く気づけます。

title・テキスト・属性の取り出し

以降の例は、次のHTMLを取得した状態を前提にします。

<html>
<head><title>サンプル記事一覧 | demo</title></head>
<body>
<ul id="list">
  <li class="item"><a href="/a/1">記事A</a><span class="date">2026-08-01</span></li>
  <li class="item"><a href="/a/2">記事B</a><span class="date">2026-08-02</span></li>
  <li class="other"><a href="/a/3">除外</a></li>
</ul>
</body></html>

titleタグの取得とSingleによる走査打ち切り

ページタイトルは doc.Find("title").Text() で取得します。上のHTMLなら次の出力です。

fmt.Println(doc.Find("title").Text())
// => サンプル記事一覧 | demo

// 最初の1件で走査を打ち切る書き方
fmt.Println(doc.FindMatcher(goquery.Single("title")).Text())
// => サンプル記事一覧 | demo

titleのように文書内で1つしか存在しない要素なら、goquery.SingleFindMatcher の組み合わせが効きます。Find が一致する全ノードを集めてから返すのに対し、Single は最初の一致で走査を止めるためです。差は要素数に比例します。div を20万個並べたHTMLに対し、先頭付近で一致する div.x を引く処理をGo 1.26.5とgoquery v1.12.0で計測したところ、Find(...).Length() が1回あたり約72ミリ秒、FindMatcher(goquery.Single(...)).Length() が約1.5マイクロ秒でした。ただしこれは一致要素が先頭付近にある条件での値です。目的の要素が末尾にしか無ければ走査量は変わりません。1ページを1回解析するだけのスクリプトなら Find のままで構いません。

Findとセレクタによる要素の絞り込み

セレクタの解釈はcascadiaが担当し、クラス・id・子孫・属性といったCSSの記法がそのまま通ります。上のHTMLでは li.item が2件、li が3件という結果になりました。

doc.Find("li.item").Length() // => 2
doc.Find("li").Length()      // => 3

クラス名で絞り込めば不要な行を最初から除外できます。取得後にテキストで判定して弾く処理を書く前に、セレクタで絞れないかを先に検討してください。

Attrによる属性値の取得

リンク先URLや data-*Attr で取り出します。戻り値は値と存在フラグの2つです。

s := doc.Find("li.item").First()

href, ok := s.Find("a").Attr("href")
// href="/a/1", ok=true

_, ok = doc.Find("li.other a").Attr("data-id")
// ok=false(属性が無い場合)

// デフォルト値を与える書き方
id := doc.Find("li.other a").AttrOr("data-id", "unknown")
// id="unknown"

第2戻り値を _ で捨てると、属性が無い場合と空文字が入っている場合を区別できなくなります。属性の欠落をエラーとして扱いたい箇所では ok を必ず受け取り、欠けていても処理を続けたい箇所では AttrOr でデフォルトを与える、と使い分けます。

Eachによる複数要素のループ処理

一覧ページから行を抜き出す処理は Each で書きます。引数はインデックスと *goquery.Selection で、Selectionに対してさらに Find を重ねられます。

doc.Find("li.item").Each(func(i int, s *goquery.Selection) {
	href, _ := s.Find("a").Attr("href")
	fmt.Printf("%d: %s (%s) %s\n",
		i,
		strings.TrimSpace(s.Find("a").Text()),
		href,
		s.Find("span.date").Text(),
	)
})

// 0: 記事A (/a/1) 2026-08-01
// 1: 記事B (/a/2) 2026-08-02

ループを使わず doc.Find("p.a").Text() のように書くと、一致した全要素のテキストが区切りなしで連結されます。実際に2つの段落を持つHTMLで確認すると 1つ目2つ目 という1本の文字列が返りました。一覧を要素ごとに扱いたいなら Each、文字列スライスがほしいなら Map、条件を満たした時点で止めたいなら EachWithBreak を選びます。EachWithBreak はコールバックがfalseを返した時点で打ち切られ、2件ある要素でも実行回数は1回でした。

日本語サイトの文字化けとcharsetパッケージによる解決

goqueryはUTF-8のバイト列を前提に動きます。Shift_JISやEUC-JPで配信されているページをそのまま NewDocumentFromReader に渡すと、パースは成功するのに中身が壊れます。Shift_JISで「日本語」と書かれたtitleを変換せずに読ませたところ、返ってきたのは次の文字列でした。

// 変換なし
doc.Find("title").Text()
// => "\x93\xfa\x96{\x8c\xea"

// golang.org/x/net/html/charset を挟む
import "golang.org/x/net/html/charset"

r, err := charset.NewReader(res.Body, res.Header.Get("Content-Type"))
if err != nil {
	log.Fatal(err)
}
doc, err := goquery.NewDocumentFromReader(r)

doc.Find("title").Text()
// => "日本語"

エラーは出ません。ログ上は正常に完了し、保存されたデータだけが壊れます。これが日本語サイトのスクレイピングで最も見落とされやすい不具合です。

charset.NewReader は第2引数のContent-Typeヘッダと、HTML内の meta charset の両方を見て文字コードを判定します。UTF-8のページに対しては何もせず素通しするため、対象サイトの文字コードが混在していても分岐は要りません。官公庁サイトや長く運用されているECサイトを相手にするなら、判定処理を書かずに常時 charset.NewReader を挟む実装を推奨します。

ただし、ヘッダとmetaが食い違うときはヘッダが優先されます。中身がShift_JISでmetaも Shift_JIS と宣言しているのに、サーバーがContent-Typeで charset=utf-8 を返すページでは、ヘッダを渡すと化けたままでした。第2引数に空文字を渡すとmetaの宣言が使われ、正しく「日本語」を取得できます。ヘッダの宣言を信用できないサイトでは、意図的に空文字を渡してmetaに寄せてください。ヘッダもmetaも無いページでは推定に頼ることになり、この場合は誤判定が起きます。なお charset パッケージは golang.org/x/text に依存するため、go get golang.org/x/net に加えて go get golang.org/x/text も実行しておきます。

複数ページ取得時の同時接続数の制御

Goで並行処理を書くのは容易ですが、URLの数だけgoroutineを起動する実装は使ってはいけません。相手サーバーに同時数十本の接続が飛び、業務妨害と判断されるリスクがあります。同時接続数は明示的に固定します。

// 事前に go get golang.org/x/sync が必要
urls := []string{
	"https://example.com/1",
	"https://example.com/2",
	"https://example.com/3",
}
client := &http.Client{Timeout: 10 * time.Second}

g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(3) // 同時接続数の上限

titles := make([]string, len(urls))
for i, u := range urls {
	g.Go(func() error {
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
		if err != nil {
			return err
		}
		res, err := client.Do(req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		if res.StatusCode != http.StatusOK {
			return fmt.Errorf("%s: status %d", u, res.StatusCode)
		}
		doc, err := goquery.NewDocumentFromReader(res.Body)
		if err != nil {
			return err
		}
		titles[i] = doc.FindMatcher(goquery.Single("title")).Text()
		return nil
	})
}
if err := g.Wait(); err != nil {
	log.Println(err)
}

errgroup.WithContextSetLimit の組み合わせなら、上限の管理とエラー時の中断を両方まかせられます。どれか1本が失敗するとcontextがキャンセルされ、残りのリクエストも打ち切られる仕組みです。ここでもクライアントに Timeout を設定している点に注意してください。errgroup のcontextはエラーが起きたときにしかキャンセルされないため、無応答のサーバーが1本あるとタイムアウトなしでは待ち続けます。

効果は測っておく価値があります。1リクエストあたり50msの遅延を返すローカルのテストサーバーに対し12件のURLを処理したところ、SetLimit(3) で210ms、同じ12件を直列で回すと620msでした。サーバー側で計測した同時リクエスト数は上限の3を超えていません。相手先の規模が不明なうちは2〜3程度に抑え、レスポンスタイムの悪化が見えたら下げる運用が安全です。

Collyとの使い分けと実行前の確認事項

goqueryとCollyは競合しません。Colly v2.3.0のgo.modは goquery v1.11.0 を依存に持ち、HTMLの要素を表す htmlelement.go の内部で goquery を使っています。CollyはgoqueryをHTML解析エンジンとして内包したクローラのフレームワーク、という関係です。

判断の目安は単純です。取得するURLが手元にあり、リクエストの組み立てを自分で制御したいならgoqueryだけで足ります。リンクをたどる巡回、robots.txtの遵守、リクエスト間隔やキャッシュの管理といった要件が出てきた時点がCollyへの移行時期。Collyは temoto/robotstxt でrobots.txtを解釈し、IgnoreRobotsTxt オプションで挙動を切り替えられます。goquery単体にこの機能はありません。robots.txtの確認は自分で実装することになります。両者の実装を並べた比較はGoでスクレイピングする方法|goqueryとCollyの使い分けと実装で扱っています。

もう1点、どちらを選んでも解決しない領域があります。JavaScriptで描画されるページです。goqueryが解析するのはサーバーが返したHTMLそのもので、ブラウザのスクリプトは実行されません。script タグでDOMを書き換えるHTMLを読ませても、生成されるはずの要素は0件のままでした。この種のページはヘッドレスブラウザを使う必要があり、Seleniumの使い方|Selenium 4でWebDriverの手動設定が不要になった書き方で扱う手法が選択肢になります。取得先の利用規約や著作権法上の扱いについては、スクレイピングとは?クローリング・APIとの違いと実装・法務の判断を解説を実装前に確認してください。

よくある質問

goqueryでtitleタグを取得するにはどうすればよいですか?

doc.Find("title").Text() で取得できます。titleは文書内に1つしかない要素なので、doc.FindMatcher(goquery.Single("title")).Text() と書くと最初の一致で走査を打ち切れます。取得結果が空文字になる場合は、HTTPステータスが200以外である、対象がJavaScriptで描画されている、文字コードがUTF-8以外である、のいずれかを疑ってください。

goquery.NewDocumentは今も使えますか?

関数自体は残っており呼び出せますが、v1.12.0のソースで非推奨と明記されています。NewDocumentFromResponse も同様です。公式のコメントは net/http でリクエストを作り、レスポンスボディを NewDocumentFromReader に渡すよう案内しています。NewDocument はUser-Agentもタイムアウトもcontextも指定できないため、新規のコードで使う理由はありません。

goqueryでJavaScriptで描画されたページは取得できますか?

取得できません。goqueryはサーバーから返ってきたHTMLを golang.org/x/net/html でパースするだけで、スクリプトは実行しません。script タグで要素を挿入するHTMLを読ませても、その要素の件数は0のままです。SPAや遅延読み込みのページを扱う場合は、ヘッドレスブラウザを経由してレンダリング後のHTMLを取得し、その文字列を NewDocumentFromReader に渡す構成になります。

importするパスはPuerkitoBioとpuerkitobioのどちらですか?

大文字を含む github.com/PuerkitoBio/goquery が正しい表記です。Goのモジュールパスは大文字小文字を区別するため、小文字で書くとモジュールを解決できません。GitHubのURLやpkg.go.devの表示が小文字になっていても、import文とgo.modには大文字を含む表記を使ってください。

goqueryとCollyはどちらを選べばよいですか?

URLが決まっていて解析だけが目的ならgoquery、リンクをたどる巡回とrobots.txtの遵守、リクエスト間隔の制御が必要ならCollyです。Colly v2.3.0は内部でgoquery v1.11.0を使っているため、後からCollyへ移してもセレクタの書き方はほぼそのまま流用できます。最初はgoqueryで始め、巡回の要件が出た時点で移行する進め方で問題ありません。

関連記事

資料請求

RELATED POSTS 関連記事