_markitdown.py — MarkItDown 類別

整條調度管線的「總電源開關」:註冊、猜型別、排序、轉換
檔案:packages/markitdown/src/markitdown/_markitdown.py(806 行)

大方向

_markitdown.py 定義 MarkItDown 類別與 ConverterRegistration。 關鍵設計:5 個 convert_* 方法全部最終匯流到同一個 _convert()—— 先猜出「這個檔案可能是什麼」的 StreamInfo 清單,再依 priority 依序嘗試每個 converter,直到有人成功。 開頭兩個模組級常數定義優先級:特定格式 0.0、通用兜底 10.0


1. 模組級:常數與外掛載入

PRIORITY_SPECIFIC_FILE_FORMAT / PRIORITY_GENERIC_FILE_FORMAT 常數優先級基準:越小越先試。

特定格式(docx/pdf/xlsx…)都給 0.0;「近乎兜底」的 PlainTextConverter / HtmlConverter / ZipConverter10.0。 註冊順序的註解寫得很明確:「越特定的 converter 要放在越通用之下」

_load_plugins() moduleLazy 載入 entry-point 群組 markitdown.plugin 的全部外掛。

用模組級全域 _plugins 記快取,第一次呼叫才真的 entry_points(group="markitdown.plugin")。單一外掛 load() 拋例外時 只 warn() 並跳過,不會中斷——這是「外掛壞了不影響主程式」的設計。

2. 建構與註冊

MarkItDown.__init__(*, enable_builtins=None, enable_plugins=None, **kwargs) 核心建 requests session、magika 實例,依旗標註冊內建/外掛 converter。

三件事:

  • requests session:預設帶 Accept: text/markdown, text/html;q=0.9..., 伺服器支援時會直接回 Markdown。
  • magikamagika.Magika() 一次初始化,供型別猜測。
  • 註冊enable_builtins 預設 None→True 註冊內建; enable_plugins 預設關閉。
教學重點:參數全走 keyword-only(*),避免位置參數錯位。
enable_builtins(**kwargs) 一次註冊 22 個內建 converter + 可選的 Azure 雲端 converter。

先解析全域選項(llm_client / llm_model / llm_prompt / exiftool_path / style_map), exiftool_path 依序來自 kwargs → EXIFTOOL_PATH 環境變數 → 白名單路徑 (/usr/bin、C:\Program Files…)搜尋。然後依註冊順序 register_converter(...) 加 22 個內建; 若提供 docintel_endpoint / cu_endpoint,再把對應 Azure converter 插到堆疊頂端(最優先)。重複呼叫會 warn()

enable_plugins(**kwargs) 一次載入全部外掛並把 kwargs 轉交 register_converters()。

呼叫 _load_plugins(),對每個 plugin 執行 plugin.register_converters(self, **kwargs)——這就是外掛拿到 llm_client / llm_model 等全域選項的方式。

register_converter(converter, *, priority=0.0) API插入註冊清單頂端;後註冊者優先。

self._converters.insert(0, ConverterRegistration(...))。 因為「後註冊者在前」+ 穩定排序,同優先級時最後註冊的最先試——外掛可以藉此覆蓋內建。 register_page_converter() 是棄用別名,僅 warn()

3. 5 個 convert_* 方法(全部匯流到 _convert)

convert(source, *, stream_info=None, **kwargs) 入口依輸入型別自動分派:str/Path/Response/BinaryIO。

最通用的入口:strhttp:/https:/file:/data: 前綴判斷走 convert_uriconvert_localPath → local; requests.Response → response;有 .read() 的 bytes 流 → stream。 否則拋 TypeError。legacy url= kwarg 會被改名成 mock_url

convert_local(path, *, stream_info=None, file_extension=None, url=None, **kwargs) 本機最窄 API:只吃本機檔案路徑。

以副檔名 + 檔名建 base_guess,用 kwargs/stream_info 補強,open 成 bytes 流後 _get_stream_info_guesses() 猜測 → _convert()。安全文件建議「只讀本機檔就用這個」。

convert_stream(stream, *, stream_info=None, ...) bytes 流處理不可 seek 的串流(先灌進 BytesIO)。

不可 seek() 的串流會先讀成 BytesIO buffer 再 seek(0), 確保 magika 能重複讀取。

convert_uri(uri, *, stream_info=None, mock_url=None, ...) URI處理 file: / data: / http: / https: 四種 scheme。
  • file:file_uri_to_path 解出 path;netloc 非空且非 localhost 會 ValueError
  • data:parse_data_uri 解出 mimetype + bytes → convert_stream。
  • http(s): → 用 session 抓(raise_for_status())→ convert_response。

convert_url() 是它的別名(未來可能棄用)。

convert_response(response, *, stream_info=None, ...) HTTP從 response headers + URL 湊出線索,讀進 BytesIO 再轉換。

content-type 拆 mimetype + charset,從 content-dispositionfilename= 或 URL path 的副檔名湊出 extension,組 base_guess 後轉換。 這讓「自己控制 HTTP 抓取」成為可能。

4. 調度核心 _convert

_convert(*, file_stream, stream_info_guesses, **kwargs) 核心依 priority 排序 converters,逐一試 accepts→convert,標準化輸出。

流程(每個 guess × 每個 converter):

  • 每次 sorted(self._converters, key=lambda x: x.priority)——priority 可能變動故每次重排。
  • converter.accepts(...)NotImplementedError 視為不接受。
  • 接受後 converter.convert(...);拋例外 → 記 FailedConversionAttemptfinally 把 stream 跳回原位。
  • 成功後標準化:每行 rstrip() + \n{3,} 壓成 \n\n,回傳。

全部失敗:有失敗紀錄 → FileConversionException(attempts);沒人接受 → UnsupportedFormatException

教學重點:全域選項(llm_client/llm_model/llm_prompt/style_map/exiftool_path) 在 _convert 裡「複製進 kwargs」傳給每個 converter——所以 CLI 的 --llm-client 能下推到所有 converter。

5. 型別猜測

_get_stream_info_guesses(file_stream, base_guess) 核心用 mimetypes + magika + charset-normalizer 產出多個 StreamInfo 線索。
  • 有副檔名無 mimetype → mimetypes.guess_type 補;反之互補。
  • magika.identify_stream() 讀位元組猜型別,文字類再讀前 4KB 用 charset_normalizer 猜編碼。
  • magika 猜測與 base_guess相容 → 合併成一個 guess;衝突 → 兩個 guess 都放,依序嘗試。
_normalize_charset(charset) 工具用 codecs.lookup 把編碼名正規化成 canonical 形式。

避免「utf-8」vs「UTF-8」vs「utf8」的比對陷阱;查不到就原樣回傳。