業務中にGo言語を使ってみた (Go言語で特定のカテゴリを含むURLをXMLファイルから抽出し、新しいXMLファイルに書き込む方法)

## はじめに
エンジニアアルバイトをしている大学生です。先日、業務中に学習中のGo言語を使用する機会がありましたのでその時の使用用途等をアウトプットの為に投稿します。

## 背景
業務中に社員の方から、XMLファイルから特定のカテゴリを含むURLを抽出し、新しいXMLファイルを用意しておいて欲しいというご依頼を受けて、せっかくなので学習中のGo言語で解決しようと思いました！

## 問題設定
以下のような構造を持つXMLファイルがあります。(実際のXMLファイルは17000行程ありました。)

```diff_xml
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/money/article1</loc></url>
  <url><loc>https://example.com/other/article2</loc></url>
  <url><loc>https://example.com/money/article3</loc></url>
  <url><loc>https://example.com/land/article4</loc></url>
</urlset>
```

上記のようなXMLファイルから、/money/ 、/land/等（この記事内ではデモとして指定しています）毎の新しいXMLファイルを作成してその中に書き込むことが目的です。
つまり/money/を含むURLだけをまとめたXMLファイルや/land/を含むURLだけをまとめたXMLファイルを新たに作成するということです。

## 解決策
以下のGo言語のコードが、目的を達成するための解決策です。

```diff_go
package main

import (
	"encoding/xml"
	"fmt"
	"os"
	"strings"
)

/*
   以下の構造体は、XMLファイルのデータをGoの構造体に変換するために定義している。
   XMLファイルの構造をGoの構造体にマッピングすることで、Go言語でXMLデータを簡単に操作できる。
*/

// UrlSet構造体は、XMLファイルの<urlset>要素に対応
type UrlSet struct {
	XMLName xml.Name `xml:"urlset"`
	Urls    []Url    `xml:"url"`
}

// Url構造体は、XMLファイルの<url>要素に対応
type Url struct {
	XMLName xml.Name `xml:"url"`
	Loc     string   `xml:"loc"`
}

func main() {
	inputFile := "all.xml"                      // 入力ファイル名
	categories := []string{"money", "jewelry", "car", "house", "land"} // 抽出するカテゴリ
	generateXMLFiles(inputFile, categories)      // カテゴリごとの.xmlファイルを生成する関数を呼び出す
}

// generateXMLFiles は、指定された入力ファイルとカテゴリのスライスを受け取り、各カテゴリの.xmlファイルを生成
func generateXMLFiles(inputFile string, categories []string) {
	categoryUrls := make(map[string][]Url)       // カテゴリごとにURLを格納するマップ

	xmlFile, err := os.Open(inputFile)           // 入力ファイルを開く
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer xmlFile.Close()

	decoder := xml.NewDecoder(xmlFile)           // XMLデコーダーを作成

	// 入力ファイルを読み込み、カテゴリごとのURLをcategoryUrlsに格納する
	for {
		token, err := decoder.Token()             // XMLトークンを取得
		if err != nil {
			break
		}

		switch elem := token.(type) {
		case xml.StartElement:
			if elem.Name.Local == "url" {         // url要素が見つかった場合
				var url Url
				err := decoder.DecodeElement(&url, &elem)  // url要素をデコードしてUrl構造体に格納
				if err != nil {
					fmt.Println("Error decoding element:", err)
					return
				}

				// カテゴリごとにURLを分類してcategoryUrlsに追加
				for _, category := range categories {
					if strings.Contains(url.Loc, "/"+category+"/") {
						categoryUrls[category] = append(categoryUrls[category], url)
					}
				}
			}
		}
	}

	// カテゴリごとの.xmlファイルを生成
	for _, category := range categories {
		outputFile := category + ".xml"           // 出力ファイル名
		newUrlSet := UrlSet{Urls: categoryUrls[category]}  // 新しいUrlSetを作成
		newXmlData, err := xml.MarshalIndent(newUrlSet, "", "  ")  // UrlSetを整形してXMLデータに変換
		if err != nil {
			fmt.Println("Error marshalling XML:", err)
			return
		}

		newXmlFile, err := os.Create(outputFile)  // 出力ファイルを作成
		if err != nil {
			fmt.Println("Error creating file:", err)
			return
		}
		defer newXmlFile.Close()

		newXmlFile.WriteString(xml.Header)         // 出力ファイルにXMLヘッダーを書き込む
		newXmlFile.Write(newXmlData)               // 出力ファイルにXMLデータを書き込む
	}
}
```

