コース目次 / 第3章

成果物 — リンクの生死を調べる道具

URLの一覧を読んで、生きているかどうかをまとめて調べるCLIを作ります。

第3章 / 全5章目安 約60分この章のゴール: urls.txt に並べたURLの生死が一覧で表示される
お使いのエディタ
ファイル操作が切り替わります

道具を作ります。URLの一覧を読んで、それぞれが生きているか調べるCLIです。ブックマークやREADMEのリンク切れ点検に、そのまま使えます。

今日のゴール

こう動く
OK  https://www.python.org/  (200)
OK  https://go.dev/  (200)
OK  https://tsumiki-key.pages.dev/  (200)
NG  https://example.com/kono-page-wa-nai  (404)

4件を 431ms で調べました

1. 調べたいURLの一覧を作る

urls.txt を作ります。

urls.txt
# 1行に1つURLを書く。# で始まる行は飛ばす。
https://www.python.org/
https://go.dev/
https://tsumiki-key.pages.dev/
https://example.com/kono-page-wa-nai

2. ミニ解説: 今日使う道具

書き方 何をするか
os.Open ファイルを開く。失敗するかもしれないので err も返る
defer f.Close() 「この関数が終わるとき閉じる」を先に予約する
bufio.Scanner 1行ずつ読む
http.Client + Get URLを見に行く
res.StatusCode 200なら生きている、404なら無い

defer はGoらしい仕組みです。「あとで必ずやること」を、忘れないうちに先に書いておけます。ファイルを開いた直後に閉じる予約を書く——この並びに慣れると、閉じ忘れが起きなくなります。

そして、試食で予告した if err != nil が本当にたくさん出てきます。Goの挨拶です。

3. 書く

hello.go は役目を終えたので削除して、main.go を作ります。

ターミナル派の方

ターミナル
rm hello.gonvim main.go

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

rmPowerShell でもそのまま使えます(Remove-Item の別名です)。

main.go
// urls.txt に並べたURLを1つずつ見に行って、生きているかどうかを表示する。
package main

import (
	"bufio"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"
)

// 待ち続けないように、1件あたりの上限を決めておく。
var client = &http.Client{Timeout: 10 * time.Second}

// readURLs は、ファイルからURLを1行ずつ読む。空行と # で始まる行は飛ばす。
func readURLs(path string) ([]string, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var urls []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		urls = append(urls, line)
	}
	return urls, scanner.Err()
}

// check は1件のURLを見に行って、表示する1行を作る。
func check(url string) string {
	res, err := client.Get(url)
	if err != nil {
		return fmt.Sprintf("NG  %s  (%v)", url, err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		return fmt.Sprintf("NG  %s  (%d)", url, res.StatusCode)
	}
	return fmt.Sprintf("OK  %s  (%d)", url, res.StatusCode)
}

func main() {
	path := "urls.txt"
	if len(os.Args) > 1 {
		path = os.Args[1]
	}

	urls, err := readURLs(path)
	if err != nil {
		fmt.Println("URLの一覧が読めませんでした:", err)
		os.Exit(1)
	}

	start := time.Now()
	for _, url := range urls {
		fmt.Println(check(url))
	}
	fmt.Printf("\n%d件を %v で調べました\n", len(urls), time.Since(start).Round(time.Millisecond))
}

4. 走らせる

VS Codeのターミナルターミナル
go run .

行頭の $ は「ここからコマンド」という目印です。$ は打たないでください。

一覧が出れば成功です。最後の1件は NG になります。わざと存在しないURLを入れてあるからです。

いま気づいてほしいこと: 4件を調べるのに、それなりに待ちましたね。1件ずつ順番に見に行っているからです。10件なら10倍待ちます。
これを次の章で直します。Goがいちばん得意なやり方で。

よそのサイトを見に行く道具だ、ということを忘れないでください。
相手のサーバに毎回リクエストを送っています。同じサイトのURLを何十件も並べる(たとえば1つのサイトのページ一覧をまるごと貼る)と、相手にとっては短時間の集中アクセスになり、迷惑をかけたり、こちら側が一時的に締め出されたりします。
他人のサイトに向けるときは、件数を控えめに。次の章で、同時に投げる数を絞る方法も紹介します。

あなたの番

urls.txt自分がよく見るサイトに書き換えて、もう一度実行してください。ブックマークから5つほど貼るだけです。まずは5〜10件くらいから。

ハマったら

会社や学校のネットワークで全部NGになる

プロキシ(社内ネットの関所)に阻まれている可能性が高いです。URLを http://localhost:... のような手元のものに変えるか、個人のネットワークで試してください。プログラムは正しく動いています。

NG に「no such host」と出る

そのURLのドメイン自体が見つかっていません。綴りを確認してください。これも道具としては正しい動作で、リンク切れを見つけたということです。

ZIPで戻したい

このページの下のZIPは、この章を終えた状態のフォルダです。解凍して、その中で作業を続けられます。自分で書き換えた部分は消えてしまうので、先に自分のファイルを別の場所へ待避してから使ってください。

こうなっていればOK

うまくいかないときは、この章の完成ファイル一式(ZIP)をダウンロードして続きから進めても大丈夫です。

卒業まであと1章です。

この章はまだ完了していません。