 |
VFP 愛用者社區 本討論區為 Visual Foxpro 愛用者經驗交流的地方, 請多多利用"搜尋"的功能, 先查看看有無前例可循, 如果還有不懂的再發問. 部份主題有附加檔案, 須先註冊成為社區居民才可以下載.
|
上一篇主題 :: 下一篇主題 |
發表人 |
內容 |
GOLDHAN
註冊時間: 2004-06-07 文章: 10
第 1 樓
|
發表於: 星期三 八月 13, 2025 10:54 pm 文章主題: 請教如何下載網頁生成的PDF檔案問題 |
|
|
國稅局有提供線上繳款書產生列印功能
https://www.etax.nat.gov.tw/etwmain/etw144w/152
填入相關資訊後,即可生成PDF檔,可自行列印後去繳款
因網頁填入的資料會寫入國稅局資料庫,並產生繳款書上對應的繳款書號
所以沒辦法依樣畫葫蘆產生相似的label來列印
目前此動作重複性太高,想用程式代替填寫並直接下載繳款書檔
用xmlhttp方式,雖然可以用POST 方式 把要填入的內容用 send(JSON) 送出
並得到回傳但是因為response.txt 非二進位內容,無法正確 轉成PDF 檔
但用 URLDownloadtoFile 似乎又不適用,只能單一網址,無法將完整的body內容送出
能否請各位前輩指點一條明路 感謝萬分 |
|
回頂端 |
|
 |
