OpenRouter 教程:用 LLM 从 PDF 中抽取表格
TL;DR · AI 摘要
OpenRouter æç¨ï¼ç¨ LLM ä» PDF 䏿½åè¡¨æ ¼ · AIHOT OpenRouterï¼Announcements · 2026-10-29 08:00 38 åéå...
核心要点
- 主题聚焦:OpenRouter 教程:用 LLM 从 PDF 中抽取表格
- 来源:AI HOT 精选,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
OpenRouter æç¨ï¼ç¨ LLM ä» PDF 䏿½åè¡¨æ ¼ · AIHOT
OpenRouterï¼Announcements
·
2026-10-29 08:00
38 åéå
ç²¾é
AI è¯å
76
OpenRouter æç¨ï¼ç¨ LLM ä» PDF 䏿½åè¡¨æ ¼
How to Extract Tables from a PDF with an LLM
AI 导读
OpenRouter å叿ç¨ï¼è®²è§£å¦ä½æ PDF 以æä»¶å½¢å¼è¿åä¸¥æ ¼ JSON schema åç»è½è¯»æä»¶ç模åï¼è®©è¡¨æ ¼ä»¥å¸¦ç±»åçè¡è¿åï¼æ¬å·äºæãååä½åèæ³¨æ è®°çæ¸ çé½åè¿å段æè¿°ã
æ¨èçç±
OpenRouter ç¨äºä»½ PDF 宿µäºå¤æ¡æ½åè·¯ç±ï¼ç»åºä¸¥æ ¼ JSON schema åæ³ã两类éé»å¤±è´¥çæ£æ¥ååè·¯ç±ææ¬å¯¹æ¯ã
æ£æ · AI ç¿»è¯
è¦ç¨ LLM ä» PDF 䏿åè¡¨æ ¼ï¼è¯·å° PDF åéç» è½å¤è¯»åæä»¶ç模å ï¼åæ¶æä¾ä¸ä¸ª ä¸¥æ ¼ç JSON schema ï¼ç¨äºæè¿°è¡¨æ ¼ä¸çä¸è¡ã模åä¼è¿å带类åçè¡ãæ°éä¼ä»¥æ´æ°è¿åï¼åæ (3,878) çäºæä¼è¿å为 -3878ï¼è·¨ä¸¤é¡µçè¡¨æ ¼ä¼è¿å为ä¸ä¸ªå表ã
æä»¬å¨äºä¸ª PDF 䏿µè¯äºè¿ä¸ç¹ï¼å ¶ä¸ä¸¤ä¸ªæ¯æ«æä»¶ãæ¯ä¸ªç´æ¥è¯»å PDF çæ¨¡åé½ç对äºå ¨é¨ 644 个åå æ ¼ãæä»¬æµè¯ä¸ç失败æ¥èª PDF å°è¾¾æ¨¡åæç»è¿çè·¯å¾ï¼å ¶ä¸ä¸¤æ¬¡è¿åäºæ£å¸¸ååºä¸æ²¡ææ¥éã
ç®è¦è¯´æ
- 为ä¸è¡å®ä¹ä¸¥æ ¼ç JSON schemaï¼å¯å° PDF è¡¨æ ¼è½¬æ¢ä¸ºå¸¦ç±»åçè¡ï¼å段æè¿°å¯å¤çæ¬å·ä¸çè´æ°åèæ³¨æ è®°çæ¸ çå·¥ä½ã
- å¨æä»¬çæµè¯ä¸ï¼è½å¤è¯»å PDF çæ¨¡ååæ¶å¤çäºæ°åæä»¶åæ«æä»¶ãæ æ³è¯»å PDF çæ¨¡åéè¦ mistral-ocr è§£æå¨æ¥å¤çæ«æä»¶ã
- éè¿å è´¹ cloudflare-ai è§£æå¨åéçæ«æä»¶ä¼è¿åé¶è¡ä¸æ²¡ææ¥éï¼å æ¤è¦æç»ç©ºç»æã
- é¿è¡¨æ ¼å¯è½ä¼å¨è¾åº token éå¶å¤åæ¢ï¼å æ¤è¦æç»ä»»ä½ finish_reason 为âlengthâçååºã
å¼å§ä¹å
è¿è¡ç¤ºä¾éè¦ä»¥ä¸å 容ã
- ä¸ä¸ªå¸¦æé¢åº¦ç OpenRouter è´¦æ·ï¼ä»¥åä¸ä¸ªåå¨å¨ OPENROUTER_API_KEY ç¯å¢åéä¸ç API key ãæ¬æåä¸çæ¨¡åæ¯ä»è´¹çã
- Python 3.10 ææ´é«çæ¬ä»¥å requests å ï¼ pip install requests ï¼ã
- Node.js 18 ææ´é«çæ¬ï¼ä½ä» ç¨äºæ¥è¿æ«å°¾ç TypeScript çæ¬ã
- ä¸ä¸ªå å«è¡¨æ ¼ç PDFãå®å¯ä»¥æ¯æ°åå¯¼åºæä»¶ææ«æä»¶ã
å¦æä½ ä¹å没æè°ç¨è¿è¯¥ APIï¼ å¿«éå ¥é¨ ä¼å±ç¤ºç¬¬ä¸ä¸ªè¯·æ±ã
代ç éç¨äºæ°å PDF åæ«æä»¶ï¼ä½ç»æåå³äº PDF æ¯å¦æææ¬å±ãæ«æè´¨éãè§£æå¨è·¯å¾å模åã示ä¾ä½¿ç¨äºäºä¸ªæµè¯ PDFï¼å®ä»¬ç±å·²ç¥æ°æ®çæï¼å æ¤æ¯ä¸ªåå æ ¼é½å¯ä»¥ä¸æºæ°æ®æ ¸å¯¹ãå®ä»¬å嫿¯ä¸å¼ å票ãä¸ä»½å£åº¦æ¥åãä¸ä»½è·¨å°ç¬¬äºé¡µç 58 è¡åè´§æ¥å¿ãä¸ä»½æ«æçéè´è®¢åï¼ä»¥åä¸ä»½æ æåå¾å¾ç²ç³ã带æåå¹¶åå æ ¼çæ«æä»¶ã
第 1 æ¥ï¼å¦ä½åé PDF å¹¶ååè¡ï¼
å°ä»¥ä¸å 容ä¿å为 extract_tables.py ãå®ä¼å° PDF ä½ä¸ºæä»¶å 容é¨ååéï¼å¦æä»¬ç PDF è¾å ¥ é¡µé¢æè¿°ï¼å¹¶éè¿ ç»æåè¾åº 请æ±ä¸ä½ ç schema å¹é ç JSONãå° strict 设为 true æ¶ï¼æ¯æä¸¥æ ¼æ¨¡å¼çæä¾åä¼è®©è¾åºç¬¦å schemaï¼å æ¤æ¯ä¸è¡é½ææ¯ä¸ªå段ä¸ç±»åæ£ç¡®ãæ¯ææ åµå æä¾åèå¼ï¼ç»æåè¾åºé¡µé¢å±ç¤ºäºå¦ä½ä» è·¯ç±å°æ¯æå®çæä¾åã
import
base64
import
json
import
os
import
requests
API
=
"https://openrouter.ai/api/v1/chat/completions"
HEADERS
=
{
"Authorization"
:
f
"Bearer
{
os.environ[
'OPENROUTER_API_KEY'
]
}
"
}
def
rows_schema
(fields):
"""Wrap the fields of one row in a strict schema for {"rows": [...]}."""
return
{
"type"
:
"object"
,
"properties"
: {
"rows"
: {
"type"
:
"array"
,
"items"
: {
"type"
:
"object"
,
"properties"
: fields,
"required"
:
list
(fields),
"additionalProperties"
:
False
}}},
"required"
: [
"rows"
],
"additionalProperties"
:
False
,
}
def
extract_table
(pdf_path, schema, model
=
"anthropic/claude-haiku-5.5"
,
engine
=
None
, max_tokens
=
8000
):
with
open
(pdf_path,
"rb"
)
as
f:
data_url
=
"data:application/pdf;base64,"
+
base64.b64encode(f.read()).decode()
body
=
{
"model"
: model,
"max_tokens"
: max_tokens,
"messages"
: [{
"role"
:
"user"
,
"content"
: [
{
"type"
:
"text"
,
"text"
:
"Extract every row of the table in this PDF. "
"Include rows from every page. "
"Return only rows that are in the document."
},
{
"type"
:
"file"
,
"file"
: {
"filename"
: os.path.basename(pdf_path),
"file_data"
: data_url}},
]}],
"response_format"
: {
"type"
:
"json_schema"
,
"json_schema"
: {
"name"
:
"table"
,
"strict"
:
True
,
"schema"
: schema}},
}
if
engine:
body[
"plugins"
]
=
[{
"id"
:
"file-parser"
,
"pdf"
: {
"engine"
: engine}}]
response
=
requests.post(
API
,
headers
=
HEADERS
,
json
=
body,
timeout
=
300
)
response.raise_for_status()
data
=
response.json()
choice
=
data[
"choices"
][
0
]
if
choice[
"finish_reason"
]
==
"length"
:
raise
ValueError
(
f
"Output stopped at max_tokens=
{
max_tokens
}
. "
"Raise it or split the PDF."
)
rows
=
json.loads(choice[
"message"
][
"content"
])[
"rows"
]
if
not
rows:
raise
ValueError
(
"No rows came back. If this PDF is a scan, use a model "
"that reads PDFs or engine='mistral-ocr'."
)
return
rows, data[
"usage"
][
"cost"
]
LINE_ITEMS
=
rows_schema({
"sku"
: {
"type"
:
"string"
},
"description"
: {
"type"
:
"string"
},
"qty"
: {
"type"
:
"integer"
},
"unit_price"
: {
"type"
:
"number"
,
"description"
:
"USD, no currency symbol or thousands separator"
},
"amount"
: {
"type"
:
"number"
,
"description"
:
"USD, no currency symbol or thousands separator"
},
})return ä¹åçä¸¤ä¸ªæ£æ¥ä¼æè·ç¬¬ 3 æ¥å第 4 æ¥ä¸æ¶µçç失败ï¼å æ¤è¯·ä¿çå®ä»¬ãææ¬æ¥èª usage.cost ï¼ ç¨éç»è®¡ ä¼å¨æ¯ä¸ªååºä¸å å«å®ã
å¨å票ä¸è¿è¡è¯¥å½æ°ã
from
extract_tables
import
extract_table,
LINE_ITEMS
rows, cost
=
extract_table(
"invoice.pdf"
,
LINE_ITEMS
)
for
row
in
rows[:
3
]:
print
(row)
print
(
f
"
{len
(rows)
}
rows, $
{
cost
:.5f
}
"
){'sku': 'NW-4100', 'description': 'Hex bolt M8x40', 'qty': 100, 'unit_price': 60.67, 'amount': 6067.0}
{'sku': 'NW-4107', 'description': 'Flange nut M8', 'qty': 2, 'unit_price': 41.72, 'amount': 83.44}
{'sku': 'NW-4114', 'description': 'Bearing 6204-2RS', 'qty': 10, 'unit_price': 52.59, 'amount': 525.9}
10 rows, $0.00067该è°ç¨èæ¶çº¦å ç§ãPDF æ¾ç¤º 6,067.00ï¼èè¡ä¸ä¿åçæ¯ 6067.0ï¼è¿æ¯ä¸ä¸ªæ éå æ¸ çå°±è½ç¸å çæ°åãåæ®µæè¿°ç§»é¤äºåä½åé符ã
第 2 æ¥ï¼å¦ä½å¤çåå¹¶åå æ ¼ãæ¬å·åèæ³¨ï¼
PDF è¡¨æ ¼æéç大é¨åæ¸ çå·¥ä½é½å¯ä»¥æ¾å ¥ schema çåæ®µæè¿°ä¸ãæä»¬çå£åº¦æ¥åæä¸ä¸ªè·¨å个å£åº¦åçâFiscal 2026â表头ï¼äºæå卿¬å·ä¸ï¼å¹¶ä¸ä¸¤ä¸ªåé¨åç§°ï¼Enterprise å Hardware resaleï¼ä¸æè注æ è®°ãåæ®µæè¿°å¤çæ¬å·åèæ³¨æ è®°ãè·¨å表头ä¸éè¦æè¿°ï¼å ä¸ºåæ®µåå·²ç»è¯´æäºæ¯ä¸ªå¼å±äºåªä¸ªå£åº¦ã
from
extract_tables
import
extract_table, rows_schema
THOUSANDS
=
{
"type"
:
"integer"
,
"description"
:
"Thousands of USD. A value in parentheses, like (1,204), is negative: -1204."
}
QUARTERLY
=
rows_schema({
"segment"
: {
"type"
:
"string"
,
"description"
:
"Segment name without footnote markers"
},
"q1"
:
THOUSANDS
,
"q2"
:
THOUSANDS
,
"q3"
:
THOUSANDS
,
"q4"
:
THOUSANDS
,
"fy_total"
:
THOUSANDS
,
})
rows, cost
=
extract_table(
"quarterly.pdf"
,
QUARTERLY
)
for
row
in
rows:
print
(row){'segment': 'Inference API', 'q1': 1813, 'q2': 7299, 'q3': 1612, 'q4': 4422, 'fy_total': 15146}
{'segment': 'Enterprise', 'q1': 1563, 'q2': 2981, 'q3': 5544, 'q4': 7667, 'fy_total': 17755}
{'segment': 'Data products', 'q1': 3163, 'q2': 9658, 'q3': 2729, 'q4': 5854, 'fy_total': 21404}
{'segment': 'Hardware resale', 'q1': 3761, 'q2': -3878, 'q3': -9774, 'q4': -1776, 'fy_total': -11667}
{'segment': 'Professional services', 'q1': 8933, 'q2': 9511, 'q3': 7805, 'q4': 5946, 'fy_total': 32195}
{'segment': 'Total operating income', 'q1': 19233, 'q2': 25571, 'q3': 7916, 'q4': 22113, 'fy_total': 74833}å¨ Hardware resale è¿ä¸è¡ä¸ï¼(3,878) è¿åæäº -3878ï¼èä¸ä¸¤ä¸ªç段åç§°éçèæ³¨æ è®°é½ä¸è§äºã
沿ååä¸åå¹¶çåå æ ¼ä¹æ¯åæ ·çéçã卿们çç²ç¥æ«æä¸ï¼æ¯ä¸ª Category åå æ ¼è·¨å å°ä¹è¡ãæä»¬æè¿ä¸ªå段æè¿°ä¸ºâ第ä¸åä¸çç±»å«ã宿¯åå¹¶åå æ ¼ï¼å æ¤è¦å¨å®è¦ççæ¯ä¸è¡ä¸éå¤ãâå¨æä»¬çæµè¯ä¸ï¼è¿è¶³ä»¥è®©ä¸ä¸ªè½è¯»å PDF çæ¨¡åé½å¨å ¨é¨ 30 è¡ä¸å¡«å ¥ç±»å«ã
第 3 æ¥ï¼æåºè¯¥ä½¿ç¨åªç§è§£æå¨è·¯çº¿ï¼
å¦ææ¨¡åè½è¯»åæä»¶ï¼æä»¬å°±æ PDF ç´æ¥ä¼ ç»å®ãå¦ææ¨¡åä¸è½è¯»åæä»¶ï¼æä»¬å è§£æ PDFï¼åæç»æåç»æ¨¡åãæä»¶è§£æå¨æä»¶ç engine 设置å³å®ä½¿ç¨åªä¸ªè§£æå¨ãå½ä½ å¨ä¸ä¸ªä¸è½è¯»åæä»¶ç模åä¸ä¸è®¾ç½®å®æ¶ï¼æä»¬ä¼ä½¿ç¨ mistral-ocr ï¼æ£å¦ PDF è¾å ¥ é¡µé¢æè¿°ãæ ¹æ®ä½ ç PDF é¿ä»ä¹æ ·æ¥éæ©è·¯çº¿ã
- è½è¯»å PDF çæ¨¡åã ä¸è®¾ç½® engine ãClaude Haiku 5.5ãGPT-6 Luna å Gemini 3.8 Flash é½è½è¯»åæä»¶ï¼èä¸è¿ä¸ä¸ªæ¨¡åé½å¤çäºæä»¬çä¸¤æ¬¡æ«æãPDF æè¾å ¥ token 计费ã
- ä¸è½è¯»å PDF çæ¨¡åï¼é å OCRã ä½¿ç¨ engine="mistral-ocr" ï¼æè ä¸è®¾ç½®å®ãå®ä¼å 对æ¯ä¸é¡µè¿è¡å å¦å符è¯å«ï¼OCRï¼ï¼å¨æ¨¡å token è´¹ç¨ä¹å¤ï¼æ¯ 1,000 页æ¶è´¹ $2ã
- ä¸è½è¯»å PDF çæ¨¡åï¼é åå 费解æå¨ã åªæå½æ¯ä¸ª PDF é½æææ¬å±æ¶æä½¿ç¨ engine="cloudflare-ai" ã导åºçåç¥¨åæ¥åæææ¬å±ãæ«æä»¶æ²¡æã
ä¸é¢æ¯ææ«æçéè´è®¢ååå«ç¨æ¯ç§è§£æå¨åéç» deepseek/deepseek-v4.1-flash çç»æãè¯¥æ¨¡åæ¥åå¾åä½ä¸æ¥åæä»¶ï¼æä»¥ PDF å¿ é¡»ç»è¿è§£æå¨ã
from
extract_tables
import
extract_table,
LINE_ITEMS
# A model that can't read PDFs, with OCR in front of it
rows, cost
=
extract_table(
"scanned.pdf"
,
LINE_ITEMS
,
model
=
"deepseek/deepseek-v4.1-flash"
,
engine
=
"mistral-ocr"
)
print
(
f
"mistral-ocr:
{len
(rows)
}
rows, $
{
cost
:.5f
}
"
)
# The same model with the free text parser
rows, cost
=
extract_table(
"scanned.pdf"
,
LINE_ITEMS
,
model
=
"deepseek/deepseek-v4.1-flash"
,
engine
=
"cloudflare-ai"
)mistral-ocr: 6 rows, $0.00211
ValueError: No rows came back. If this PDF is a scan, use a model that reads PDFs or engine='mistral-ocr'.é£ä¸ª ValueError æ¥èªç¬¬ 1 æ¥ä¸çç©ºè¡æ£æ¥ãå¦ææ²¡æè¯¥æ£æ¥ï¼ç¬¬äºæ¬¡è°ç¨ä¼è¿å 200 OKï¼å¹¶å¸¦æ finish_reason: "stop" å {"rows": []} ãè§£æå¨å¨æ«æä»¶ä¸æ¾ä¸å°ææ¬ï¼æ¨¡å乿£ç¡®å°æ¥åäºç©ºè¡¨æ ¼ã没æä»»ä½ä¸è¥¿æåºéè¯¯ï¼æä»¥æ²¡æè¯¥æ£æ¥çæµæ°´çº¿ä¼åå¨é¶è¡ï¼ç¶åç»§ç»å¤çä¸ä¸ä¸ªæä»¶ãè¿æ¯ä¸¤ä¸ªéé»å¤±è´¥ä¸ç第ä¸ä¸ªãæä»¬çç²ç¥æ«æè¡¨ç°ç¸åï¼30 è¡ä¸å¾äº 0 åã
å¦å¤ä¸¤ç§è·¯çº¿é误ä¼å¤§å£°å¤±è´¥ãæ¼éç弿åç§°ä¼è¿åè¿ä¸ªé误ã
400 {"error":{"message":"plugins.0.pdf.engine: Invalid input","code":400}}åç弿åªéç¨äºè½è¯»åæä»¶ç模åãå½æä»¬è¿æ¯å¨ DeepSeek V4.1 Flash ä¸è®¾ç½® engine="native" æ¶ï¼è¯·æ±æåäºä¸æ¬¡ï¼ç¶åä¸¤æ¬¡å æä¾åé误è失败ã
400 Provider returned error: "The request was rejected. Possible causes: input exceeds the model's maximum context length, or the request contains invalid parameters."å¨åä¸ä¸ªæ¨¡åä¸ä¸è®¾ç½® engine æ¶ï¼æä»¬è¯äºä¸¤æ¬¡é½æåäºï¼æä»¥ä¸è¦å¨ä¸è½è¯»åæä»¶ç模åä¸è®¾ç½® native ã
第 4 æ¥ï¼ä¸ºä»ä¹æçé¿è¡¨æ ¼è¿åæ¶è¢«æªæäºï¼
模åååºçæ¯ä¸è¡é½ä¼æ¶èè¾åº tokenï¼æä»¥é¿è¡¨æ ¼éè¦è¾é«ç max_tokens ãæä»¬é£ä»½ 58 è¡çåè´§æ¥å¿æ ¹æ®æ¨¡åä¸å使ç¨äº 2,500 å° 7,700 个è¾åº tokenï¼è¿è®©æå°å¦ç模åå¨ extract_table ä¸é»è®¤ç 8,000 ä¹ä¸å 乿²¡æä½éãç®ä¸æ¥æ¯è¡æ¯ 43 å° 133 个 tokenã对äºé¿è¡¨æ ¼ï¼ä»æ¯ä¸ªé¢æè¡çº¦ 150 个 token å¼å§ãç¶åæµéä½ èªå·±çè¡ï¼å¹¶ä¿ç finish_reason æ£æ¥ï¼å 为计æ°ä¼å 模åå schema èå¼ãä¸é¢æ¯éå¶å¤ªä½æ¶ä¼åççæ åµã
from
extract_tables
import
extract_table, rows_schema
SHIPMENTS
=
rows_schema({
"date"
: {
"type"
:
"string"
,
"description"
:
"YYYY-MM-DD"
},
"shipment_id"
: {
"type"
:
"string"
},
"origin"
: {
"type"
:
"string"
},
"destination"
: {
"type"
:
"string"
},
"weight_kg"
: {
"type"
:
"number"
,
"description"
:
"No thousands separator"
},
"status"
: {
"type"
:
"string"
},
})
rows, cost
=
extract_table(
"shipments.pdf"
,
SHIPMENTS
,
max_tokens
=
600
)ValueError: Output stopped at max_tokens=600. Raise it or split the PDF.å¦ææ²¡æ finish_reason æ£æ¥ï¼è¿ä¸ªè¯·æ±ä¼ä»¥ä¸¤ç§æ¹å¼ä¹ä¸å¤±è´¥ãæ®é JSON è§£æä¼å¨è¢«æªæçæ°ç»ä¸æåºé误ã
JSONDecodeError: Unterminated string starting at: line 1 column 627 (char 626)第äºç§æ¹å¼æ¯å¦ä¸ä¸ªéé»å¤±è´¥ãå¼å¯ Response Healing æä»¶åï¼JSON è½è§£æï¼ä½ ä¼å¾å° 58 è¡ä¸ç 5 è¡ï¼ä¸æ²¡æå¼å¸¸ãHealing ä¼ä¿®å¤è¾åºçè¯æ³ï¼å ¶ææ¡£è¦åè¯´å®æ æ³æ¢å¤è¢« max_tokens æªæçè¾åºï¼æä»¥å®æ æ³æ·»å 模å仿ªååºçè¡ã finish_reason æ£æ¥è½æè·è¿ä¸¤ç§æ åµã
第 5 æ¥ï¼å¦ä½æè¡åå ¥ CSVï¼
æ¯ä¸è¡é½æ schema ä¸å®ä¹çé®ï¼å æ¤ csv.DictWriter å¯ä»¥ä» schema ä¸è·åååï¼å¹¶æåæ ·åå ¥åè¡ãExcel å¯ä»¥ç´æ¥æå¼ç»æã
import
csv
from
extract_tables
import
extract_table
# Define SHIPMENTS here, copied from step 4
rows, cost
=
extract_table(
"shipments.pdf"
,
SHIPMENTS
,
model
=
"openai/gpt-6-luna"
)
with
open
(
"shipments.csv"
,
"w"
,
newline
=
""
)
as
f:
fieldnames
=
list
(
SHIPMENTS
[
"properties"
][
"rows"
][
"items"
][
"properties"
])
writer
=
csv.DictWriter(f,
fieldnames
=
fieldnames)
writer.writeheader()
writer.writerows(rows)
print
(
f
"Wrote
{len
(rows)
}
rows to shipments.csv ($
{
cost
:.5f
}
)"
)Wrote 58 rows to shipments.csv ($0.00130)
date,shipment_id,origin,destination,weight_kg,status
2026-09-01,SHP-88200,Savannah,Valencia,8754.6,In transit
2026-09-01,SHP-88213,Busan,Los Angeles,2074.7,Customs holdè¡¨æ ¼å»¶ä¼¸å°äºç¬¬äºé¡µï¼å¹¶å¨é£ééå¤äºè¡¨å¤´ãCSV åªæä¸ä¸ªè¡¨å¤´ï¼å å«å ¨é¨ 58 è¡ã
ä½ åºè¯¥ä½¿ç¨åªä¸ªæ¨¡åï¼
æä»¬å¯¹å ¨é¨äºä¸ª PDF åè¿è¡äºä¸æ¬¡æ¯æ¡è·¯çº¿ï¼å ±å 页ã644 个åå æ ¼ï¼å¹¶å°æ¯ä¸ªåå æ ¼ä¸çæè¿äº PDF æ¶æç¨çæ°æ®è¿è¡äºæ¯è¾ãæ¯æ¬¡è¿è¡é½ä½¿ç¨äºä¸é¢ç extract_table 彿°åæç¤ºè¯ï¼å¹¶éç¨æä»¬çé»è®¤æä¾åè·¯ç±ãæ¯ 1,000 é¡µçææ¬æ¯å é¡µçæ» usage.cost ææ¯ä¾æ¾å¤§åçç»æãæ¨¡ååä»·æ ¼æ¥èªæä»¬æªè³ 2026 å¹´ 10 æ 8 æ¥ç模åç®å½ãè¿äºç»ææè¿°çæ¯è¿ä¸ªæµè¯éï¼è鿍¡åçä¸è¬æ§è½ã
å¨è¿ä¸ªæµè¯éä¸ï¼ GPT-6 Luna å Claude Haiku 5.5 æ¯è®©æ¯ä¸ªåå æ ¼é½æ£ç¡®çæä¾¿å®è·¯çº¿ï¼å æ¬å¨ç²ç³æ«æä»¶ä¸ï¼ä¸¤è çæ ä»·å为æ¯ç¾ä¸è¾å ¥ token 0.10 ç¾å ãè¿ä½¿å¾å®ä»¬æä¸ºå¨ä½ èªå·±ç PDF ä¸è¿è¡é¦æ¬¡æµè¯çåçéæ©ã Gemini 3.8 Flash åæ ·åç¡®ï¼ä½å¨ç¸åæä»¶ä¸æ¯é¡µææ¬é«åºå «å°ååãå ¶æ 价为æ¯ç¾ä¸è¾å ¥ token 1.50 ç¾å ï¼2026 å¹´ 10 æ 21 æ¥ææ£è³ 0.75 ç¾å ã
OCR è·¯çº¿çææ¬é«äº DeepSeek ä½ token ä»·æ ¼ææç¤ºçæ°´å¹³ï¼å ä¸ºæ¯ 1,000 页 2 ç¾å ç OCR è´¹ç¨ä¼å å°æ¯ä¸ªè¯·æ±ä¸ï¼ä½ å¯ä»¥å¨ usage.cost ä¸çå°è¿ä¸ç¹ãå®ç两å¤éè¯¯æ¯ Enterprise å Hardware resale ä¸çèæ³¨æ è®°ï¼å°½ç®¡ schema è¦æ±ç§»é¤å®ä»¬ï¼å®å´ä¿çäºãå 费解æå¨æ£ç¡®å¤çäºå ¨é¨ä¸ä¸ªæ°å PDFï¼ä½ä¸¤ä¸ªæ«æä»¶å®å ¨é误ã
ææ¬åæ¯æ ¹æ®æ¯æ¡è·¯çº¿å 页æ¨ç®åºæ¥çãç¨å®æ¥æ¯è¾è·¯çº¿ï¼ç¶åå¨éæ©ä¹åï¼ä½¿ç¨ é»éè¯ä¼°é å¨ä½ èªå·±ç PDF æ ·æ¬ä¸æµéåç¡®çãæ¶é´åææ¬ï¼å 为æ´å¯éçé¡µé¢ææ¬æ´é«ï¼å ¶ä»å¸å±å¯è½è¡¨ç°ä¸åã
ä»ä¹æ¶å PDF åºå°±è¶³å¤äºï¼
å¦æä½ çææ PDF 齿¯æ°åçï¼å¹¶ä¸æ¯ä¸ªè¡¨æ ¼åªæä¸ä¸ªè¡¨å¤´è¡ï¼é£ä¹å pdfplumber è¿æ ·çå è´¹åºå°±è½æ¾å°è¡¨æ ¼ãæä»¬å¨ç¸åæä»¶ä¸è¿è¡äºå®ãå®å¨ä¸ä¸ªæ°å PDF 䏿¾å°äºæ¯ä¸ªè¡¨æ ¼ï¼å¹¶å°æ¯ä¸ªåå æ ¼ä½ä¸ºå符串è¿åï¼ä¾å¦ â6,067.00â æ â(3,878)âãå£åº¦æ¥åç两级表头è¿å为两è¡ï¼åå¹¶åå æ ¼ä¸ä¸º None ï¼èåè´§æ¥å¿è¿å为两个ç¬ç«çè¡¨æ ¼ï¼æ¯é¡µä¸ä¸ªãå®å¨ä¸¤ä¸ªæ«æä»¶ä¸é½æ²¡ææ¾å°ä»»ä½å 容ï¼å ä¸ºæ«æä»¶æ²¡æå¯è¯»åçææ¬å±ã
å æ¤ï¼å½ä½ çè¡¨æ ¼ç®åä¸ PDF æ¯æ°åçæ¶ï¼ä¸ä¸ªåºå°±è¶³å¤äºã䏿¦ä½ å¼å§ä¸ºå并表头ãåé¡µåæ°åæ ¼å¼ç¼åæ¸ ç代ç ï¼ç¬¬ 2 æ¥ä¸ç schema ç»´æ¤èµ·æ¥å·¥ä½éæ´å°ã
TypeScript ä¸çç¸å请æ±
请æ±ä½å¨ä»»ä½è¯è¨ä¸é½æ¯ç¸åçãå°å ¶ä¿å为 extract.mjs ï¼å¹¶å¨ Node.js 18 ææ´é«çæ¬ä¸ä½¿ç¨ node extract.mjs invoice.pdf è¿è¡å®ã
import
{ readFileSync }
from
"node:fs"
;
import
{ basename }
from
"node:path"
;
const
path
=
process.argv[
2
];
const
fileData
=
`data:application/pdf;base64,${
readFileSync
(
path
).
toString
(
"base64"
)
}`
;
const
money
=
{ type:
"number"
, description:
"USD, no currency symbol or thousands separator"
};
const
schema
=
{
type:
"object"
,
properties: { rows: { type:
"array"
, items: {
type:
"object"
,
properties: { sku: { type:
"string"
}, description: { type:
"string"
},
qty: { type:
"integer"
}, unit_price: money, amount: money },
required: [
"sku"
,
"description"
,
"qty"
,
"unit_price"
,
"amount"
],
additionalProperties:
false
} } },
required: [
"rows"
],
additionalProperties:
false
,
};
const
res
=
await
fetch
(
"https://openrouter.ai/api/v1/chat/completions"
, {
method:
"POST"
,
headers: { Authorization:
`Bearer ${
process
.
env
.
OPENROUTER_API_KEY
}`
,
"Content-Type"
:
"application/json"
},
body:
JSON
.
stringify
({
model:
"openai/gpt-6-luna"
,
messages: [{ role:
"user"
, content: [
{ type:
"text"
, text:
"Extract every row of the table in this PDF. Include rows from every page. Return only rows that are in the document."
},
{ type:
"file"
, file: { filename:
basename
(path), file_data: fileData } },
] }],
response_format: { type:
"json_schema"
, json_schema: { name:
"table"
, strict:
true
, schema } },
}),
});
if
(
!
res.ok)
throw
new
Error
(
`HTTP ${
res
.
status
}: ${
await
res
.
text
()
}`
);
const
data
=
await
res.
json
();
const
choice
=
data.choices[
0
];
if
(choice.finish_reason
!==
"stop"
)
throw
new
Error
(
`Unexpected finish_reason: ${
choice
.
finish_reason
}`
);
const
{
rows
}
=
JSON
.
parse
(choice.message.content);
if
(rows.
length
===
0
)
throw
new
Error
(
"No rows came back."
);
console.
log
(rows.
length
+
" rows, finish_reason="
+
choice.finish_reason
+
", $"
+
data.usage.cost);10 rows, finish_reason=stop, $0.00020275èæ¬æ£æ¥ HTTP ç¶æï¼ç¶ååºç¨ä¸ Python çæ¬ç¸åçä¸¤é¡¹æ£æ¥ãé¤é finish_reason 为 âstopâ ä¸ rows è³å°æä¸è¡ï¼å¦åå®ä¼åæ¢ã
ç»è®º
ä½¿ç¨ LLM ä» PDF 䏿åè¡¨æ ¼åªéä¸ä¸ªè¯·æ±ï¼å° PDF ä½ä¸ºæä»¶é¨åï¼å¹¶ä½¿ç¨æè¿°ä¸è¡çä¸¥æ ¼ schemaãå¨æä»¬çæµè¯ä¸ï¼åç¡®çæ¥èªæ¨¡åç´æ¥è¯»å PDFï¼èé£é©æ¥èª PDF æèµ°ç路线ãå¯¹æ«æä»¶ä½¿ç¨ææ¬è§£æå¨ä»¥å对é¿è¡¨æ ¼ä½¿ç¨ token éå¶ï¼é½è¿åäºçèµ·æ¥æ£å¸¸çååºã extract_table ä¸çä¸¤é¡¹æ£æ¥è½æè·è¿ä¸¤ç§æ åµï¼å æ¤è¯·å¨ä½ è¿è¡çä»»ä½ä»£ç çæ¬ä¸ä¿çå®ä»¬ãå¨ç¡®å®è·¯çº¿ä¹åï¼ç¨ä½ èªå·±ç PDF æ ·æ¬è¿è¡å®ï¼å¹¶æ¯è¾è¡ãæ¶é´å usage.cost ã
常è§é®é¢
LLM è½è¯»åæ«æç PDF åï¼
å¯ä»¥ï¼åææ¯æ¨¡åç´æ¥è¯»å PDFï¼æè ä½ éè¿ mistral-ocr è§£æå¨åéãææä¸ä¸ªè½è¯»å PDF çæ¨¡å齿£ç¡®è¯å«äºæä»¬çä¸¤ä»½æ«æä»¶ï¼DeepSeek V4.1 Flash é å mistral-ocr 乿¯å¦æ¤ãå cloudflare-ai è¿æ ·çææ¬è§£æå¨å¯¹æ«æä»¶ä¼è¿åç©ºç»æä¸ä¸æ¥éï¼å ä¸ºæ«æä»¶æ²¡æææ¬å±ã
卿们çå 个æµè¯é¡µé¢ä¸ï¼æ 2026 å¹´ 10 æçä»·æ ¼è®¡ç®ï¼ä½¿ç¨ GPT-6 Luna æ¶ææ¬æç®çº¦ä¸ºæ¯ 1,000 页 $0.77ï¼ä½¿ç¨ Claude Haiku 5.5 æ¶ä¸º $1.00ãç»ç± mistral-ocr çæ¨¡åæ¯ 1,000 页é¢å¤å¢å $2 ç OCR è´¹ç¨ã请æ¥çå½åä»·æ ¼ï¼å¹¶æ ¹æ®ä½ èªå·±ç馿¹è¯·æ±è¯»å usage.cost ï¼å 为å¯éè¡¨æ ¼çææ¬æ´é«ã
模åä¼ç¼é PDF ä¸ä¸åå¨çè¡åï¼
卿们坹äºä»½æµè¯ PDF ç 25 次æåä¸ï¼æ²¡æçå°ç¼é çè¡ï¼ä½è¿å¹¶ä¸è½æé¤å¨å ¶ä»ææ¡£ä¸åºç°è¿ç§æ åµãæ´å¤§çé£é©æ¯æ¼è¡ï¼åå å¯è½æ¯è¾åºå¨ token ä¸éå¤è¢«æªæï¼æè è§£æå¨æ²¡ææ¾å°ææ¬ã请å°è¡æ°æå°è®¡ä¸ææ¡£è¿è¡æ¯å¯¹ã
对äºè¡¨å¤´ç®åçæ°åç PDFï¼pdfplumber å è´¹ï¼å¹¶ä¸å¨æä»¬ææçæ°åçæµè¯æä»¶ä¸é½æ¾å°äºæ¯ä¸å¼ è¡¨æ ¼ãå®å°æ¯ä¸ªåå æ ¼ä½ä¸ºææ¬è¿åï¼æå¤çº§è¡¨å¤´æåæåç¬çè¡ï¼èå¨æä»¬çæ«æä»¶ä¸æ²¡ææ¾å°ä»»ä½å 容ãCamelot å Tabula æ¯å ¶ä»å è´¹é项ï¼ä½æä»¬å¨æ¬æå䏿²¡ææµè¯å®ä»¬ã
åç»æ¥éª¤
- PDF è¾å ¥ ä»ç»äºå¦ä½éè¿ URL èé base64 åé PDFãè§£æå¨å¼æåå ¶ä»·æ ¼ï¼ä»¥åæä»¶æ³¨è§£ââå®è®©ä½ å¯ä»¥æè§£æç»æååå»ï¼è¿æ ·å°±ä¸å¿ 为åä¸ä¸ª PDF é夿¯ä»è§£æè´¹ç¨ã
- ç»æåè¾åº è§£éäºä¸¥æ ¼ç JSON schemaï¼ä»¥åå¦ä½åªè·¯ç±å°æ¯æå®ä»¬çæä¾åã
- é¢è®¾ å°æ¨¡åãç³»ç»æç¤ºè¯åæä¾å设置åå¨å¨ä¸ä¸ªåç§°ä¸ãææåæä»¤æ¾è¿é¢è®¾çç³»ç»æç¤ºè¯ä¸ï¼å¹¶å¨æ¯ä¸ªæå¡ä¸è°ç¨ @preset/your-name ï¼è¿æ ·å®ä»¬é½ä½¿ç¨ç¸åçé ç½®ãæ¯æ¬¡è¯·æ±æ¶ä»è¦å¨ response_format ä¸åé schemaï¼å 为é¢è®¾ææ¡£å¹¶æªå°å ¶åå¨é¢è®¾æåå¨ç设置ä¸ãæä»¬ç é¢è®¾æå ä»ç»äºå ·ä½è®¾ç½®æ¥éª¤ã
- 对äºç§¯åç PDFï¼ Batch API ä¼å¼æ¥è¿è¡ç¸åç请æ±ï¼éå¸¸å¨ 24 å°æ¶å 宿ã anthropic/claude-haiku-5.5:batch å openai/gpt-6-luna:batch ææ åæ¯ token ä»·æ ¼çä¸åè®¡è´¹ãæ¹å¤ç请æ±ä»¥å ¬å ± URL èé base64 æ¥æ¶ PDFï¼å æ¤éè¦å æç®¡æä»¶ãæä»¶ URL ä» å¨ååºæä»¶è¾å ¥ç模åä¸å¯ç¨ï¼å æ¤ç¬¬ 3 æ¥ä¸ DeepSeek é å mistral-ocr çæ¹æ¡å¨æ¹å¤çä¸ä¸å¯ç¨ï¼èå¨ OpenAI 模åä¸åªè½éè¿ Responses API 使ç¨ãæä»¬å¨æ¬æå䏿²¡ææµéæ¹å¤çè¿è¡æ åµã
- å¦æä½ å¨ LangChain ä¸æå»ºæ¤åè½ï¼ ChatOpenRouter ä¼éä¼ PDF è¾å ¥ï¼å¹¶æ¯æä½¿ç¨ with_structured_output çä¸¥æ ¼ JSON schemaã
- å¦æä½ çæºææ¡£æ¯å¾çèé PDFï¼ å¦ä½å LLM åéå¾ç ä»ç»äºå¾çå 容é¨åã
æ¥æºï¼ OpenRouterï¼Announcements · openrouter.ai
OpenRouter
夿¨¡æ
æç¨å®è·µ
#
æç¨/å®è·µ