這是一個在瀏覽器使用的使用者腳本,可以讓你簡單的在 Pixiv 上面存圖
載點與原始碼
使用教學請點進上方的 GreasyFork 中閱讀,本文主要以介紹原理為主
功能
- 存單圖
- 存多圖為 zip
- 存漫畫為 zip
- 存動圖為 gif
- 自訂檔案名稱格式
- 存圖只需按一個鍵
- 支援多頁面,包括作品頁、首頁、收藏、搜尋等等...
- 與 Patchouli
腳本相容
架構
以下是本腳本的基本執行流程:
按下鍵盤的事件 -> 取得目前選取的圖片 id -> 取得圖片資訊 ->
根據種類個別下載圖片
按下鍵盤並取得圖片 id
v0.6.0 的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| { const SELECTOR_MAP = { '/': 'a.work:hover,a._work:hover,.illust-item-root>a:hover', '/bookmark.php': 'a.work:hover,.image-item-image>a:hover', '/new_illust.php': 'a.work:hover,.image-item-image>a:hover', '/bookmark_new_illust.php': 'figure>div>a:hover,.illust-item-root>a:hover', '/member_illust.php': 'div[role=presentation]>a:hover,canvas:hover', '/ranking.php': 'a.work:hover,.illust-item-root>a:hover', '/search.php': 'figure>div>a:hover', '/member.php': '[href^="/member_illust.php"]:hover,.illust-item-root>a:hover' } const selector = SELECTOR_MAP[location.pathname] addEventListener('keydown', e => { if (e.which !== KEYCODE_TO_SAVE) return e.preventDefault() e.stopPropagation() let id if (!id && $('#Patchouli')) { const el = $('.image-item-image:hover>a') if (!el) return id = /\d+/.exec(el.href.split('/').pop())[0] } else if (typeof selector === 'string') { const el = $(selector) if (!el) return if (el.href) id = /\d+/.exec(el.href.split('/').pop())[0] else id = new URLSearchParams(location.search).get('illust_id') } else { id = selector() } if (id) saveImage(FORMAT, id).catch(console.error) }) }
|
其中可以很明顯的知道它在收到keydown
的事件時先檢查是否與設定好的按鍵相符,不符則直接忽略
下方會根據目前的頁面從上面的SELECTOR_MAP
取得特定得選擇器,而該選擇器都會選到正在hover
狀態的<a>
元素,並從其href
中取得圖片的
id 而若是使用者有安裝 Patchouli
腳本,則會直接使用選擇器.image-item-image:hover>a
從它的圖片列表中選取圖片
在取得 id 後會直接呼叫saveImage
函數去儲存圖片
saveImage 函數
v0.6.0 的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| const saveImage = async ({ single, multiple }, id) => { const illustData = await getIllustData(id) let results const { illustType } = illustData switch (illustType) { case 0: case 1: { const url = illustData.urls.original const ext = url .split('/') .pop() .split('.') .pop() if (illustData.pageCount === 1) { results = [[single(illustData) + '.' + ext, await getCrossOriginBlob(url)]] } else { const len = illustData.pageCount const ar = [] for (let i = 0; i < len; i++) { ar.push( Promise.all([ multiple(illustData, i) + '.' + ext, getCrossOriginBlob(url.replace('p0', `p${i}`)) ]) ) } results = await Promise.all(ar) } } break case 2: { const fname = single(illustData) const numCpu = navigator.hardwareConcurrency || 4 const gif = new GIF({ workers: numCpu * 4, quality: 10 }) const ugoiraMeta = await getUgoiraMeta(id) const ugoiraZip = await xf.get(ugoiraMeta.originalSrc).blob() const { files } = await JSZip.loadAsync(ugoiraZip) const gifFrames = await Promise.all(Object.values(files).map(f => f.async('blob').then(blobToImg))) const getGif = (data, frames) => new Promise((res, rej) => { for (let i = 0; i < frames.length; i++) { gif.addFrame(frames[i], { delay: data.frames[i].delay }) } gif.on('finished', x => { console.timeEnd('gif') res(x) }) gif.on('error', rej) gif.render() }) results = await [[fname + '.gif', await getGif(ugoiraMeta, gifFrames)]] } } if (results.length === 1) { const [f, blob] = results[0] downloadBlob(blob, f) } else { const zip = new JSZip() for (const [f, blob] of results) { zip.file(f, blob) } const blob = await zip.generateAsync({ type: 'blob' }) const zipname = single(illustData) downloadBlob(blob, zipname) } }
|
取得圖片資訊
這部分的程式碼被我寫成函數了,可以在 L90-L92
看到 它會發送GET /ajax/illust/:id
形式的請求取得 json
格式的圖片資訊,並依照其中的illustType
分類,0
和1
分別是插畫與漫畫,而2
代表的是動圖
動圖方面則另外發送GET /ajax/illust/:id/ugoira_meta
的請求取得資訊
單圖、多圖與漫畫
這在上方的case 0:
case 1:
的部分
先從illustData.urls.original
取得網址與副檔名,然後依照圖片數量選擇要呼叫FORMAT.single
還是FORMAT.multiple
得到檔案名稱,並將結果存到results
陣列裡
results
陣列的格式大略為Pair<name,blobdata>[]
,然後在下方依照其數量選擇直接下載或壓縮為zip
再下載
壓縮檔是使用 JSZip
處裡的,支援壓縮和解壓縮
動圖
在case 2:
的部分
這會先從ugoiraMeta.originalSrc
取得一個壓縮檔的網址,然後取得其內容並將它解壓縮
再來會用額外的 gif.js
產生一個GIF
的物件,其workers
數量是 CPU
數量的四倍
再來後面會利用到解壓縮出來的檔案frames
和各張圖所持續的時間資料ugoiraMeta.frames
去呼叫gif.addFrame
函數
當圖片處理好之後就直接回傳它的 blobdata 存到 results
陣列裡面,接下來做的事就和上面一樣了
getCrossOriginBlob 函數
v0.6.0 的程式碼:
1
| const getCrossOriginBlob = (url, Referer = 'https://www.pixiv.net/') => gxf.get(url, { headers: { Referer } }).blob()
|
其實可以發現我在抓圖片時會使用這個函數,原因是我需要取得圖片資料,但圖片的網域在pximg.net
,直接發送請求會有同源政策的問題
而使用腳本管理器的跨域請求函數GM_xmlhttpRequest
預設不會帶Referer
,這又會導致pximg.net
回傳403 Forbidden
其中的gxf
是結合 gmxhr-fetch 和 xfetch-js
所產生出的物件,基本上就是一個跨域發 http 請求的 http client
後記
其實我發這篇文章是因為覺得太久發新文章了,之前不發新聞章是為了大學學測。但現在已經考完了,再不發真的說不過去...