CPS0204
註冊時間: 2014-08-24 文章: 480
第 2 樓
|
發表於: 星期二 九月 16, 2025 3:18 pm 文章主題: |
|
|
以下是 付費版chat gpt
可以的,重點是「要用支援 cookie/CSRF、且能把回應當二進位儲存」的方式,不要用只會回傳文字的介面。下面提供一條穩定的做法(以你常用的 Windows/VFP9 為主),外加 PowerShell / Python 兩個備用方案。
思路分 4 步:先抓表單頁 → 取隱藏欄位/Token + Cookie → 送出表單 → 以同一個工作階段下載 PDF(用二進位寫檔)。
---
# 做法總覽(務必照順序)
1. 手動操作一次,用瀏覽器開發者工具(F12 → Network)觀察:
* 生成繳款書時實際打到哪個 **POST URL**。
* 送出的 **Content-Type**(多半是 `application/x-www-form-urlencoded` 或 `application/json`)。
* 是否帶 **CSRF/Anti-Forgery token**、隱藏欄位(例如 `__RequestVerificationToken`)。
* 伺服器是否先回 **JSON(含檔案網址/檔名)**,或直接 **302 轉址到 PDF**。
* 下載 PDF 時是否需要 **Referer**、**Cookie(如 JSESSIONID)**。
2. 程式自動化時一定先 **GET 表單頁**,拿到:
* Cookie(如 `Set-Cookie: JSESSIONID=...`)
* 隱藏欄位 / Token(從 HTML 抓)
3. 以 **同一組 Cookie**、正確 Content-Type、帶齊必要欄位去 **POST**。
4. 伺服器多半會:
* a) 回傳 JSON 裡有一個下載網址(再用 GET 抓 PDF),或
* b) 直接回傳 PDF(二進位內容)。
無論 a/b,都要用「二進位串流」把回應寫成 `.pdf` 檔。
---
# VFP 9 範例(WinHTTP + ADODB.Stream)
> 特色:支援 TLS、可自行帶 Cookie/Headers、`ResponseBody` 可拿到位元組陣列。
```foxpro
*--- 儲存二進位到檔案的小工具
FUNCTION SaveBytesToFile(tcPath, taBytes)
LOCAL loStream
loStream = CREATEOBJECT("ADODB.Stream")
loStream.Type = 1 && adTypeBinary
loStream.Open
loStream.Write(taBytes)
loStream.SaveToFile(tcPath, 2) && adSaveCreateOverWrite
loStream.Close
RETURN .T.
ENDFUNC
*--- 取隱藏欄位的小工具(簡易字串抓取,可改用正則)
FUNCTION HtmlBetween(tcHtml, tcStart, tcEnd)
LOCAL lnPos1, lnPos2
lnPos1 = AT(tcStart, tcHtml)
IF lnPos1 = 0
RETURN ""
ENDIF
lnPos1 = lnPos1 + LEN(tcStart)
lnPos2 = AT(tcEnd, SUBSTR(tcHtml, lnPos1))
IF lnPos2 = 0
RETURN ""
ENDIF
RETURN SUBSTR(tcHtml, lnPos1, lnPos2-1)
ENDFUNC
PROCEDURE GetPaySlipPdf
LOCAL lcFormUrl, lcPostUrl, lcPdfOut, loHttp, lcHtml, lcCookies, lcToken, lcBody, laPdf
lcFormUrl = "https://www.etax.nat.gov.tw/etwmain/etw144w/152" && 表單頁
lcPostUrl = "<把 Network 面板看到的實際處理 URL 貼上>" && 可能不同
lcPdfOut = "C:\Temp\Payslip.pdf"
loHttp = CREATEOBJECT("WinHttp.WinHttpRequest.5.1")
loHttp.Option(6) = .T. && 自動跟隨 Redirects
* 1) 先 GET 表單頁,取得 Cookie + Hidden Token
loHttp.Open("GET", lcFormUrl, .F.)
loHttp.SetRequestHeader("Accept", "text/html,application/xhtml+xml")
loHttp.Send()
lcHtml = loHttp.ResponseText
* 收集 Cookie(簡單做法:從回應標頭抓 Set-Cookie)
LOCAL lcHdr
lcHdr = loHttp.GetAllResponseHeaders()
lcCookies = ""
LOCAL lnPos, lnCrLf
lnPos = AT("Set-Cookie:", lcHdr)
DO WHILE lnPos > 0
lnCrLf = AT(CHR(13)+CHR(10), SUBSTR(lcHdr, lnPos))
lcCookies = lcCookies + " " + ;
LEFT(SUBSTR(lcHdr, lnPos+11), lnCrLf-11)
lcHdr = STREXTRACT(lcHdr, CHR(13)+CHR(10), "", 1, 1) && 往後切
lnPos = AT("Set-Cookie:", lcHdr)
ENDDO
lcCookies = STRTRAN(lcCookies, CHR(13)+CHR(10), "")
* 如果頁面有 Anti-Forgery Token(舉例),從 HTML 擷取
lcToken = HtmlBetween(lcHtml, 'name="__RequestVerificationToken" value="', '"')
* 以上視實際頁面為準,沒有就留空
* 2) 準備 POST 的表單內容
* 如果是 application/x-www-form-urlencoded:
* 依照 Network 面板看到的欄位組出字串(要 URL-Encode)
* 例如:caseNo=...&payerId=...&amount=...&__RequestVerificationToken=...
lcBody = "fieldA=valueA&fieldB=valueB"
IF !EMPTY(lcToken)
lcBody = lcBody + "&__RequestVerificationToken=" + lcToken
ENDIF
loHttp.Open("POST", lcPostUrl, .F.)
loHttp.SetRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8")
loHttp.SetRequestHeader("Referer", lcFormUrl)
IF !EMPTY(lcCookies)
* 將多個 cookie 串成一行:name=value; name2=value2...
loHttp.SetRequestHeader("Cookie", lcCookies)
ENDIF
loHttp.SetRequestHeader("Accept", "application/json,application/pdf,*/*")
loHttp.Send(lcBody)
* 3) 依伺服器行為分流:
LOCAL lcCT
lcCT = UPPER(loHttp.GetResponseHeader("Content-Type"))
IF "APPLICATION/PDF" $ lcCT
* 直接拿到 PDF(二進位 ResponseBody)
laPdf = loHttp.ResponseBody
=SaveBytesToFile(lcPdfOut, laPdf)
MESSAGEBOX("PDF 已儲存:" + lcPdfOut)
RETURN
ENDIF
* 如果回 JSON,解析出下載網址 downloadUrl(以下僅為範例)
LOCAL lcJson, lcDownloadUrl
lcJson = loHttp.ResponseText
lcDownloadUrl = STREXTRACT(lcJson, '"downloadUrl":"', '"')
IF EMPTY(lcDownloadUrl)
MESSAGEBOX("未取得下載網址,請檢查 POST 欄位/Token/Headers。")
RETURN
ENDIF
* 4) 以同一組 Cookie 去 GET 下載網址,並把回應當 PDF 寫檔
loHttp.Open("GET", lcDownloadUrl, .F.)
loHttp.SetRequestHeader("Referer", lcFormUrl)
IF !EMPTY(lcCookies)
loHttp.SetRequestHeader("Cookie", lcCookies)
ENDIF
loHttp.SetRequestHeader("Accept", "application/pdf,*/*")
loHttp.Send()
IF "APPLICATION/PDF" $ UPPER(loHttp.GetResponseHeader("Content-Type"))
laPdf = loHttp.ResponseBody
=SaveBytesToFile(lcPdfOut, laPdf)
MESSAGEBOX("PDF 已儲存:" + lcPdfOut)
ELSE
MESSAGEBOX("下載回應非 PDF,請再檢查流程(可能需要額外查詢碼或 Referer)。")
ENDIF
ENDPROC
```
### 為什麼你之前用 `responseText` 不行?
* 因為 PDF 是二進位資料;`responseText` 會以字串(且可能走錯誤編碼)處理,檔案必毀。
* 需用 `ResponseBody`(位元組陣列)→ `ADODB.Stream` 以 **Binary** 寫入檔案。
### 關鍵眉角
* **Cookie 與 Token**:一定要延續第一次 GET 拿到的 Cookie/Token(含 CSRF、JSESSIONID 之類)。
* **Content-Type**:和瀏覽器送出的一模一樣(form-url-encoded 或 JSON)。
* **Referer / Origin**:有些站會檢查,照實送。
* **Redirect / 檔名**:有時會 302 到實體 PDF;也可從 `Content-Disposition` 抓檔名。
* **壓縮**:WinHTTP 會處理常見 gzip/deflate,通常不用手動解壓。
* **HTTPS/TLS**:Windows 新版環境預設支援 TLS 1.2;老機器若握手失敗需更新系統或改用 PowerShell/Python。
---
# 備案 A:PowerShell(最省事,易維護)
```powershell
$FormUrl = "https://www.etax.nat.gov.tw/etwmain/etw144w/152"
$Session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
# 1) 取表單頁(拿 Cookie + Token)
$pg = Invoke-WebRequest -Uri $FormUrl -WebSession $Session
# 若需要 Token,從 $pg.Content 用正則抓出來(視實際頁面)
# $token = [regex]::Match($pg.Content, 'name="__RequestVerificationToken" value="([^"]+)"').Groups[1].Value
# 2) 組表單
$body = @{
fieldA = 'valueA'
fieldB = 'valueB'
# __RequestVerificationToken = $token
}
# 3) 送出
$res = Invoke-WebRequest -Uri "<POST_URL>" -Method Post -Body $body `
-ContentType "application/x-www-form-urlencoded; charset=UTF-8" `
-WebSession $Session -Headers @{Referer=$FormUrl; Accept="application/json,application/pdf,*/*"}
# 4) 若回 JSON,取下載網址;若直接是 PDF,用 -OutFile 儲存
if ($res.Headers.'Content-Type' -like '*application/pdf*') {
[IO.File]::WriteAllBytes("C:\Temp\Payslip.pdf", $res.Content)
} else {
$json = $res.Content | ConvertFrom-Json
$pdfUrl = $json.downloadUrl
Invoke-WebRequest -Uri $pdfUrl -OutFile "C:\Temp\Payslip.pdf" -WebSession $Session -Headers @{Referer=$FormUrl; Accept="application/pdf,*/*"}
}
```
---
# 備案 B:Python(requests)
```python
import re, requests, os
s = requests.Session()
form_url = "https://www.etax.nat.gov.tw/etwmain/etw144w/152"
# 1) 取表單頁
r = s.get(form_url)
r.raise_for_status()
# 需要的話抓 token
# m = re.search(r'name="__RequestVerificationToken" value="([^"]+)"', r.text)
# token = m.group(1) if m else ""
# 2) 送出表單
post_url = "<POST_URL>"
payload = {
"fieldA": "valueA",
"fieldB": "valueB",
# "__RequestVerificationToken": token,
}
headers = {"Referer": form_url, "Accept": "application/json,application/pdf,*/*"}
r2 = s.post(post_url, data=payload, headers=headers)
r2.raise_for_status()
def save_pdf(resp, path):
with open(path, "wb") as f:
f.write(resp.content)
if "application/pdf" in r2.headers.get("Content-Type", "").lower():
save_pdf(r2, r"C:\Temp\Payslip.pdf")
else:
j = r2.json()
pdf_url = j["downloadUrl"]
r3 = s.get(pdf_url, headers={"Referer": form_url})
r3.raise_for_status()
save_pdf(r3, r"C:\Temp\Payslip.pdf")
```
---
## 常見坑位
* **CAPTCHA/圖形驗證**:若流程某步驟有人機驗證,就需要改走「自動化瀏覽器」(Playwright / Chrome DevTools Protocol)。
* **一次性 Token / 到期**:Token 或工作階段過期會回錯誤頁,請確保「GET → POST → 下載」整串在同一 Session、且時效內完成。
* **`URLDownloadToFile` 為何不適合**:它無法隨意帶 Headers/Cookies/Body,只適合「已經是最終檔案網址且不需驗證」的情境。你可以在**先取得最終下載 URL 與 Cookie**之後,再改用它,但通常實務上不如 WinHTTP/PowerShell/requests 彈性。
---
如果你把 Network 面板看到的 **POST URL、必要欄位名** 提給我(可遮蔽敏感值),我可以直接幫你把上面的 VFP 範例改成「可用版」並對應你們要填的欄位,一次就能下載出 PDF。也可以選擇 PowerShell 方案,維護成本更低、對 IT 友善。 |
|
回頂端 |
|
 |
|
|
您 無法 在這個版面發表文章 您 無法 在這個版面回覆文章 您 無法 在這個版面編輯文章 您 無法 在這個版面刪除文章 您 無法 在這個版面進行投票 您 無法 在這個版面附加檔案 您 無法 在這個版面下載檔案
|
|