文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

javaでサイトマップxmlを生成するならsitemapgen4jで決まり!

手軽に生成できていい感じですよ〜


f:id:treeapps:20180426142529p:plain

定番のサイトマップxmlですが、皆さんどうやって生成してますか?
開発者であれば大抵自前でそれっぽいユーティリティーを作ったりするかと思います。
しかし自前で作ると色々面倒な事が起こります。
今回は自前のユーティリティーの何が問題か、sitemapgen4jを使うとどう変わるか、
sitemapgen4jの注意点、について話題にします。
Google Code Archive - Long-term storage for Google Code Project Hosting.

自前のユーティリティーの問題点

自前のユーティリティーの問題点について、順に見て行きましょう。

実装言語がバラバラ

意識しないと複数の言語で実装してしまう事があります。
そもそもサイトマップxmlですが、普通はサイトマップインデックスファイルを作ると思います。
すると、以下の処理が必要になります。

1,URLを記述したログを蓄積する。
2,bash等でログを結合・URLの重複を除去して1ファイル化する。
3,bashからjavaのバッチを呼び出し、2のマージログを元にサイトマップxmlを生成。
4,3のサイトマップxmlをgz圧縮する。
5,4の全ファイルを対象にサイトマップインデックスを生成する。

私の場合、それぞれ以下の言語で実装していました。
1,javaのlog4j・logback。ローテートもロガーが勝手にやってくれて便利。
2,bashでegrepで対象ログを抽出し、bashからjavaのバッチを呼ぶ。
3,2のbashでサイトマップ生成バッチを呼ぶ。
4,2のbashの最後でgz圧縮する。
5,2のbashでrubyのスクリプトを読んでサイトマップxmlを生成する。

こんな具合にjava・ruby・bashが入り乱れてバラバラです。
sitemapgen4jを使うと、4・5がsitemapgen4jに任せられます。
sitemapgen4jは面倒なgz圧縮までやってくれます。

拡張コンテンツに対応できない

サイトマップxmlは標準でプロトコルを拡張できる仕様になっており、googleが拡張しています。

他にもサイトマップを使って、サイト上の動画、画像、モバイル、ニュースなど、特殊なタイプのコンテンツに関するメタデータを Google に提供することができます。たとえば、動画のサイトマップ エントリには、動画の再生時間、カテゴリ、一般向けかどうかを指定できます。画像のサイトマップ エントリには、画像の題材、形式、ライセンスに関する情報を指定できます。サイトマップを使用して、最終更新日やページの変更頻度といったサイトに関する詳細情報を指定することもできます。ニュース情報を送信する際は、別のサイトマップを使用することをおすすめします。

https://support.google.com/webmasters/answer/156184?hl=ja

このように、動画・画像・モバイル・ニュースもあるんです。
さて、自前にユーティリティーはそれらに対応していますか?簡単に対応できますか?
sitemapgen4jはそれら全てに標準で対応しています。

XMLのバリデーションができない

あなたの自前のユーティリティー、生成したXMLのバリデーションはできますか?
できない、又は手間が増えるからやらない、という場合がほとんどじゃないでしょうか。
sitemapgen4jであればメソッド1個呼ぶだけでバリデーションできちゃいます。

大量にログがあるとOutOfMemoryする

これは単に実装する際にOutOfMemoryしないように順次書き込み・バッファをflush、
等とメモリを気にして実装できているかの問題です。
自前のユーティリティーでもできるかと思いますが、面倒ではないでしょうか。
sitemapgen4jで試しに100万行のURLを流しこんでみましたが、OutOfMemoryせず実行できました。
ファイルのRWは意識しないとすぐOutOfMemoryするので、意識しないでいいのはありがたいのです。

sitemapgen4jで実装する際の注意点等

Google Code Archive - Long-term storage for Google Code Project Hosting.
sitemapgen4jを使うメリットは解ったと思うので、実際に実装してみましょう。
私が軽く試してみたところ、あれ?と思う内部仕様があったので順に解決していきます。

サイトマップインデックスまで自動生成するサンプル

まずはサンプルです。

