Apache Tika 2.9.2に変更してからparse()メソッドがエラーを出すようになった

# はじめに

Apache Tikaのバージョンを1.85から2.9.2に変更したところ、``nkf --guess``が**ASCII**と判定するWebページのデータを処理しようとしたタイミングで例外が発生するようになりました。

このファイル自体は1998年前後に作成されたもので、HTMLとしてはかなり古い仕様で作成されています。

もう少し調べてみるとUS-ASCII形式であることや古いファイルが問題を引き起すのではなく、文字コードがTika内部で判別できないことによるエラーであることが分かりました。問題はnginxが返すようなデフォルトのエラページでも発生します。

もう少し原因を探るとexec-maven-pluginを利用すると問題は発生しません。問題はJARファイルを利用した時に発生していてApache Tika 2.7.0までは問題なく動作し、2.8.0〜2.9.1までは **maven-assembly-plugin** で作成した *-jar-with-dependencies.jar ファイルを利用すると問題なく処理できることが分かり、2.9.2ではどうやってもエラーを回避する方法は見つかりませんでした。

エラーメッセージは次のとおりです。

```text:Tikaのエラーメッセージ
Failed to detect the character encoding of a document
```

Tikaを1.85から2.9.2に対応させたので、その過程でコード上の処理に漏れがあったのかとも思ったのですが、前述したとおりJARファイルの構成によるもののようです。

根本原因の調査はできませんでしたが対応方法は分かったので顛末をまとめます。

## 1.85と2.9.2の処理の違いを調査する

htmlParseオブジェクトは、org.apache.tika.parser.html.HtmlParserのインスタンスなのですが、1.28.5でもparse()メソッドの本体は同じ処理をしています。

2.9.2のコードを確認すると、例外を投げている処理は org.apache.tika.detect.AutoDetectReader のdetect()メソッドが出しています。ここの処理も1.28.5と2.9.2で違いはありません。

## Tika 2.9.2からバージョンを下げて挙動の違いを確認する

ここら辺から2.9.2固有の挙動なのではと思いライブラリのバージョンを下げていくと、Apache Tikaの2.7.0では例外を出さず、2.8.0から例外の送出が始まっていることが分かります。

2.7.0の挙動を確認すると無事に動作した場合はmetadataオブジェクトの内容がtryの前後で次のように変化します。

```text:
## tryブロックの直前
Content-Type=text/html
## ↓ htmlParser.parse()の処理後tryブロックの直後
Content-Encoding=windows-1252 Content-Type=text/html; charset=windows-1252
```

Windows-1252エンコーディングはいわゆるUS-ASCIIにラテン語圏の文字セットを加えた8ビットコードになっています。いずれにしてもTikaによってcharsetが判別できれば問題なさそうです。

Webサーバー側のContent-TypeにはUTF-8の指定は入っていません。

```bash:curlコマンドの出力
HTTP/1.1 200 OK
Date: Thu, 12 Sep 2024 05:16:34 GMT
Server: Apache
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Last-Modified: Thu, 12 Sep 2024 05:10:10 GMT
ETag: "a0-621e51ee7d080"
Accept-Ranges: bytes
Content-Length: 160
X-Content-Type-Options: nosniff
Content-Type: text/html
```

問題が発生しないURLを確認するとサーバーのcontent-typeにcharset=utf-8が指定されていたり、HTML5等の仕様に従っているコンテンツのようでした。

## 概念実証コードの作成

ここまでの調査から、どうやらUTF-8などと判定できないようなWebページで問題になることが分かってきました。

```bash:nkfによる文字コードの判定結果
## 正しく処理できないWebページ
$ curl https://www.yadiary.net/notfound  | nkf --guess
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   178  100   178    0     0    715      0 --:--:-- --:--:-- --:--:--   717
ASCII (CRLF)

## 正しく処理できるWebページ
$ curl https://www.yadiary.net/  | nkf --guess
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6798    0  6798    0     0  51834      0 --:--:-- --:--:-- --:--:-- 51893
UTF-8 (LF)
```

これだと英語圏でかなり問題が出そうな感じですが、一緒に使っているcrawler4jのコードに起因するような感じがするのでシンプルなプログラムを作成して確認を続けます。

以下で行った作業はGitHubに登録しています。

