REPL 和 jar 中的 Tika Parser 行为不同

Posted

技术标签:

【中文标题】REPL 和 jar 中的 Tika Parser 行为不同【英文标题】:Tika Parser behaviour different in REPL and jar 【发布时间】:2021-11-30 04:15:53 【问题描述】:

说明

我们在pantomime 后面的包装版本中使用Tika Parser,没有配置(默认),它使用AutoDetectParser。由于缺少功能,升级 Tika 依赖项至关重要。在将 tika 覆盖为 1.27 的依赖项以及其他依赖项之后,我们观察到了一些意想不到的行为。在lein repl 中运行服务时,tika 正确转换了相关文档 (.doc)。 之后我们用lein uberjar打包了clj代码。

现在有趣的部分开始了: 随后是对 tika 的深度(th)和类型转换的野生虫子狩猎。 发现问题出在不同的解析数据类型上。在 REPL 中解析的数据类型是 javax.mail.internet.MimeMessage,而生成的 .jar 中解析的数据类型是 javax.mail.util.SharedByteArrayInputStream。 日志摘录:

REPL

[2021-10-08 16:56:30,406] TRACE xxx - Unpacking MimeMessage -1
[2021-10-08 16:56:30,407] DEBUG xxx - |> type javax.mail.internet.MimeMessage
[2021-10-08 16:56:30,415] DEBUG xxx - ||> javax.mail.internet.MimeMultipart@397fca9a
[2021-10-08 16:56:30,415] DEBUG xxx - |-> transforming multipart/mixed to Mime for instance javax.mail.internet.MimeMultipart

罐子

[2021-10-08 17:02:55,181] TRACE xxx - Unpacking MimeMessage -1
[2021-10-08 17:02:55,183] DEBUG xxx - |> type javax.mail.internet.MimeMessage
[2021-10-08 17:02:55,191] DEBUG xxx - ||> javax.mail.util.SharedByteArrayInputStream@b83be05
[2021-10-08 17:02:55,193] WARN xxx - Mime unpacking unsuccessful: multipart/mixed - javax.mail.util.SharedByteArrayInputStream

调查结果

我怀疑可能存在依赖冲突,因为 tika-parser 和 simplejavamail 模块都使用 MimeMessage 的实现,jakarta.mailjavax.mail 都提供了该实现。我已经解决了 leiningen 暗示的依赖冲突,但怀疑lein repl 如何解决依赖关系可能会发生一些事情,这使得它在这种情况下工作。 使用干净的profiles.clj 复制了此行为。 对此函数.getContent 的日志记录和堆栈跟踪提示,该函数负责返回适当的对象。

project.clj