try {
    W3CDateFormat dateFormat = new W3CDateFormat(Pattern.DAY);
    dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));

    WebSitemapGenerator wsg = WebSitemapGenerator
            .builder("http://www.example.com", myDir)
            .dateFormat(dateFormat).gzip(true).build();
    wsg.addUrl("http://www.example.com/index.html");

    for (int i = 0; i < 10000; i++) {
        WebSitemapUrl url = new WebSitemapUrl.Options(
                "http://www.example.com/index.html")
                .lastMod(new Date()).priority(1.0)
                .changeFreq(ChangeFreq.HOURLY).build();
        wsg.addUrl(url);
    }

    wsg.write();
    wsg.writeSitemapsWithIndex();
} catch (MalformedURLException e) {
    e.printStackTrace();
}

見た感じ問題無さそうに見えます。単に1万URLあると仮定してサイトマップインデックスを生成してます。
さて、これを実行してみます。

Exception in thread "main" java.lang.RuntimeException: No URLs added, sitemap index would be empty; you must add some URLs with addUrls
	at com.redfin.sitemapgenerator.SitemapIndexGenerator.write(SitemapIndexGenerator.java:203)
	at com.redfin.sitemapgenerator.SitemapGenerator.writeSitemapsWithIndex(SitemapGenerator.java:178)
	at com.redfin.sitemapgenerator.WebSitemapGenerator.writeSitemapsWithIndex(WebSitemapGenerator.java:14)
	at tree.sitemap.SitemapxmlTest.test1(SitemapxmlTest.java:46)
	at tree.sitemap.SitemapxmlTest.main(SitemapxmlTest.java:20)

あれ?URLが無い???
これは何が起きているかというと、サイトマップインデックスを生成してないのです。
なぜ生成できてないかというと、内部的にURLが50000件以上あった場合のみ生成する実装だからなのです。
ソースを追うと、以下のコードが見つかりました。