最後に以下コマンドを叩けば各カテゴリのXMLファイルが生成されます。(main.goファイルに上記コードを記述した場合)

```
    username@usernamenoMacBook-Pro create-new-xmlfile % go run main.go
```

## 処理の流れ

これから、各処理を順を追って解説したいと思います。誤りがありましたら指摘していただきたいです。

```diff_go
// UrlSet構造体は、XMLファイルの<urlset>要素に対応
type UrlSet struct {
	XMLName xml.Name `xml:"urlset"`
	Urls    []Url    `xml:"url"`
}

// Url構造体は、XMLファイルの<url>要素に対応
type Url struct {
	XMLName xml.Name `xml:"url"`
	Loc     string   `xml:"loc"`
}
```

上記の構造体は、XMLファイルのデータをGoの構造体に変換するために定義されています。XMLファイルの構造をGoの構造体にマッピングすることで、Go言語でXMLデータを簡単に操作できます。

`UrlSet`構造体は、XMLファイルのurlset要素に対応します。構造体のフィールドについて説明します。

* `XMLName`はurlset要素の名前情報を格納します。タグ名はurlsetであることを示すために、xml:"urlset"というタグが付けられています。

* `Urls`はurl要素の配列です。url要素は、urlsetタグ内に複数存在するので、スライス型（[]Url）が使用されています。url要素に対応するために、xml:"url"というタグが付けられています。

`Url`構造体は、XMLファイルのurl要素に対応します。構造体のフィールドについて説明します。

* `XMLName`はurl要素の名前情報を格納します。タグ名はurlであることを示すために、xml:"url"というタグが付けられています。

* `Loc`はloc要素のテキスト情報を格納します。loc要素は、url内に存在するため、string型が使用されています。loc要素に対応するために、xml:"loc"というタグが付けられています。

```diff_go
func main() {
    inputFile := "all.xml" // 入力ファイル名
    categories := []string{"money", "jewelry", "car", "house", "land"} // 抽出するカテゴリ
    generateXMLFiles(inputFile, categories) // カテゴリごとの.xmlファイルを生成する関数を呼び出す
}
```

上記の`main`関数ではinputFileに対象のXMLファイル名を設定し、抽出したいカテゴリをcategoriesスライスに設定します。
そして、generateXMLFiles関数に引数としてinputFileとcategoriesを渡して、新しいXMLファイルを生成します。

```diff_go
func generateXMLFiles(inputFile string, categories []string) {
	categoryUrls := make(map[string][]Url)       // カテゴリごとにURLを格納するマップ

	xmlFile, err := os.Open(inputFile)           // 入力ファイルを開く
	if err != nil {
		fmt.Println("Error opening file:", err)
		return
	}
	defer xmlFile.Close()

	decoder := xml.NewDecoder(xmlFile)           // XMLデコーダーを作成

	// 入力ファイルを読み込み、カテゴリごとのURLをcategoryUrlsに格納する
	for {
		token, err := decoder.Token()             // XMLトークンを取得
		if err != nil {
			break
		}

		switch elem := token.(type) {
		case xml.StartElement:
			if elem.Name.Local == "url" {         // url要素が見つかった場合
				var url Url
				err := decoder.DecodeElement(&url, &elem)  // url要素をデコードしてUrl構造体に格納
				if err != nil {
					fmt.Println("Error decoding element:", err)
					return
				}

				// カテゴリごとにURLを分類してcategoryUrlsに追加
				for _, category := range categories {
					if strings.Contains(url.Loc, "/"+category+"/") {
						categoryUrls[category] = append(categoryUrls[category], url)
					}
				}
			}
		}
	}

	// カテゴリごとの.xmlファイルを生成
	for _, category := range categories {
		outputFile := category + ".xml"           // 出力ファイル名
		newUrlSet := UrlSet{Urls: categoryUrls[category]}  // 新しいUrlSetを作成
		newXmlData, err := xml.MarshalIndent(newUrlSet, "", "  ")  // UrlSetを整形してXMLデータに変換
		if err != nil {
			fmt.Println("Error marshalling XML:", err)
			return
		}

		newXmlFile, err := os.Create(outputFile)  // 出力ファイルを作成
		if err != nil {
			fmt.Println("Error creating file:", err)
			return
		}
		defer newXmlFile.Close()

		newXmlFile.WriteString(xml.Header)         // 出力ファイルにXMLヘッダーを書き込む
		newXmlFile.Write(newXmlData)               // 出力ファイルにXMLデータを書き込む
	}
}
```

