Go は web scraping の fetching(取得)の半分において、間違いなく最も優れた主流言語です。goroutine は数千の同時リクエストを何でもないことのように感じさせ、その結果は単一のバイナリにコンパイルされて、どんなサーバーにも放り込めます。parsing と anti-bot の半分は、Python がいまだに深いツールボックスを持っている領域です。このガイドは Go スクレイパーを誠実なやり方で組み立てます — まず標準ライブラリと goquery、次にクロールとレート制限のための Colly、そして自分の IP を BAN させないワーカープール — そして、DIY の Go スクレイピングがどこで機能しなくなり、API が引き継ぐのかを正確に伝えます。
なぜ Go で Web Scraping するのか — そしてなぜしないのか
Go を選ぶ理由は本物です。だからこそ公平に述べましょう。
生の速度と並行性。 スクレイピングは I/O バウンドな作業であり、goroutine はどの主流言語でも最も安価な並行性のプリミティブです — それぞれスタックは数キロバイト。200 の同時接続を飽和させる Go スクレイパーは午後のプロジェクトで済みます。Python での同等物は asyncio、非同期 HTTP クライアント、そして多くのチームが決して完全には身につけないメンタルモデルを意味します。
単一バイナリのデプロイ。 go build は 1 つの静的な実行ファイルを与えてくれます。virtualenv も、サーバー上での依存関係の解決も、「works on my machine」もありません。cron ジョブ、コンテナ、あるいは安価な VPS の上で暮らすスクレイパーにとって、これは本当に過小評価された利点です。
デフォルトで型付き出力。 構造体(struct)にパースするので、スキーマのずれはコンパイルエラーか、明らかに空のフィールドとして現れます — 実行から 3 日後に静かな KeyError として、ではなく。
さて、正直なデメリットを。Go のスクレイピング エコシステムはより小さい です。Python には BeautifulSoup、Scrapy、ステルスパッチ付きの Playwright、curl-impersonate のバインディング、そしてあらゆるエッジケースに対する 10 年分の Stack Overflow の回答があります。Go にあるのは goquery、Colly、chromedp — 良いツールですが、その数は少なく、anti-bot やステルスのライブラリ はさらにずっと少ないのです。加えて、より多くのボイラープレートを書くことになります。リクエストごとのエラー処理、あらゆる形に対する明示的な struct です。もしボトルネックがスループットではなく、雑然とした HTML のパースや洗練されたボット検知の回避であるなら、web scraping with Python のほうが寛容な出発点です — そのガイドは requests、BeautifulSoup、Scrapy で同じ旅路をたどります。
スループット、デプロイ、そして長時間稼働の信頼性こそが気になるなら、読み進めてください。
Level 1: net/http + goquery
標準ライブラリの net/http がページを取得し、goquery がパース済みの DOM に対して jQuery スタイルの CSS セレクタを与えてくれます。次でインストールします。
go get github.com/PuerkitoBio/goquery
以下は books.toscrape.com スタイルのカタログページ向けの完全なスクレイパーです — 取得し、ステータスコードを確認し、要素を選択し、struct に抽出し、JSON を出力します。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"github.com/PuerkitoBio/goquery"
)
type Book struct {
Title string `json:"title"`
Price string `json:"price"`
InStock bool `json:"in_stock"`
}
func main() {
res, err := http.Get("https://books.toscrape.com/")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
}
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
log.Fatal(err)
}
var books []Book
doc.Find("article.product_pod").Each(func(i int, s *goquery.Selection) {
books = append(books, Book{
Title: s.Find("h3 a").AttrOr("title", ""),
Price: strings.TrimSpace(s.Find(".price_color").Text()),
InStock: s.Find(".instock.availability").Length() > 0,
})
})
out, err := json.MarshalIndent(books, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
}
身につけるべきパターンはこうです。NewDocumentFromReader はレスポンスボディを一度だけパースし、Find は任意の CSS セレクタを取り、Each はマッチを反復し、AttrOr / Text は妥当なフォールバックとともに値を取り出します。パースの前に必ず res.StatusCode を確認してください — goquery はエラーページも平気で空の Selection にパースするので、幻のバグを追いかけることになります。本番向けの標準ライブラリの注意点をもう 2 つ。ゼロ値のデフォルト(決してタイムアウトしない)ではなく明示的な Timeout を持つ http.Client を使うこと、そして goquery は UTF-8 を期待するので、レガシーなエンコーディングは先に変換しておくことです。
このレベルは静的な HTML に対する一回限りのジョブに最適です。ページをまたいでリンクをたどる必要が出た瞬間、フレームワークが欲しくなります。
Level 2: Colly — Go のスクレイピングフレームワーク
Colly は fetch-parse-follow のループを、イベントコールバックを備えた collector にまとめます。Go にとって Scrapy に最も近い存在であり、web crawling — 1 ページをパースするだけでなくリンクをたどること — をほとんど儀式なしで扱います。v2 のモジュールパスに注意してください。
go get github.com/gocolly/colly/v2
同じ書店を、今度は「next」リンクを通じてすべてのカタログページをクロールしながら、丁寧な遅延を組み込んで。
package main
import (
"fmt"
"log"
"time"
"github.com/gocolly/colly/v2"
)
func main() {
c := colly.NewCollector(
colly.AllowedDomains("books.toscrape.com"),
)
// Politeness: delay between requests to this domain.
if err := c.Limit(&colly.LimitRule{
DomainGlob: "books.toscrape.com",
Delay: 500 * time.Millisecond,
RandomDelay: 500 * time.Millisecond,
}); err != nil {
log.Fatal(err)
}
c.OnHTML("article.product_pod", func(e *colly.HTMLElement) {
fmt.Printf("%s — %s\n",
e.ChildAttr("h3 a", "title"),
e.ChildText(".price_color"))
})
// Pagination: follow the next-page link until it stops existing.
c.OnHTML("li.next a", func(e *colly.HTMLElement) {
e.Request.Visit(e.Attr("href"))
})
c.OnError(func(r *colly.Response, err error) {
log.Println("request failed:", r.Request.URL, err)
})
if err := c.Visit("https://books.toscrape.com/"); err != nil {
log.Fatal(err)
}
}
3 つのアイデアがフレームワーク全体を支えます。OnHTML コールバック はセレクタにマッチするすべての要素に対して発火するので、抽出もリンクのたどりも、どちらも単なるコールバックです。ページネーション は e.Request.Visit(e.Attr("href")) です — Colly は相対 URL を解決し、訪問済みのページを重複排除し、AllowedDomains を尊重するので、うっかり広いインターネットをクロールしてしまうことはありません。LimitRule は、自分でタイミングのコードを書かずに、ドメインごとの Delay と RandomDelay のジッターを与えてくれます。
Level 3: 並行処理を正しく行う
ここは Go の読者が求めてやってきたセクションであり、そして大半の Go スクレイパーが誤るところでもあります。素朴な一手 — URL ごとに 1 つの goroutine を立てる — はデモでは動き、本番では溶け落ちます。数千の同時接続を開き、ターゲットサイトにレート制限されるか BAN され、自分自身のプロセスがファイルディスクリプタを使い果たします。並行処理には 2 つの上限が必要です。同時に何個のワーカーが走るか、そして 1 秒あたり何個のリクエストがプロセスを出ていくか です。(なぜそうなのかのサーバー側からの視点は what is rate limiting を参照してください。)
イディオム的な形は、チャネルから供給される上限付きのワーカープールで、グローバルなレート制限として働く共有の time.Ticker を伴います。
func scrapeAll(urls []string, workers, perSecond int) {
jobs := make(chan string)
ticker := time.NewTicker(time.Second / time.Duration(perSecond))
defer ticker.Stop()
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for u := range jobs {
<-ticker.C // one tick per request, shared by all workers
if err := scrapeOne(u); err != nil {
log.Println(u, err)
}
}
}()
}
for _, u := range urls {
jobs <- u
}
close(jobs)
wg.Wait()
}
10 ワーカー・毎秒 5 リクエストは、自分が管理していないサイトに対する妥当な出発点です。ticker は共有されるので、ワーカーを増やすとオーバーラップの許容度が上がり(遅いレスポンスがキューを止めない)、リクエストレートは上がりません — 2 つのつまみは独立したままで、これはレート制限に対してチューニングするときにまさに欲しいものです。
すでに Colly の中にいるなら、同じ挙動を宣言的に得られます — collector を colly.Async() で作り、LimitRule で Parallelism に上限を設け、Wait でブロックします。
c := colly.NewCollector(colly.Async())
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: 4,
Delay: 200 * time.Millisecond,
})
// ... register OnHTML callbacks, then:
c.Visit(startURL)
c.Wait() // block until all async requests finish
いずれにせよ、ルールは同じです。ワーカーを上限で抑え、リクエストを計量し、429 と 403 のレスポンスは、より強くリトライする合図ではなく、後退する合図として扱うことです。
JS の重いページ: chromedp はあるが、コストを天秤にかける
上で述べたことはすべて、データがサーバーの送る HTML の中にあることを前提としています。ますますそうではなくなっています — データはクライアント側で JavaScript によってレンダリングされ、net/http は空の殻を見ます。Go の答えは chromedp で、DevTools プロトコル越しに本物の Chrome インスタンスを操作します(what is a headless browser を参照)。
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var html string
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/js-rendered-listing"),
chromedp.WaitVisible(".product-grid"),
chromedp.OuterHTML("html", &html),
)
// feed html into goquery.NewDocumentFromReader as before
動きますし、API も心地よいものです。ですが、自分が何に申し込んだのか正直になりましょう。コンテキストごとにフルの Chrome(数百 MB の RAM)、これまで整然としていた単一バイナリのデプロイに紛れ込む Chrome のバイナリ、そして素の HTTP リクエストのおよそ 10–50x のレイテンシです。スループットは毎秒数百ページから一握りへと落ちます。特定の価値の高いターゲットが本当にレンダリングを必要とするときに chromedp を使ってください — デフォルトの経路としてではなく。多くの場合、より良い一手は DevTools を開いて、ページ自身が呼んでいる JSON エンドポイントを見つけることです。それを net/http でスクレイピングするほうが、レンダリングがこの先どれだけ速くなろうとも速いのです。
本番の壁 — そして API という橋
上記のいずれかを、実在の商用ターゲット — Amazon、Google、ソーシャルプラットフォーム — に対してスケールさせると、あなたのコードの品質とは何の関係もない壁にぶつかります。anti-bot システムです。データセンターの IP はフラグを立てられ、CAPTCHA が現れ、ヘッダーだけではあなたを救えません。
Go にはここで知っておく価値のある固有の問題があります。anti-bot ベンダーは TLS ハンドシェイク のフィンガープリントを取ります — クライアントが提示する暗号スイートと拡張を、JA3/JA4 シグネチャへとハッシュ化するのです — そして Go の crypto/tls スタックは、Chrome のものとは似ても似つかないハンドシェイクを生成します。保護されたサイトは、User-Agent をどれだけ入念に偽装しても、HTTP のバイトを 1 つも送る前に Go クライアントを識別できます。これは browser fingerprinting の一種であり、ブラウザの TLS を模倣するコミュニティのフォークは存在するものの、それらはまさに、Go のエコシステムが Python より少ししか持たない、ニッチで急速に朽ちていく依存関係の類です。立ち向かう相手の全体像については scraping sites that block bots を参照してください。
その時点での現実的な一手は、Go を得意なこと — オーケストレーション、並行性、パイプライン — のために残しつつ、敵対的な取得はスクレイピング API に委ねることです。Crawlora は素の HTTPS 越しに構造化エンドポイントを公開しているので、呼び出しはごく普通の net/http であり、レスポンスはあなたがパースして世話を焼かねばならない HTML ではなく、正規化された JSON です。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
)
type searchResponse struct {
Code int `json:"code"`
Data []struct {
ASIN string `json:"asin"`
Link string `json:"link"`
ListPrice float64 `json:"list_price"`
} `json:"data"`
}
func main() {
q := url.Values{}
q.Set("k", "mechanical keyboard")
q.Set("page", "1")
req, err := http.NewRequest(http.MethodGet,
"https://api.crawlora.net/api/v1/amazon/search?"+q.Encode(), nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("x-api-key", os.Getenv("CRAWLORA_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var sr searchResponse
if err := json.NewDecoder(res.Body).Decode(&sr); err != nil {
log.Fatal(err)
}
for _, item := range sr.Data {
fmt.Printf("%s $%.2f %s\n", item.ASIN, item.ListPrice, item.Link)
}
}
ローテーションするプロキシもなく、追いかける TLS 偽装のフォークもなく、Amazon がリデザインを出したときに直すパーサーもありません — ドキュメント化された JSON の上の型付き struct、まさに Go がそのために作られたワークフローです。プロキシ、レンダリング、リトライはエンドポイントの裏側で起こります。生の net/http より生成されたクライアントを好むなら、official Go SDK(TypeScript と Python と並んで)もあり、Level 3 の同じワーカープールのパターンがそのまま当てはまります — 生のページの代わりに API 呼び出しに向けるだけです。課金は pay-on-success です。成功した 2xx レスポンスにのみ課金されるので、失敗した取得は何のコストもかかりません(pricing)。ターゲットを自分でスクレイピングするか、そのために API を呼ぶかは判断の問題です — web scraping vs API がそれを一通り案内します。
- 静的な HTML、1 サイト、控えめなボリューム: net/http + goquery だけで足ります。
- ページネーションを伴うクロール: AllowedDomains と LimitRule(Delay + RandomDelay)を備えた Colly v2。
- 高ボリューム: 上限付きのワーカープール + 共有の time.Ticker、あるいは Parallelism を伴う colly.Async — 決して上限のない goroutine ではなく。
- 本物の User-Agent と http.Client のタイムアウトを設定し、429/403 では後退する。
- JS でレンダリングされるページ: chromedp に手を伸ばす前に、背後にある JSON エンドポイントを確認する。
- anti-bot の壁と TLS フィンガープリントによるブロック: そうしたターゲットはスクレイピング API 経由にし、Go のコードはオーケストレーションのために残す。
まとめ
Go は、Go が設計対象とした種類のシステムのように見えるスクレイパーに報います。並行的で、上限があり、型付きで、単一のバイナリとしてデプロイされたものに。net/http と goquery で始め、クロールが必要になったら Colly に進み、そしてすべての前に ticker 付きのワーカープールを置いてください。あるターゲットの防御が、データの価値以上のエンジニアリング時間を要するようになったら、同じコードベースから API へ橋渡ししましょう — playground では Go を 1 行も書かずにエンドポイントを試せますし、docs では全エンドポイントのカタログを見て回れます。
パイプラインには Go を残し、anti-bot の軍拡競争は省く
構造化エンドポイント、正規化された JSON、マネージドなプロキシとリトライ — 素の net/http か公式の Go SDK で呼び出せます。無料枠: 毎月 2,000 クレジット、カード不要。
関連する読み物: Web scraping with Python(より大きなエコシステムでの同じ旅路)· Scraping sites that block bots · Web scraping vs API · What is browser fingerprinting
よくある質問
Go は web scraping に向いていますか?
はい — Go はスクレイピングの取得(fetching)の半分で際立っています。goroutine は高並行なリクエストを安価にし、go build はどこにでもデプロイできる単一の静的バイナリを生成します。ただしエコシステムは Python より小さく、パーサーは少なく、anti-bot やステルスのライブラリははるかに少ないため、Go は洗練されたボット検知の回避よりもスループットとデプロイが重要な場面に最も適しています。
Colly と goquery — 何が違いますか?
goquery はパース用のライブラリです。net/http で既に取得した HTML に対して、jQuery スタイルの CSS セレクタ(Find、Each、Attr)を与えてくれます。Colly は goquery のセレクタエンジンの上に築かれた、完全なスクレイピングフレームワークです。取得ループ、OnHTML コールバック、リンクのたどりとページネーション、重複排除、そして LimitRule によるドメインごとの遅延と並列数を管理します。1 ページのジョブには goquery 単体を、クロールが必要なときには Colly を使いましょう。
スクレイピングにおいて Go は Python より速いですか?
ネットワークバウンドな部分については、規模が大きくなると明確に速いです。goroutine のおかげで、Go スクレイパーはわずかなメモリのオーバーヘッドで、しかも async フレームワークを学ぶことなく、数百の同時接続を保持できます。単一ページではネットワークが支配的で、言語はほとんど関係ありません。Python は asyncio でその差を縮めますが、Go の並行処理は正しく書くのがより簡単で、コンパイル済みバイナリはワーカーあたりのメモリ使用量がより少なくて済みます。
Go で JavaScript レンダリングのページをスクレイピングするには?
chromedp を使います。これは DevTools プロトコル越しに headless Chrome を操作します。Navigate、WaitVisible、そしてレンダリング済みの HTML を抽出して goquery でパースします。コストには注意してください — コンテキストごとにフルの Chrome、大きなメモリ使用、そして素の HTTP リクエストの 10–50x のレイテンシです。手を伸ばす前に、ページが呼んでいる JSON エンドポイントを DevTools で確認しましょう。それを net/http で直接取得するほうが、たいてい速いのです。
ブラウザの User-Agent を使っても、なぜサイトは Go スクレイパーをブロックするのですか?
anti-bot システムは TLS ハンドシェイクのフィンガープリント(JA3/JA4 シグネチャ)を取ります。そして Go の crypto/tls スタックは、Chrome のものとは似ても似つかないハンドシェイクを生成します — そのため保護されたサイトは、HTTP ヘッダーが 1 つ送られる前に Go クライアントを識別できます。User-Agent を偽装しても TLS フィンガープリントは変わりません。TLS を模倣するコミュニティのフォークは存在しますが、すぐに朽ちてしまいます。手厚く保護されたターゲットには、取得をスクレイピング API 経由にするほうが信頼できます。
並行動作する Go スクレイパーをレート制限するには?
2 つを独立して制限します。ワーカー数(jobs チャネルから読む固定の goroutine プール)と、リクエストレート(各ワーカーがリクエストを撃つ前に読み取る共有の time.Ticker)です。Colly では、宣言的な同等物は colly.Async() に Parallelism と Delay を備えた LimitRule を加え、そして c.Wait() です。429 と 403 のレスポンスは、より強くリトライする合図ではなく、後退する合図として扱いましょう。
Crawlora に Go SDK はありますか?
はい — Crawlora は TypeScript と Python の SDK と並んで、公式の Go SDK モジュールを公開しています。そしてすべてのエンドポイントは素の net/http でも動きます。https://api.crawlora.net/api/v1 のドキュメント化されたパスに、x-api-key ヘッダーへ入れたキーとともに GET を送り、正規化された JSON を struct にデコードするだけです。無料枠は毎月 2,000 クレジット、カード不要で、課金は pay-on-success — 成功した 2xx レスポンスにのみ課金されます。
