テキストから HTML タグを除去する
たとえば
This is a <strong>sample</strong> text
with <a href="#">HTML</a> tags.
というテキストデータが与えられた場合に <strong> や <a> といった HTML タグを除去することを考える。
正規表現でタグを除去する
単純に <...> の文字列パターンを除去できればいいのであれば正規表現が使える。
こんな感じ。
package striptag
import (
"html"
"regexp"
)
// StripTagsRegexp removes all HTML tags from the input string and unescapes any HTML entities.
func StripTagsRegexp(s string) string {
re := regexp.MustCompile(`<[^>]*>`)
return html.UnescapeString(re.ReplaceAllString(s, ""))
}
実際に動かしてみよう。 入力テキストを
package example
const (
Text1 = `This is a <strong>sample</strong> text
with <a href="#">HTML</a> tags.`
)
と定義しておいて,次のように main() 関数を定義する。
//go:build ignore
package main
import (
"striptag"
"striptag/example"
)
func main() {
println(striptag.StripTagsRegexp(example.Text1))
}
これを実行すると
$ go run sample1a.go
This is a sample text
with HTML tags.
と出力される。 ここまでは問題ない。 問題は入力テキストに異物が混ざってた場合。 たとえば
const (
Text2 = `This is a <strong>sample</strong> text
with <a href="#">HTML</a> tags.<script>alert("XSS")</script>`
)
のような感じ(末尾に <script> タグがある)。
これを StripTagsRegexp() 関数で処理すると
This is a sample text
with HTML tags.alert("XSS")
のように <script> タグの中身が露出してしまう。
Tokenizer を使ってタグを除去する
これを正規表現で対処するのは(やれないことはないだろうが)かなり面倒な気がするので,やり方を変えて golang.org/x/net/html パッケージの Tokenizer を使うことにする。
こんな感じかな。
package striptag
import (
"errors"
"html"
"io"
"strings"
ghtml "golang.org/x/net/html"
)
// skipTags defines the HTML tags whose content should be skipped when stripping tags.
var skipTags = map[string]bool{
"script": true,
"style": true,
"noscript": true,
"iframe": true,
"object": true,
"embed": true,
"textarea": true,
"title": true,
}
// StripTagsTokenizer removes HTML tags from the input string while skipping
// the content of certain tags defined in skipTags.
func StripTagsTokenizer(s string) (string, error) {
tokenizer := ghtml.NewTokenizer(strings.NewReader(s))
var b strings.Builder
b.Grow(len(s))
skipDepth := 0
for {
tt := tokenizer.Next() // get the next token type
switch tt {
case ghtml.ErrorToken: // handle error token
if errors.Is(tokenizer.Err(), io.EOF) {
return b.String(), nil // return the accumulated text at the end of input
}
return "", tokenizer.Err()
case ghtml.StartTagToken: // handle start tag token
t := tokenizer.Token()
if skipTags[t.Data] {
skipDepth++ // start skipping content of this tag
}
case ghtml.EndTagToken: // handle end tag token
t := tokenizer.Token()
if skipDepth > 0 && skipTags[t.Data] {
skipDepth-- // stop skipping one level of skipped content
}
case ghtml.TextToken: // handle text token
if skipDepth == 0 {
b.WriteString(html.UnescapeString(string(tokenizer.Text())))
}
}
}
}
これで <script> ... </script> の中身をスキップできる筈。
試してみよう。
main() 関数を
//go:build ignore
package main
import (
"fmt"
"os"
"striptag"
"striptag/example"
)
func main() {
out, err := striptag.StripTagsTokenizer(example.Text2)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
fmt.Println(out)
}
と書き直して実行する。
$ go run sample2b.go
This is a sample text
with HTML tags.
おー。 上手く行ったかな。
改行タグを改行コードに変換する
応用として,前節の StripTagsTokenizer() 関数に <br> タグを改行コードに変換する機能を追加する。
// changeBrToNewline handles the <br> tag by converting it to a newline character in the output.
func changeBrToNewline(t ghtml.Token, b *strings.Builder) {
if t.Data == "br" {
// Handle <br> as a line break.
b.WriteByte('\n')
}
}
// StripTagsTokenizer removes HTML tags from the input string while skipping
// the content of certain tags defined in skipTags.
func StripTagsTokenizer(s string) (string, error) {
tokenizer := ghtml.NewTokenizer(strings.NewReader(s))
var b strings.Builder
b.Grow(len(s))
skipDepth := 0
for {
tt := tokenizer.Next() // get the next token type
switch tt {
case ghtml.ErrorToken: // handle error token
if errors.Is(tokenizer.Err(), io.EOF) {
return b.String(), nil // return the accumulated text at the end of input
}
return "", tokenizer.Err()
case ghtml.StartTagToken: // handle start tag token
t := tokenizer.Token()
if skipTags[t.Data] {
skipDepth++ // start skipping content of this tag
}
if skipDepth == 0 {
changeBrToNewline(t, &b)
}
case ghtml.SelfClosingTagToken: // handle self-closing tag token
t := tokenizer.Token()
if skipDepth == 0 {
changeBrToNewline(t, &b)
}
case ghtml.EndTagToken: // handle end tag token
t := tokenizer.Token()
if skipDepth > 0 && skipTags[t.Data] {
skipDepth-- // stop skipping one level of skipped content
}
case ghtml.TextToken: // handle text token
if skipDepth == 0 {
b.WriteString(html.UnescapeString(string(tokenizer.Text())))
}
}
}
}
修正した StripTagsTokenizer() 関数を使って以下のテキストを処理する。
const (
Text3 = `This is a <strong>sample</strong> text<br>with <a href="#">HTML</a> tags.<script>alert("XSS")</script>`
)
処理結果はこんな感じ。
This is a sample text
with HTML tags.
text<br>with の <br> タグは改行に変換されていることが確認できた。
ここまできたら <p> のようなブロック要素にも対応したいところだけど,実際にブロック要素か否かはスタイル情報に依存するため,今回は割愛する。
参考図書
- プログラミング言語Go - 丸善出版 理工・医学・人文社会科学の専門書出版社
- 明解で効率的なプログラムを描くための書。 柴田 芳樹 訳
- Release 2016-06-15
- 評価
[Comment] 著者のひとりは,あの “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。と思ったら絶版状態らしい(2025-01 現在)。復刊を望む!
Powered by linkcard
- Go言語 100Tips ありがちなミスを把握し、実装を最適化する impress top gearシリーズ
- Teiva Harsanyi (著), 柴田 芳樹 (著)
- インプレス 2023-08-18 (Release 2023-08-18)
- Kindle版
- B0CFL1DK8Q (ASIN)
- 評価
[Comment] 版元で PDF 版を購入可能。事実上の Effective Go とも言える充実の内容。オリジナルは敢えてタイトルに “tips” という単語を入れるのを避けたのに邦題が「100 Tips」とかなっていて,原作者がお怒りとの噂(あくまで噂)
- Go言語で学ぶ並行プログラミング 他言語にも適用できる原則とベストプラクティス impress top gearシリーズ
- James Cutajar (著), 柴田 芳樹 (著)
- インプレス 2024-12-04 (Release 2024-12-04)
- Kindle版
- B0DNYMMBBQ (ASIN)
- 評価
[Comment] 読書会のために購入。インプレス社の本は Kindle 版より版元で PDF 版を買うのがオススメ。「並行処理」について原理的な解説から丁寧に書かれている。 Go で解説されているが Go 以外の言語でも応用できる。