* [https://github.com/YasuhiroABE/tika-parser-example](https://github.com/YasuhiroABE/tika-parser-example)

## コードを実行して分かったこと

最初はまったく気がついていなかったのですが、shadingしたJARファイルを利用すると問題が発生します。

通常はテストのためにexec-maven-pluginを利用していたため、単体テストでは気がついていませんでした。

```bash:問題のないケース
$ make c
$ make run
mvn exec:java
...
builtin: https://www.yadiary.net/notfound
builtin: bodyBytes.length=196

dc:title=404 Not Found Content-Encoding=ISO-8859-1 Content-Type=text/html; charset=ISO-8859-1
...
```

JARファイルを利用すると問題が発生していてます。アプリケーションはコンテナにしているのでJARファイルを使っています。

```bash:問題が発生するケース
$ java -jar target/encoding-issue-1.0.0.jar 
builtin: https://www.yadiary.net/notfound
builtin: bodyBytes.length=196

Error: Failed to detect the character encoding of a document
...
```

exec-maven-pluginを利用すると実行の度にカスタムクラスローダーが必要なJARファイルを読み込むだろうと思うので、shadedされたJARファイルがclassファイルを配置する過程の挙動の違いが原因だろうなとは思いつつ、もう少しTikaのバージョンの違いについて調べてみます。

## Tikaのバージョンによる挙動の違い

pom.xmlファイルをいくつか準備して挙動の違いを確認しました。

ファイルはGitHubのプロジェクトに含めています。

### Tika 2.7.0の場合

```bash:
$ mvn -f pom270.xml clean compile package
```

exec-maven-plugin経由で実行します。

```bash:
$ mvn -f pom270.xml exec:java
...省略...
builtin: https://www.yadiary.net/notfound
builtin: bodyBytes.length=196

dc:title=404 Not Found Content-Encoding=ISO-8859-1 Content-Type=text/html; charset=ISO-8859-1
httpclient5: https://www.yadiary.net/notfound
httpclient5: bodyBytes.length=196

dc:title=404 Not Found Content-Encoding=ISO-8859-1 Content-Type=text/html; charset=ISO-8859-1
```

encoding-issue-1.0.0.jar を使用しても結果は同じです。

```bash:Tika 2.7.0 encoding-issue-1.0.0.jar
$ java -jar target/encoding-issue-1.0.0.jar
...省略...
builtin: https://www.yadiary.net/notfound
builtin: bodyBytes.length=196

dc:title=404 Not Found Content-Encoding=ISO-8859-1 Content-Type=text/html; charset=ISO-8859-1
httpclient5: https://www.yadiary.net/notfound
httpclient5: bodyBytes.length=196

dc:title=404 Not Found Content-Encoding=ISO-8859-1 Content-Type=text/html; charset=ISO-8859-1
```

encoding-issue-1.0.0-jar-with-dependencies.jarを使用します。

```bash:Tika 2.7.0 encoding-issue-1.0.0-jar-with-dependencies.jar
$ java -jar target/encoding-issue-1.0.0-jar-with-dependencies.jar
builtin: https://www.yadiary.net/notfound
builtin: bodyBytes.length=196

Error: Failed to detect the character encoding of a document
httpclient5: https://www.yadiary.net/notfound
httpclient5: bodyBytes.length=196

Error: Failed to detect the character encoding of a document
```

### 2.8.0, 2.9.1, 2.9.2でも同様に確認した結果のまとめ

次の表のようにそれぞれ実行方法によって結果が異なります。

Tika 2.9.1まではmaven-assembly-pluginを使うことで対応しましたが、2.9.2ではいまのところmavenを使わないと実行できない状況です。

|Tika Version  |exec:java  | .jar  | jar-with-dependencies.jar
|---|---|---|---|
|2.7.0  | OK | OK | NG |
|2.8.0  | OK | NG | OK |
|2.9.1  | OK | NG | OK |
|2.9.2  | OK | NG | NG |

2.7.0では完全にうまく動作していると思ったのですが、maven-assembly-pluginで生成されるJARファイルは失敗します。

ここまでが背景のまとめです。

## 参考資料

* [https://tika.apache.org/2.9.2/detection.html](https://tika.apache.org/2.9.2/detection.html)
* [https://qiita.com/YasuhiroABE/items/4df272fcc0c2cccc4173](https://qiita.com/YasuhiroABE/items/4df272fcc0c2cccc4173)
* [https://stackoverflow.com/questions/51382751/maven-exec-works-but-java-jar-does-not](https://stackoverflow.com/questions/51382751/maven-exec-works-but-java-jar-does-not)
* [https://github.com/qos-ch/logback/issues/744](https://github.com/qos-ch/logback/issues/744)
* [https://github.com/aws/serverless-java-container/issues/133](https://github.com/aws/serverless-java-container/issues/133)
* [https://product.hubspot.com/blog/the-fault-in-our-jars-why-we-stopped-building-fat-jars](https://product.hubspot.com/blog/the-fault-in-our-jars-why-we-stopped-building-fat-jars)
* [https://github.com/HubSpot/SlimFast](https://github.com/HubSpot/SlimFast)

# 対応の検討

実際に背景をまとめるまでは紆余曲折ありましたが、なんとか情報は整理できたと思います。

参考資料に挙げているGitHubのIssues#133にあるリンクを辿ってHubSpotの記事を読むとJARに全てのファイルをまとめることで規模の大きなプロジェクトではファイル転送時のネットワーク帯域にも悩んでいるようです。

target/libディレクトリの内容をコピーして、JARファイルには依存関係のないアプリケーションのコードだけを含めるという考え方だとexec-maven-pluginと同様の環境が得られるでしょうから問題が解決しそうです。

Examplesに挙げられているCopy Goalを単純に加えて最新版のTika 2.9.2で試してみます。

```bash:
$ cat pom.xml
...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <configuration>
          <archive>
            <manifest>
              <addClasspath>true</addClasspath>
              <mainClass>org.yasundial.app.tika.App</mainClass>
              <classpathPrefix>lib/</classpathPrefix>
              <classpathLayoutType>repository</classpathLayoutType>
            </manifest>
          </archive>
        </configuration>
      </plugin>
      <plugin>
        <groupId>com.hubspot.maven.plugins</groupId>
        <artifactId>slimfast-plugin</artifactId>
        <version>0.22</version>
        <executions>
          <execution>
            <goals>
              <goal>copy</goal>
            </goals>
            <phase>package</phase>
          </execution>
        </executions>
      </plugin>
    </plugins>   
  </build>
...
```

```bash:
$ make c
$ make p
## 下記の実行方法はいずれもtarget/lib/ディレクトリに依存しています
$ java -jar target/encoding-issue-1.0.0.jar
$ java -cp target/lib:target/original-encoding-issue-1.0.0.jar org.yasundial.app.tika.App
```

いずれの方法でも問題なく無事にTika 2.9.2でも動作が確認できました。コメントにも記載していますが、JARファイル単独では起動できないのでJARファイルが配置されているディレクトリにtarget/lib/ディレクトリのコピーが必要です。

JARファイルのMANIFEST.MFを確認するとClass-Path:が大量に明記されています。

```text:META-INF/MANIFEST.MFの抜粋
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.2
Build-Jdk-Spec: 21
Class-Path: lib/org/apache/tika/tika-core/2.9.2/tika-core-2.9.2.jar lib/
 org/slf4j/slf4j-api/2.0.10/slf4j-api-2.0.10.jar lib/commons-io/commons-
...
Main-Class: org.yasundial.app.tika.App
```

現在はこの状態のpom.xmlをGitHubのプロジェクトに配置しています。

## crawler4jでの問題解決

元々のcrawler4jのpom.xmlにslimfastを適用したところ、問題なく動作しました。

最終的にpom.xmlからmaven-assembly-pluginとmaven-shade-pluginの定義全体を削除しています。

## さいごに

最終的には依存性のあるJARファイルをlibディレクトリに配置することでexec:javaと同様の環境することで解決しました。

バージョン違いのライブラリを使う時に必ずしも新しいバージョンのclassファイルが参照されていないのだろうと思いますが根本原因の調査は時間がかかりそうなので、とりあえず現実的なワークアラウンドを取ることで一旦終りにします。

Apache Tikaのバージョンを上げてみるとdeprecatedなメソッド呼び出しなどがあって修正箇所は少なかったですが、ライブラリの依存性以外にも少し手を加える必要がありました。

この他にはAutoDetectParserが実際にはほぼ何も自動的に判別してくれなくなっていたりして、ここら辺の挙動もひょっとすると関連した影響なのかなと思いましたが、最終的には個別にContent-Typeをみて明示的にParserを呼び出すようにして解決しています。

アプリケーション全体を最適化するには良い機会だったのですが、まだ日本語Postscriptをps2pdfで変換した後に日本語が抽出できないとか、httpclient5への移行も進めていますが日本語の抽出に失敗するなど、他の問題も顕在化しました。

いまのところGitHubに登録しているコードはhttpclient5を利用した部分は日本語部分が文字化けします。これは明示的に"UTF-8"を指定することで解決できます。

```java:httpclient5のEntitUtils.toString()で文字化けする問題への対応
diff --git a/src/main/java/org/yasundial/app/tika/App.java b/src/main/java/org/yasundial/app/tika/App.java
index c2ad449..0935ee8 100644
--- a/src/main/java/org/yasundial/app/tika/App.java
+++ b/src/main/java/org/yasundial/app/tika/App.java
@@ -77,7 +77,7 @@ public class App {
             final HttpGet httpget = new HttpGet(urlText);
 
             final Result result = httpclient.execute(httpget, response -> {
-                    return new Result(response.getCode(), EntityUtils.toString(response.getEntity()));
+                    return new Result(response.getCode(), EntityUtils.toString(response.getEntity(), "UTF-8"));
                 });
             byte[] bodyBytes = result.content.getBytes();
             System.out.println("httpclient5: bodyBytes.length=" + bodyBytes.length);
```

作成したGitHubのプロジェクトはWebクローラーこそ使用していませんが、取得するURLを変更することでTikaの動作確認には応用できるんじゃないかなと思います。この記事が何かしら参考になれば幸いです。