abstract class SitemapGenerator<U extends ISitemapUrl, THIS extends SitemapGenerator<U,THIS>> {
	/** 50000 URLs per sitemap maximum */
	public static final int MAX_URLS_PER_SITEMAP = 50000;

詳細に追っていませんが、ステップ実行すると以下のurlsのsizeが0になっていました。

/** Writes out the sitemap index */
public void write() {
    if (urls.size() == 0) throw new RuntimeException("No URLs added, sitemap index would be empty; you must add some URLs with addUrls");
    try {
        // TODO gzip? is that legal for a sitemap index?
        FileWriter out = new FileWriter(outFile);
        writeSiteMap(out);
        if (autoValidate) SitemapValidator.validateSitemapIndex(outFile);
    } catch (IOException e) {
        throw new RuntimeException("Problem writing sitemap index file " + outFile, e);
    } catch (SAXException e) {
        throw new RuntimeException("Problem validating sitemap index file (bug?)", e);
    }
}

つまり、サイトマップインデックスは50000URL以上ある前提となっている、
それ以下の場合はインデックスが作成できずエラーを返す、という仕様のようです。
更に、これはgz圧縮する場合限定の挙動のようです。
これは困りますね。インデックスを作るか作らないかを判定してあげないといけません。

では別の方法が無いか探ってみると、exampleに書いてました。

try {
    WebSitemapGenerator wsg;
    // generate foo sitemap
    wsg = WebSitemapGenerator.builder("http://www.example.com", myDir)
            .fileNamePrefix("foo").build();
    for (int i = 0; i < 5; i++)
        wsg.addUrl("http://www.example.com/foo" + i + ".html");
    wsg.write();

    // generate bar sitemap
    wsg = WebSitemapGenerator.builder("http://www.example.com", myDir)
            .fileNamePrefix("bar").build();
    for (int i = 0; i < 5; i++)
        wsg.addUrl("http://www.example.com/bar" + i + ".html");
    wsg.write();

    // generate sitemap index for foo + bar
    SitemapIndexGenerator sig = new SitemapIndexGenerator(
            "http://www.example.com", myFile);
    sig.addUrl("http://www.example.com/foo.xml");
    sig.addUrl("http://www.example.com/bar.xml");
    sig.write();
} catch (MalformedURLException e) {
    e.printStackTrace();
}

SitemapIndexGeneratorを使うと、必ずサイトマップインデックスを作成できますが、
自分でURLを指定してあげないといけません。exampleではfoo.xml、bar.xmlとなっていますが、
fooとbarが50000URLを超えると、foo1.xml.gz、foo2.xml.gz、となってしまいます。
これを解決するためには、インデックス生成前にファイル名リストを収集しておくといいです。
具体的には、以下のようなコードで解決できます。

private void test() {
    File myFile = new File("/tmp/sitemap/sitemap_index.xml");
    File myDir = new File("/tmp/sitemap");
    WebSitemapGenerator wsg = null;
    try {
        wsg = WebSitemapGenerator.builder("http://www.example.com", myDir)
                .fileNamePrefix("foo").gzip(true).build();
        for (int i = 0; i < 100000; i++)
            wsg.addUrl("http://www.example.com/foo" + i + ".html");
        wsg.write();

        wsg = WebSitemapGenerator.builder("http://www.example.com", myDir)
                .fileNamePrefix("bar").gzip(true).build();
        for (int i = 0; i < 100000; i++)
            wsg.addUrl("http://www.example.com/bar" + i + ".html");
        wsg.write();

        SitemapIndexGenerator sig = new SitemapIndexGenerator(
                "http://www.example.com", myFile);
        List<String> fooNames = getFileNames("foo");
        for (String fooName : fooNames)
            sig.addUrl("http://www.example.com/" + fooName);
        sig.write();
        
        List<String> barNames = getFileNames("bar");
        for (String barName : barNames)
            sig.addUrl("http://www.example.com/" + barName);
        sig.write();
    } catch (MalformedURLException e) {
        e.printStackTrace();
    }
}

private List<String> getFileNames(final String prefix) {
    File dir = new File("/tmp/sitemap");
    File[] files = dir.listFiles(new FileFilter() {
        @Override
        public boolean accept(File f) {
            return f.getName().matches(prefix + "[0-9]*.xml.gz");
        }
    });
    List<String> list = new ArrayList<String>();
    for (File f : files)
        list.add(f.getName());
    return list;
}

今回は10万URLにしています。
これを実行すると、以下のファイルが生成されます。

treemacpro:sitemap tree$ ll
total 1096
-rw-r--r--  1 tree  wheel   133K  7  4 00:23 bar1.xml.gz
-rw-r--r--  1 tree  wheel   132K  7  4 00:23 bar2.xml.gz
-rw-r--r--  1 tree  wheel   133K  7  4 00:23 foo1.xml.gz
-rw-r--r--  1 tree  wheel   132K  7  4 00:23 foo2.xml.gz
-rw-r--r--  1 tree  wheel   633B  7  4 00:23 sitemap_index.xml

sitemap_index.xmlを見てみます。

treemacpro:sitemap tree$ cat sitemap_index.xml
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>http://www.example.com/foo1.xml.gz</loc>
    <lastmod>2013-07-04T00:23:34.635+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://www.example.com/foo2.xml.gz</loc>
    <lastmod>2013-07-04T00:23:34.635+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://www.example.com/bar1.xml.gz</loc>
    <lastmod>2013-07-04T00:23:34.635+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://www.example.com/bar2.xml.gz</loc>
    <lastmod>2013-07-04T00:23:34.635+09:00</lastmod>
  </sitemap>

おお!ちゃんと収集したファイル名を使ってURLができてますね!
試しにURLの数を50000にして再度実行してみます。

treemacpro:sitemap tree$ ll
total 552
-rw-r--r--  1 tree  wheel   133K  7  4 00:26 bar.xml.gz
-rw-r--r--  1 tree  wheel   133K  7  4 00:26 foo.xml.gz
-rw-r--r--  1 tree  wheel   375B  7  4 00:26 sitemap_index.xml
treemacpro:sitemap tree$ cat sitemap_index.xml
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>http://www.example.com/foo.xml.gz</loc>
    <lastmod>2013-07-04T00:26:09.845+09:00</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://www.example.com/bar.xml.gz</loc>
    <lastmod>2013-07-04T00:26:09.845+09:00</lastmod>
  </sitemap>

よし、foo1.xml.gzではなくfoo.xml.gzになってますが、ばっちり反映されてます!!
やったぜ。

・・
・・・
・・・・
実は「やったぜ」にはまだ早い
やったぜを言う前に、sslについての問題も見てみましょう。

httpとhttpsを混在させる際の問題

実はhttpとhttpsが混在すると問題が発生します。
試しにbarのURLをhttpsに変えて実行してみます。

Exception in thread "main" java.lang.RuntimeException: Url https://www.example.com/bar0.html doesn't start with base URL http://www.example.com
	at com.redfin.sitemapgenerator.UrlUtils.checkUrl(UrlUtils.java:10)
	at com.redfin.sitemapgenerator.SitemapGenerator.addUrl(SitemapGenerator.java:59)
	at com.redfin.sitemapgenerator.SitemapGenerator.addUrl(SitemapGenerator.java:119)
	at tree.sitemap.SitemapxmlTest.test2(SitemapxmlTest.java:69)
	at tree.sitemap.SitemapxmlTest.main(SitemapxmlTest.java:23)

あらら。

wsg = WebSitemapGenerator.builder("http://www.example.com", myDir) // ←これ
        .fileNamePrefix("bar").gzip(true).build();
for (int i = 0; i < 100000; i++)
    wsg.addUrl("https://www.example.com/bar" + i + ".html"); // ←これ
wsg.write();

http://www.example.com"と指定してるのにhttps://www.example.comが混入し、怒られました。
という事でこれは builder("https://www.example.com", myDir). とする事で解決できます。
このエラーから解るように、WebSitemapGeneratorはhttp用とhttps用を事前に作っておく
といいかと思います。最終的には以下のコードになりました。

private void test() {
    File myFile = new File("/tmp/sitemap/sitemap_index.xml");
    File myDir = new File("/tmp/sitemap");
    WebSitemapGenerator wsg = null;
    try {
        wsg = WebSitemapGenerator.builder("http://www.example.com", myDir)
                .fileNamePrefix("foo").gzip(true).build();
        for (int i = 0; i < 100000; i++)
            wsg.addUrl("http://www.example.com/foo" + i + ".html");
        wsg.write();

        wsg = WebSitemapGenerator.builder("https://www.example.com", myDir)
                .fileNamePrefix("bar").gzip(true).build();
        for (int i = 0; i < 100000; i++)
            wsg.addUrl("https://www.example.com/bar" + i + ".html");
        wsg.write();

        SitemapIndexGenerator sig = new SitemapIndexGenerator(
                "http://www.example.com", myFile);
        List<String> fooNames = getFileNames("foo");
        for (String fooName : fooNames)
            sig.addUrl("http://www.example.com/" + fooName);
        sig.write();
        
        List<String> barNames = getFileNames("bar");
        for (String barName : barNames)
            sig.addUrl("http://www.example.com/" + barName);
        sig.write();
    } catch (MalformedURLException e) {
        e.printStackTrace();
    }
}

private List<String> getFileNames(final String prefix) {
    File dir = new File("/tmp/sitemap");
    File[] files = dir.listFiles(new FileFilter() {
        @Override
        public boolean accept(File f) {
            return f.getName().matches(prefix + "[0-9]*.xml.gz");
        }
    });
    List<String> list = new ArrayList<String>();
    for (File f : files)
        list.add(f.getName());
    return list;
}

後は、for (int i = 0; i < 100000; i++) の部分をマージしたログファイルを読み込む処理に
差し替えるだけで完成です!httpとhttpsに対応できるので、
大半の画面はhttpだけど、特定の画面だけhttpsだよ、という大人の事情にも対応できます!!

これからはじめる SEO内部対策の教科書

これからはじめる SEO内部対策の教科書

検索エンジン上位表示 SEO完全ガイド ソーシャルメディア時代の内部対策&外部対策

検索エンジン上位表示 SEO完全ガイド ソーシャルメディア時代の内部対策&外部対策

SEO最新トレンド2013 Vol.1

SEO最新トレンド2013 Vol.1