2014年10月31日金曜日

com.sun.syndication.io.ParsingFeedException: Invalid XML: Error on line 14: An invalid XML character (Unicode: 0x1d) was found in the CDATA section.

概要

JavaでRSSフィードを取得してパースしているときに発生しました
原因はXML内にunicodeが含まれるためでした
力技ですがunicodeが含まれている場合の対応コードを書いたので紹介します
本当はRSSを配信しているサービスとかASP側で対応してほしいものですが。。。

環境

  • Java 1.7
  • Eclipse 4.4(Luna)
  • rome-fetcher 0.9

対応方法

今回、自分はrome-fetherというJavaのRSSパーサを使っていました
エラーが発生する箇所はFeedFetcher.retrieveFeedというメソッドをコールした部分で
retrieveFeedの引数に指定したRSSフィードのXMLがぶっ壊れているとParsingFeedExceptionが発生するようです

対応方法はだいぶ力技な感じがしますがParsingFeedException or SAXException が発生したら独自のRSSパーサがコールされ、独自のパーサでは取得したRSS情報のXMLからunicode文字を削除した上でXMLの解析を開始させます

やっていることは以上でソース的には以下のような感じにしました

package test;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * 独自のRSSパーサをテストするためのクラス
 * 
 * @author kakakikikeke
 *
 */
public class RssReader {

    String uri;

    public RssReader(String uri) {
        super();
        this.uri = uri;
    }

    /**
     * 独自のRSSパーサ
     */
    public void fetcher() {
        // Javaがデフォルトで提供するDOM解析クラスを使ってパースします
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder builder = factory.newDocumentBuilder();
            factory.setValidating(true);
            Node root = null;
            try {
                // 何も考えずパースする、XMLが壊れている場合はSAXExceptionが発生する
                root = builder.parse(uri);
            } catch (SAXException e) {
                // XMLが壊れている場合はunicodeを取り除いた上でパースする
                root = builder.parse(getTrimmedUnicodeXML(), "UTF-8");
            }
            // パースが完了したらDOM構造から必要な情報を取得する
            NodeList nl = root.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                if (nl.item(i).getNodeName().equals("rss")) {
                    NodeList nl2 = nl.item(i).getChildNodes();
                    for (int j = 0; j < nl2.getLength(); j++) {
                        NodeList nl3 = nl2.item(j).getChildNodes();
                        for (int k = 0; k < nl3.getLength(); k++) {
                            if (nl3.item(k).getNodeName().equals("item")) {
                                NodeList nl4 = nl3.item(k).getChildNodes();
                                for (int l = 0; l < nl4.getLength(); l++) {
                                    System.out.println(nl4.item(l).getNodeName());
                                    System.out.println(nl4.item(l).getFirstChild().getNodeValue());
                                }
                                System.out.println();
                            }
                        }
                    }       
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * unicode文字を取り除したXML情報を取得する
     * 
     * @return
     * @throws IOException
     */
    public InputStream getTrimmedUnicodeXML() throws IOException {
        // RSSフィードを指定してXMLを取得し文字列に格納する
        URL url = new URL(uri);
        HttpURLConnection urlconn = (HttpURLConnection)url.openConnection();
        urlconn.setRequestProperty("Accept-Charset", "UTF-8");
        urlconn.connect();
        BufferedReader reader = new BufferedReader(new InputStreamReader(urlconn.getInputStream(), "UTF-8"));
        String xml = "";
        while (true){
            String line = reader.readLine();
            if ( line == null ){
                break;
            }
            xml += line;
        }
        reader.close();
        urlconn.disconnect();
        // 取得したXMLからunicode文字を取り除く
        xml = xml.replaceAll("[\\00-\\x08\\x0a-\\x1f\\x7f]", "");

        // 最終的にInputStreamで渡す必要があるため一旦ファイルに書き出す
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("rss20.xml"),"UTF-8"); 
        PrintWriter pw = new PrintWriter(osw);
        pw.write(xml);
        pw.close();
        osw.close();

        // ファイルからInputStremを生成する
        InputStream in = new FileInputStream("rss20.xml");
        return in;
    }

    /**
     * 実行メインクラス
     * 
     * @param args
     */
    public static void main(String[] args) {
        // ここにパースが失敗するRSSフィードのURLを指定してください
        String uri = "http://rssblog.ameba.jp/figma/rss20.xml";
        RssReader rss2 = new RssReader(uri);
        rss2.fetcher();
    }

}

ポイントは
root = builder.parse(uri);の部分でここでSAXExceptionが発生したらgetTrimmedUnicodeXMLでunicode文字が除外されたXMLを元にparseを実行します

parseメソッドはjavax.xml.parsers.DocumentBuilderクラスのメソッドでInputStreamを引数にすることでパースすることもできます
なのでgetTrimmedUnicodeXMLでは一旦XMLを取得した上でStringに格納しreplaceAllメソッドでunicode文字を空白に置換した上で
さらにファイルに書き込んでそれを読み込むことでInputStreamを生成しています
(この辺はわざわざファイルにしなくてもInputStreamを生成できるもっといい方法があるかもしれません)

流れはだいたいそんな感じです
一応これを使ったらちゃんとパースして目的の情報を取得することはできました
それでもまだエラーになってしまう場合はreplaceAllする文字の種類を増やしてみてください
XMLの詳しいルールはわかりませんが、まだXML内に含んではいけない文字が含まれているはずです

参考サイト

0 件のコメント:

コメントを投稿