1. 程式人生 > >談談如何繞過 TinyPNG 對上傳圖片數量的限制

談談如何繞過 TinyPNG 對上傳圖片數量的限制

前端er, 又稱為切圖仔,平時經常需要用 PSD 匯出 PNG 或 JPG,但是匯出來的的圖片一般比較大,往往需要用一些其他工具壓縮後再發布到生產環境。 以前常用的做法是,使用 [image-webpack-loader](https://www.npmjs.com/package/image-webpack-loader) ,在 `webpack` 打包專案時自動壓縮圖片。但是這 `loader` 是基於 [imagemin](https://github.com/imagemin/imagemin) 的,他的壓縮失真比較嚴重,而且壓縮率也不是很高。經常出現一種情況就是,在本地開發的時候,圖片明明很清晰的,但是一旦釋出到生產環境 ,因為經過了 `img-webapck-loader` 壓縮,導致圖片嚴重失真。也因為這個原因,美術小姐姐在驗收專案的時候,常常會把我叫過去,說:哎呀,我的 PSD 明明是很清晰的呀,為什麼你放到網站上去就那麼模糊了呢。我說因為壓縮過了呀。然後她說這個壓縮工具不給力呀,就推薦了一個線上壓縮工具給我: [TinyPNG線上地址](https://tinypng.com/) 初體驗 `tinypng` 真是把我驚呆了, 一般的 PNG 壓縮率竟然可以高達 `50% - 70% `,並且肉眼看不出來任何的失真。 TinyPNG 的原理是將 PNG24 位真彩色圖片壓縮成 PNG8 位索引圖片,從而做到基本不損失畫質和觀感。至於具體演算法怎麼實現的也沒有深入研究守,有興趣的可以自行查閱相關資料。 tinypg 除了可以通過網頁端上傳圖片,也提供了 api 的方式 ,所以可以使用 node 或其他指令碼語言進行上傳的,可以參考這裡: [https://tinify.cn/developers/reference](https://tinify.cn/developers/reference) 然而,這麼好用的壓縮工具有一個很明顯缺陷的,就是每天只能正常壓縮 20 張圖片。超過 20 張之後,就會經常出來 `Too many files uploaded at once`。如下紅色的部分: ![](https://user-gold-cdn.xitu.io/2020/6/30/173052b67ba2483a?w=778&h=625&f=png&s=571190) 當然,這是針對免費版使用者而言的。如果想要避免這個限制,你需要花 25 美金買一年的會員。當然,我們程式設計師大多數都是很吝嗇的,就連到外面吃飯要不要加個滷蛋都要糾結半個小時的,更別說給 25 美金他了。 但是,程式設計師的特點是,都愛折騰!所以我決定要嘗試著繞過這 20 張圖片的上傳限制。一般來說,這種免登入就可試用的系統,都是通過使用者IP來限制使用者的操作次數的,所以就決定從修改 IP 的方式來繞開這個限制。 最簡單的作法是使用動態的代理 IP, 比如這裡 https://ip.ihuan.me/ 就有一些免費代理IP,但是,這些 IP 都是國外的,很慢的,我們的目的是要上傳並壓縮圖片,使用這麼慢的代理顯然是不可行的。 所以就只能尋找其他的辦法。 就在陷入迷茫的時候,突然想到了一個事實 :目前的 web 架構大多數都是通過 nginx 作為反向代理的,如下: ![](https://user-gold-cdn.xitu.io/2020/6/30/173052adbf44b65d?w=720&h=551&f=jpeg&s=20137) 從上圖可以看出,客戶端並不是直接請求應用服務的,而是通過統一接入層(往往是 nginx) 來轉發請求的。那麼問題來,應用服務是如何獲取到客戶端的 IP 的呢? 玩過 `nginx` 的同學應該都知道 , 這個通用的解決方案就是 `X-Forwarded-For` 請求頭。 簡單的來說,就是所有的反向代理都實現一個統一的約定,在轉發請求給下游服務之前,把請求代理的 IP 地址寫入到 X-Forwarded-For 頭中,形成了一個 IP 地址列: ``` X-Forwarded-For: client, proxy1, proxy2 ``` 這個方案雖然不是正式的 HTTP 協議,但已經成為了一個事實標準,基本上所有的反向代理服務都實現了這個功能,以確保下游的服務可以感知到經過的反向代理,並從中獲取到使用者的 IP 地址。 好了,既然應用服務是通過 `X-Forwarded-For` 頭部來獲取客戶端 IP 的,那麼我們能不能在客戶端偽造這個頭部,每次上傳圖片的時候都設定一個隨機的 IP 呢?所以就決定嘗試一下。結果發現,該方法確實可行!竟然可以欺騙 tinypng 的伺服器,從而繞開了上傳次數的限制。 完整程式碼如下: ``` const fs = require('fs'); const path = require('path'); const https = require('https'); const { URL } = require('url'); const cwd = process.cwd(); const root = cwd; exts = ['.jpg', '.png'], max = 5200000; // 5MB == 5242848.754299136 const options = { method: 'POST', hostname: 'tinypng.com', path: '/web/shrink', headers: { rejectUnauthorized: false, 'Postman-Token': Date.now(), 'Cache-Control': 'no-cache', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36' } }; fileList(root); // 生成隨機IP, 賦值給 X-Forwarded-For function getRandomIP() { return Array.from(Array(4)).map(() => parseInt(Math.random() * 255)).join('.') } // 獲取檔案列表 function fileList(folder) { fs.readdir(folder, (err, files) => { if (err) console.error(err); files.forEach(file => { fileFilter(path.join(folder, file)); }); }); } // 過濾檔案格式,返回所有jpg,png圖片 function fileFilter(file) { fs.stat(file, (err, stats) => { if (err) return console.error(err); if ( // 必須是檔案,小於5MB,字尾 jpg||png stats.size <= max && stats.isFile() && exts.includes(path.extname(file)) ) { // 通過 X-Forwarded-For 頭部偽造客戶端IP options.headers['X-Forwarded-For'] = getRandomIP(); fileUpload(file); // console.log('可以壓縮:' + file); } // if (stats.isDirectory()) fileList(file + '/'); }); } // 非同步API,壓縮圖片 // {"error":"Bad request","message":"Request is invalid"} // {"input": { "size": 887, "type": "image/png" },"output": { "size": 785, "type": "image/png", "width": 81, "height": 81, "ratio": 0.885, "url": "https://tinypng.com/web/output/7aztz90nq5p9545zch8gjzqg5ubdatd6" }} function fileUpload(img) { var req = https.request(options, function(res) { res.on('data', buf =>
{ let obj = JSON.parse(buf.toString()); if (obj.error) { console.log(`[${img}]:壓縮失敗!報錯:${obj.message}`); } else { fileUpdate(img, obj); } }); }); req.write(fs.readFileSync(img), 'binary'); req.on('error', e => { console.error(e); }); req.end(); } // 該方法被迴圈呼叫,請求圖片資料 function fileUpdate(imgpath, obj) { const outputDir = path.join(cwd , 'output'); imgpath = path.join(cwd , 'output', imgpath.replace(cwd, '')); if(!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir); } let options = new URL(obj.output.url); let req = https.request(options, res =>
{ let body = ''; res.setEncoding('binary'); res.on('data', function(data) { body += data; }); res.on('end', function() { fs.writeFile(imgpath, body, 'binary', err => { if (err) return console.error(err); console.log( `[${imgpath}] \n 壓縮成功,原始大小-${obj.input.size},壓縮大小-${ obj.output.size },優化比例-${obj.output.ratio}` ); }); }); }); req.on('error', e =>
{ console.error(e); }); req.end(); } ``` 這段程式碼是參考了 [nodejs 全自動使用 Tinypng (免費版,無需任何配置)壓縮圖片](https://segmentfault.com/a/1190000015467084) ,只是簡單的加上了動態的 IP 頭,從而實現了不受上傳次數限制。 核心的程式碼在這裡: ``` // 生成隨機IP, 賦值給 X-Forwarded-For function getRandomIP() { return Array.from(Array(4)).map(() => parseInt(Math.random() * 255)).join('.') } // .... options.headers['X-Forwarded-For'] = getRandomIP(); //.... ``` 程式碼上傳到了 github ,有興趣的可以點選下面連線檢視並 star 關注 喔: [super-tinypng](https://github.com/zhanyuzhang/super-tinypng) 同時,也釋出到了 npm 上面。只需要全域性安裝: ``` npm i super-tinypng -g ``` 然後在你想要壓縮圖片的目錄裡面執行 `super-tinypng` 就能自動壓縮圖片了,並且不會有數量