(defproject my-project :lein-v
  :plugins [[lein-parent "0.3.8"]
            [com.roomkey/lein-v "7.2.0"]]
  :parent-project :path "../../project.clj"
                   :inherit [:managed-dependencies :repositories :manifest :url :prep-tasks]
  :dependencies [[org.clojure/clojure]
                 [...]
                 [com.novemberain/pantomime "2.11.0" :exclusions 
                   [org.apache.commons/commons-compress
                    org.apache.pdfbox/fontbox
                    org.apache.tika/tika-parsers]]
                 [org.apache.tika/tika-parsers "1.27"]
                 
                 [com.sun.mail/javax.mail "1.6.2"]
                 [net.htmlparser.jericho/jericho-html "3.4"]
                 [org.simplejavamail/simple-java-mail "6.6.1"]
                 [org.simplejavamail/outlook-module "6.6.1"]]
   :profiles :uberjar :global-vars        *warn-on-reflection* true
                       :aot                :all
                       :omit-source        true
                       :uberjar-exclusions ["project.clj" #"xxx/.*\.clj$"]
                       :jvm-opts           ["-XX:-OmitStackTraceInFastThrow"]
                       :uberjar-name       "service.jar"
  :repl-options :init-ns xxx.init)

受影响的代码

(ns xxx.convert
  (:import (java.time OffsetDateTime ZoneOffset)
           (java.util Properties UUID Base64 Date)
           (java.io InputStream ByteArrayInputStream)
           (javax.activation MimeType)
           (javax.mail.internet MimeMessage MimeMultipart MimeBodyPart InternetAddress MimeUtility)
           (javax.mail Session Message Message$RecipientType)
           (net.htmlparser.jericho Source Renderer)
           (org.apache.commons.io IOUtils)

(defn- unpack-content [^Message message 
                       :keys [max-text-length max-attachments] :as params
                       counter]
  (log/debug "|> type" (type message))
  (when (or (not max-attachments)
            (< @counter max-attachments))
    (let [base-type (-> message .getContentType MimeType. .getBaseType string/lower-case)
          content   (.getContent message)] ;; <<< [!] This is where the conversion happens
      (log/debug "||>" (.toString ^MimeMultipart content))
      :content-type base-type
       :content      (cond
                       (instance? String content)
                       (do
                         (log/trace "|-> transforming" base-type "to String")
                         (swap! counter inc)
                         (->> (if (html? base-type)
                                (render-html content)
                                content)
                              (truncate max-text-length)))

                       (and (instance? InputStream content)
                            (.getFileName message))
                       (let [filename     (MimeUtility/decodeText (.getFileName message))
                             encoded-file (do
                                            (swap! counter inc)
                                            (base64-file content filename))]
                         (log/trace "|-> transforming" base-type "to InputStream")
                         (.close ^InputStream content)
                         encoded-file)

                       (or (instance? MimeMessage content)
                           (instance? MimeMultipart content)
                           (instance? MimeBodyPart content))
                       (do
                         (log/debug "|-> transforming" base-type "to Mime for instance" (type content))
                         (unpack-mime content params counter))

                       :else
                       (do
                         (log/warn "Mime unpacking unsuccessful:" base-type "-" (type content))
                         ))))) 

很遗憾,我无法共享相应的 .doc 文件。然而,它是一个官方文档,带有表格、图像等普通邮件的通用设置。

为什么会发生,我该如何解决?

【问题讨论】:

这全是猜测:在不同的包上拥有两个类不是冲突或问题(如果某些代码尝试了 smart things™,则可能是这样)。从 javax 到 jakarta 的切换很可能是在上游的某个主要版本上完成的,不应该打击你。我的猜测是,你有 deps,可能会将两者都放在同一个类中——并且你在 repl-time 和 run-time 有不同的类路径顺序(通过生成的 uberjar)。我会检查,最终在 uberjar 中的文件是否实际上来自您期望的 dep。并且仍然进行三次检查,如果这不仅仅是由于输入而导致的侥幸...... 如何检查这种实际的依赖关系? 如果您想排除类或 META-INF 中的冲突,您可以尝试运行生产代码,而不是作为 uberjar,而是作为常规 jar。例如。使用lein with-profile uberjar cp 获取所有库。然后使用这些和你的工件(不是 uberjar)并运行它。如果这再次起作用,您可以开始在您的部门中找到冲突(或放弃 uberjar) 谢谢@cfrick。我用java -cp $(lein with-profile uberjar) xxx.init 运行了这个工件并且可以重现正确的行为 那么我的下一个猜测将是 META-INF 中的内容。我对 javax.mail 一无所知,但通常那些“旧”库允许通过一些工厂/配置/属性/...配置实现 - 很可能您的至少两个部门包含此类文件,错误的一个获胜。如果您可以查明问题,一些库还允许通过-D 属性进行设置。 【参考方案1】:

Leiningen 在依赖顺序方面有一个众所周知的“特性”[1]。 Lein 按顺序加载每个依赖项,递归地包括所有传递依赖项。想象一下,您有 lein deps,例如:

[aaa/a "5.0"]
[bbb/b "5.0"]

但是,aaa/abbb/b 版本 2.1 具有传递依赖。然后 Lein 将引入 2.1 的版本 bbb/b 并忽略您要求的显式版本。

解决这个问题的唯一方法是在project.clj 中使用分层的依赖关系列表,例如:

; priority 1 libs
[bbb/b  "5.0"]
[zzz/z  "10.3"]
...

; priority 2 libs
[org.clojure/clojure "1.10.3"]
[aaa/a "5.0"]
...

这将迫使 Lein 按照您选择的优先顺序引入库。

请尝试上述方法并报告结果。您可能还需要从代码中打印出 Classpath 顺序,以验证您获得了预期的顺序和版本号。


旁注:

我始终建议您将 lein-ancient 插件添加到您的配置中,例如:

  :plugins [[com.jakemccrary/lein-test-refresh "0.24.1"]
            [lein-ancient "0.7.0"]
            ]

然后您可以定义别名:

lanc () 
    echo ""
    echo ""-----------------------------------------------------------------------------
    echo "project.clj:"
    echo ""
    lein ancient check :all
    echo ""
    echo ""-----------------------------------------------------------------------------
    echo "profiles.clj:"
    echo ""
    lein ancient check-profiles :all
    echo ""-----------------------------------------------------------------------------
    echo ""

确定哪些依赖项需要升级。请注意,这两部分都很重要!


[1] 古代电脑笑话:

  Q:  How do you recognize a computer salesman?
  A:  He says, "That's not a bug, that's a feature!"

【讨论】:

嗨艾伦,谢谢您的回答。我知道 lein Ancient,实际上用它来升级依赖项。我不知道可以使用check-profiles :all,但lein ancientlein ancient check :all 没有区别。我也尝试交换订单,但不幸的是这并没有改变行为。我什至尝试在lein ancient show-versions 的帮助下使用不同的版本。问题显然只发生在我打包 uberjar 时。删除工件 jar 并仅运行带有 cp 的 uberjar 可以正常工作。【参考方案2】:

leiningen uberjar 的工作方式基本上是压缩所有的 deps 放入一个 jar 文件中。

这在大多数情况下都没有问题,因为文件结构存在冲突 非常罕见,因为 java 包或 clojure 命名空间将事物分开。 当然有奇怪的命名冲突,有时图书馆复制 来自不同图书馆的东西供当地收养(一个好的图书馆 维护者将“隐藏”这些副本,因此没有机会 冲突)。

也就是说,jar 中的文件更有可能发生冲突: /META-INF/ 下的东西。在您的情况下,javax.mailjakarta.mail 正在努力成为邮件接口的提供者 通过META-INF/services/javax.mail.Provider

如果这些文件是从类路径加载的,那么最好的情况是 场景,库显式搜索所有此类文件和 在出现多个结果时制定解决策略。更可能, 库只是让类加载器决定(很可能是第一项 在 CP 中,包含文件,但这当然是一个实现 细节,你不应该依赖它)。在这种情况下,你很可能 正是在你的 REPL 中开发时幸运的地方并且得到了正确的 捡起来。

现在,如果 uberjar 压缩了所有这些 jar,则最后一个文件获胜。所以 解决策略和您的“幸运首选”都没有帮助。所以呢 可以做到:

摆脱两个部门之一。您很可能希望保留 jakarta 的,因为较新的名称暗示较新的库;这 取决于您正在使用的其他库,它们是否可以工作 使用新版本,当然可能需要更改您的代码 也是。 不要滚动 uberjar:我不知道有什么插件可以给你 类似于 gradle 中的 application 插件。但是你可以得到 所有使用lein with-profile uberjar cp 和非uberjar 的部门 神器。然后基本上从java -cp libs/* my.main.ns开始

【讨论】:

以上是关于REPL 和 jar 中的 Tika Parser 行为不同的主要内容,如果未能解决你的问题,请参考以下文章

tika的使用

Tika Parser放慢了StormCrawler的速度

将 tika 与 python 一起使用,运行时错误:无法启动 tika 服务器

空解析器 tika python

如何使用java从excel表提取内容

使用 tika 库从 java 中的图像中提取文本