上記の`generateXMLFiles` 関数は、指定されたXMLファイルから、指定されたカテゴリを含むURLを抽出し、新しいXMLファイルに書き込む処理を行います。以下に、関数内の処理を順に説明します。

`categoryUrls`はカテゴリごとのURLを格納するマップを作成します。キーはカテゴリ名で、値はそのカテゴリに含まれるURLのリストです。以下はcategoryUrlsのイメージです。

```diff_go
categoryUrls := map[string][]Url{
    "money": {
        {XMLName: xml.Name{Local: "url"}, Loc: "https://example.com/money/article1"},
        // ... 他の/money/ を含むURL
    },
    "jewelry": {
        {XMLName: xml.Name{Local: "url"}, Loc: "https://example.com/jewelry/article4"},
        // ... 他の/jewelry/ を含むURL
    },
    "car": {
        {XMLName: xml.Name{Local: "url"}, Loc: "https://example.com/car/article5"},
        // ... 他の/car/ を含むURL
    },
    "house": {
        {XMLName: xml.Name{Local: "url"}, Loc: "https://example.com/house/article6"},
        // ... 他の/house/ を含むURL
    },
    "land": {
        {XMLName: xml.Name{Local: "url"}, Loc: "https://example.com/land/article7"},
        // ... 他の/land/ を含むURL
    },
}
```

入力ファイルを開きます。エラーが発生した場合、エラーメッセージを表示し、処理を終了します。関数の終了時に、入力ファイルを閉じるように指定します。以下がそれらの処理です。

```diff_go
xmlFile, err := os.Open(inputFile)           // 入力ファイルを開く
if err != nil {
	fmt.Println("Error opening file:", err)
	return
}
defer xmlFile.Close()
```

以下はXMLファイルのデコーダを作成します。

```diff_go
decoder := xml.NewDecoder(xmlFile)           // XMLデコーダーを作成
```

以下のループ処理で、XMLファイル内のトークンを逐次的に処理します。ここでのトークンは開始タグ、終了タグ、属性、テキストなどを指します。

```diff_go
// 入力ファイルを読み込み、カテゴリごとのURLをcategoryUrlsに格納する
for {
    	token, err := decoder.Token()
		if err != nil {
			break
		}

		switch elem := token.(type) {
		case xml.StartElement:
			if elem.Name.Local == "url" {
				var url Url
				err := decoder.DecodeElement(&url, &elem)
				if err != nil {
					fmt.Println("Error decoding element:", err)
					return
				}

				for _, category := range categories {
					if strings.Contains(url.Loc, "/"+category+"/") {
						categoryUrls[category] = append(categoryUrls[category], url)
					}
				}
			}
		}
}
```

上記のループ処理を分けて解説していきます。

下記はデコーダーから次のトークンを取得しています。トークンがなくなるか、エラーが発生した場合、ループを抜けます。

```diff_go
token, err := decoder.Token()
if err != nil {
	break
}
```

下記はトークンの型に応じた処理を行っています。トークンが開始タグの場合、次の処理を実行できます。

```diff_go
switch elem := token.(type) {
		case xml.StartElement:
// ... 他の処理
}
```

タグ名が "url" の場合、URLを格納するためのUrl構造体の変数を作成し開始タグをデコードし、Url構造体に格納します。

次にカテゴリのリストをループ処理し、URLが指定されたカテゴリを含む場合、`categoryUrls` マップに追加します。

下記がそれらに関するコードです。

```diff_go
if elem.Name.Local == "url" { // url要素が見つかった場合
				var url Url
				err := decoder.DecodeElement(&url, &elem) // url要素をデコードしてUrl構造体に格納
				if err != nil {
					fmt.Println("Error decoding element:", err)
					return
				}

                // カテゴリごとにURLを分類してcategoryUrlsに追加
				for _, category := range categories {
					if strings.Contains(url.Loc, "/"+category+"/") {
						categoryUrls[category] = append(categoryUrls[category], url)
					}
				}
			}
```

カテゴリのリストをループ処理し、以下の処理を実行します。

```diff_go
// カテゴリごとの.xmlファイルを生成
for _, category := range categories {
		outputFile := category + ".xml" // 出力ファイル名
		newUrlSet := UrlSet{Urls: categoryUrls[category]} // 新しいUrlSetを作成
		newXmlData, err := xml.MarshalIndent(newUrlSet, "", "  ") // UrlSetを整形してXMLデータに変換
		if err != nil {
			fmt.Println("Error marshalling XML:", err)
			return
		}

		newXmlFile, err := os.Create(outputFile) // 出力ファイルを作成
		if err != nil {
			fmt.Println("Error creating file:", err)
			return
		}
		defer newXmlFile.Close()

		newXmlFile.WriteString(xml.Header) // 出力ファイルにXMLヘッダーを書き込む
		newXmlFile.Write(newXmlData) // 出力ファイルにXMLデータを書き込む
}
```

上記のループ処理を分けて解説していきます。

まず、カテゴリごとの出力ファイル名を設定し、カテゴリごとのURLリストを含む新しいUrlSet構造体を作成します。

その次に、`xml.MarshalIndent` 関数を使って、UrlSet構造体を整形されたXML文字列に変換します。開始インデントは空文字列（""）で、インデントの幅は2つの空白文字（" "）です。エラーがあればエラーメッセージを表示し、処理を終了します。

下記がそれらに関するコードです。

```diff_go
for _, category := range categories {
        outputFile := category + ".xml" // 出力ファイル名
		newUrlSet := UrlSet{Urls: categoryUrls[category]} // 新しいUrlSetを作成
		newXmlData, err := xml.MarshalIndent(newUrlSet, "", "  ") // UrlSetを整形してXMLデータに変換
		if err != nil {
			fmt.Println("Error marshalling XML:", err)
			return
		}
// ... 他の処理
}
```

下記は出力ファイルを作成します。エラーが発生した場合、エラーメッセージを表示し、処理を終了します。関数の終了時に、出力ファイルを閉じるように指定します。

```diff_go
newXmlFile, err := os.Create(outputFile) // 出力ファイルを作成
		if err != nil {
			fmt.Println("Error creating file:", err)
			return
}
defer newXmlFile.Close()
```

下記は出力ファイルにxml.Headerを書き込みます。これにより、XML宣言が正しくファイルの先頭に追加されます。最後に、整形されたXMLデータを出力ファイルに書き込みます。

```diff_go
newXmlFile.WriteString(xml.Header) // 出力ファイルにXMLヘッダーを書き込む
newXmlFile.Write(newXmlData) // 出力ファイルにXMLデータを書き込む
```

## まとめ
この記事では、Go言語を使用して特定のカテゴリを含むURLをXMLファイルから抽出し、新しいXMLファイルに書き込む方法について解説しました。この方法を用いることで、複数のカテゴリに対応した新しいXMLファイルを効率的に作成することができます。

## 補足
去年の年末に以下の記事を投稿しましたのでよかったら読んでみてください！(これからエンジニアインターンやアルバイトをしてみようとしている方向けの内容となっています)

https://qiita.com/hrk_ym/items/03a484c010f42b3eba37