從零開始教你寫一個NPM包
前言
本文主要記錄我開發一個npm包:pixiv-login 時的心得體會,其中穿插了一些日常開發的流程和技巧,希望對新手有所啟發,大佬們看看就好_(:3」∠)
pixiv-login
pixiv-login的功能就是模擬用戶登錄網站pixiv,獲取cookie
源碼
npm
安裝:
npm install --save pixiv-login
使用:
const pixivLogin = require(pixiv-login);pixivLogin({ username: 你的用戶名, password: 你的密碼}).then((cookie) => { console.log(cookie);}).catch((error) => { console.log(error);});
開發工具
日常開發中,我常用的IDE是vscode+webstorm+sublime,其中vscode因為其啟動快,功能多,調試方便,受到大多數開發者的青睞。在接下來的教程中,我就以vscode進行演示了。至於終端,由於是在windows平台,所以我選擇了cmder代替原生cmd,畢竟cmder支持大多數linux命令。
初始化項目
mkdir pixiv-logincd pixiv-loginnpm init
一路回車就好
安裝依賴
要模擬登陸,我們就需要一個http庫,這裡我選擇了axios,同時獲取的html字元串我們需要解析,cheerio就是首選了
npm i axios cheerio --save
debug
畢業參加工作也有幾個月了,其中學到了很重要的一個技能就是debug。說出來也不怕大家笑,在大學時,debug就是console.log大法好,基本就不用斷點來追蹤,其實用好斷點,效率比console.log要高很多。還記得當初看到同事花式debug時,心中不禁感慨:為什麼你們會這麼熟練啊!
使用vscode進行node調試是非常方便的
首先新建一個index.js文件,項目結構如下(我本地的npm版本是5.x,所以會多一個package-lock.json文件,npm版本3.x的沒有該文件):
然後點擊左側第4個圖標,添加配置
配置文件如下:
{ // Use IntelliSense to learn about possible Node.js debug attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [{ "type": "node", "request": "launch", "name": "Launch Program", "program": "${workspaceRoot}\index.js" }]}
其中最重要的一句是 "program": "${workspaceRoot}\index.js",它表示debug時,項目的啟動文件是index.js
至此,debug的配置就完成了。
現在編寫index.js文件
const axios = require(axios);const cheerio = require(cheerio);axios.get(https://www.pixiv.net) .then(function(response) { const $ = cheerio.load(response.data); const title = $(title).text(); debugger; console.log(title); }) .catch(function(error) { console.log(error); });
按下F5啟動調試模式,如果一切正常,那麼效果如下:
可以看到,程序卡在了第8行
如果你把滑鼠移到response變數上,可以發現,vscode會自動顯示該變數的值,這比直接console.log(response)清晰簡潔多了
如果想繼續執行程序,可以接著按下F5或者右上角的綠色箭頭
程序執行完成,控制台打出了pixiv首頁的title值
除了使用debugger語句打斷點,你也可以直接點擊代碼的行數打斷點
比如上圖,我就在第8行處打了一個斷點,效果是一樣的
還有一個小技巧,在debug模式下,你可以隨意修改變數的值,比如現在程序卡在了第8行,這時你在控制台修改title的值
按下回車,然後繼續執行代碼,這時控制台輸出的title值就是deepred,而不是真正的title值
這個技巧,在平時開發過程中,當需要繞過某些驗證時,非常有用
正式開始
雖然我們最後是要寫一個npm包,但是首先,我們先把獲取cookie的功能實現了,然後再思考怎麼封裝為一個npm包,供其他人使用。
進入登錄頁面 登錄頁,我們先登錄一次,看看前端向後台發送了哪些數據
這裡需要特別注意,我們要勾選`preserve log`,這樣,即使頁面刷新跳轉了,http請求記錄仍然會記錄下來
可以看到,post_key是登錄的關鍵點,p站使用了該值來防止CSRF
post_key怎麼獲取呢?
經過頁面分析,發現在登錄頁面,有個隱藏表單域(後來發現,其實在首頁就已經寫出來了):
可以清楚看到,post_key已經寫出來了,我們只需要用cheerio解析出該input的值就ok了
const post_key = $(input[name="post_key"]).val();
獲取post_key
const axios = require(axios);const cheerio = require(cheerio);const LOGIN_URL = https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index;const USER_AGENT = Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36;const LOGIN_API = https://accounts.pixiv.net/api/login?lang=zh;const getKey = axios({ method: get, url: LOGIN_URL, headers: { User-Agent: USER_AGENT }}).then((response) => { const $ = cheerio.load(response.data); const post_key = $(input[name="post_key"]).val(); const cookie = response.headers[set-cookie].join(; ); if (post_key && cookie) { return { post_key, cookie }; } return Promise.reject("no post_key");}).catch((error) => { console.log(error);});getKey.then(({ post_key, cookie }) => { debugger;})
F5運行代碼
注意:打開註冊頁時,註冊頁會返回一些cookie,這些cookie在登錄時也是需要隨密碼,用戶名一起發送過去的
獲取到了post_key, cookie,我們就可以愉快的把登錄數據發送給後台介面了
const querystring = require(querystring);getKey.then(({ post_key, cookie }) => { axios({ method: post, url: LOGIN_API, headers: { User-Agent: USER_AGENT, Content-Type: application/x-www-form-urlencoded; charset=UTF-8, Origin: https://accounts.pixiv.net, Referer: https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index, X-Requested-With: XMLHttpRequest, Cookie: cookie }, data: querystring.stringify({ pixiv_id: 你的用戶名, password: 你的密碼, captcha: , g_recaptcha_response: , post_key: post_key, source: pc, ref: wwwtop_accounts_index, return_to: http://www.pixiv.net/ }) }).then((response) => { if (response.headers[set-cookie]) { const cookie = response.headers[set-cookie].join( ;); debugger; } else { return Promise.reject(new Error("no cookie")) } }).catch((error) => { console.log(error); });});
注意其中這段代碼:
data: querystring.stringify({ pixiv_id: 你的用戶名, password: 你的密碼, captcha: , g_recaptcha_response: , post_key: post_key, source: pc, ref: wwwtop_accounts_index, return_to: http://www.pixiv.net/ })
這裡有個巨大的坑,axios默認把數據轉成json格式,如果你想發送application/x-www-form-urlencoded的數據,就需要使用querystring模塊
詳情見: using-applicationx-www-form-urlencoded-format
如果一切正常,那麼效果如下:
其中的PHPSESSID和device_token就是伺服器端返回的登錄標識,說明我們登錄成功了
程序運行的同時,你也很可能收到P站的登錄郵件
好了,目前為止,我們已經成功獲取到了cookie,實現了最基本的功能。
特別注意
程序不要運行太多次,因為每次運行,你就登錄一次P站,如果被P站監測到頻繁登錄,它會開啟驗證碼模式,這時,你除了需要發送用戶名和密碼,還需要向後台發送驗證碼值
data: querystring.stringify({ pixiv_id: 你的用戶名, password: 你的密碼, captcha: 你還需要填驗證碼, g_recaptcha_response: , post_key: post_key, source: pc, ref: wwwtop_accounts_index, return_to: http://www.pixiv.net/ })
也就是,captcha欄位不再是空值了!
基本功能的完整代碼
const axios = require(axios);const cheerio = require(cheerio);const querystring = require(querystring);const LOGIN_URL = https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index;const USER_AGENT = Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36;const LOGIN_API = https://accounts.pixiv.net/api/login?lang=zh;const getKey = axios({ method: get, url: LOGIN_URL, headers: { User-Agent: USER_AGENT }}).then((response) => { const $ = cheerio.load(response.data); const post_key = $(input[name="post_key"]).val(); const cookie = response.headers[set-cookie].join(; ); if (post_key && cookie) { return { post_key, cookie }; } return Promise.reject("no post_key");}).catch((error) => { console.log(error);});getKey.then(({ post_key, cookie }) => { axios({ method: post, url: LOGIN_API, headers: { User-Agent: USER_AGENT, Content-Type: application/x-www-form-urlencoded; charset=UTF-8, Origin: https://accounts.pixiv.net, Referer: https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index, X-Requested-With: XMLHttpRequest, Cookie: cookie }, data: querystring.stringify({ pixiv_id: 你的用戶名, password: 你的密碼, captcha: , g_recaptcha_response: , post_key: post_key, source: pc, ref: wwwtop_accounts_index, return_to: http://www.pixiv.net/ }) }).then((response) => { if (response.headers[set-cookie]) { const cookie = response.headers[set-cookie].join( ;); console.log(cookie); } else { return Promise.reject(new Error("no cookie")); } }).catch((error) => { console.log(error); });});
封裝成一個npm包
登錄P站獲取cookie這個功能,如果我們想讓其他開發者也能方便調用,就可以考慮將其封裝為一個npm包發布出去,這也算是對開源社區做出自己的一份貢獻。
首先我們回想一下,我們調用其他npm包時是怎麼做的?
const cheerio = require(cheerio);const $ = cheerio.load(response.data);
同理,我們現在規定pixiv-login的用法:
const pixivLogin = require(pixiv-login);pixivLogin({ username: 你的用戶名, password: 你的密碼}).then((cookie) => {console.log(cookie);}).catch((error) => {console.log(error);})
pixiv-login對外暴露一個函數,該函數接受一個配置對象,裡面記錄了用戶名和密碼
現在,我們來改造index.js
const pixivLogin = ({ username, password }) => {};module.exports = pixivLogin;
最基本的骨架就是定義一個函數,然後把該函數導出
由於我們需要支持Promise寫法,所以導出的pixivLogin本身要返回一個Promise
const pixivLogin = ({ username, password }) => { return new Promise((resolve, reject) => { })};
之後,只要把原先的代碼套進去就好了
完整代碼:
const axios = require(axios);const cheerio = require(cheerio);const querystring = require(querystring);const LOGIN_URL = https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index;const USER_AGENT = Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36;const LOGIN_API = https://accounts.pixiv.net/api/login?lang=zh;const pixivLogin = ({ username, password }) => { return new Promise((resolve, reject) => { const getKey = axios({ method: get, url: LOGIN_URL, headers: { User-Agent: USER_AGENT } }).then((response) => { const $ = cheerio.load(response.data); const post_key = $(input[name="post_key"]).val(); const cookie = response.headers[set-cookie].join(; ); if (post_key && cookie) { return { post_key, cookie }; } reject(new Error(no post_key)); }).catch((error) => { reject(error); }); getKey.then(({ post_key, cookie }) => { axios({ method: post, url: LOGIN_API, headers: { User-Agent: USER_AGENT, Content-Type: application/x-www-form-urlencoded; charset=UTF-8, Origin: https://accounts.pixiv.net, Referer: https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index, X-Requested-With: XMLHttpRequest, Cookie: cookie }, data: querystring.stringify({ pixiv_id: username, password: password, captcha: , g_recaptcha_response: , post_key: post_key, source: pc, ref: wwwtop_accounts_index, return_to: http://www.pixiv.net/ }) }).then((response) => { if (response.headers[set-cookie]) { const cookie = response.headers[set-cookie].join( ;); resolve(cookie); } else { reject(new Error(no cookie)); } }).catch((error) => { reject(error); }); }); })}module.exports = pixivLogin;
發布npm包
README
每個npm包,一般都需要配一段介紹文字,來告訴使用者如何安裝使用,比如lodash的首頁
新建一個README.md,填寫相關信息
有時,我們會看到一些npm包有很漂亮的版本號圖標:
這些圖標,其實可以在https://shields.io/上製作
登錄該網站,下拉到最下面
輸入你想要的文字,版本號,顏色, 然後點擊按鈕
就可以得到圖片的訪問地址了
修改剛才的README.md,加上我們的版本號吧!
gitignore
我們現在的文件夾目錄應該如下所示:
其實node_modules以及.vscode是完全不用上傳的,所以為了防止發布時帶上這些文件夾,我們要新建一個.gitignore
.vscode/node_modules/
註冊
到 npmjs上註冊一個賬號
然後在終端輸入
npm adduser
輸入用戶名,密碼,郵箱即可登入成功
這裡還有一個坑!
如果你的npm使用的是淘寶鏡像,那麼是無法登陸成功的
最簡單的解決方法:
npm i nrm -gnrm use npm
nrm是個npm鏡像管理工具,可以很方便的切換鏡像源
登陸成功後,輸入
npm whoami
如果出現了你的用戶名,說明你已經成功登陸了
發布
特別注意:
因為pixiv-login這個名字已經被我佔用了,所以你需要改成其他名字
修改pacakge.json文件的name欄位
npm publish
即可發布成功啦!
下載
發布成功後,我們就可以下載自己的包了
npm i pixiv-login
使用pixiv-login包
我們可以用pixiv-login做一些有趣(♂)的事
比如:
下載 R-18每周排行榜的圖片
沒登錄的用戶是無法訪問R18區的,所以我們需要模擬登陸
const fs = require(fs);const axios = require(axios);const pixivLogin = require(pixiv-login);const cheerio = require(cheerio);const USER_AGENT = Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36;pixivLogin({ username: 你的用戶名, password: 你的密碼}).then((cookie) => { // 把cookie寫入文件中,則下次無需再次獲取cookie,直接讀取文件即可 fs.writeFileSync(cookie.txt, cookie);}).then((response) => { const cookie = fs.readFileSync(cookie.txt, utf8); axios({ method: get, url: https://www.pixiv.net/ranking.php?mode=weekly_r18, headers: { User-Agent: USER_AGENT, Referer: https://www.pixiv.net, Cookie: cookie }, }) .then(function(response) { const $ = cheerio.load(response.data); const src = $(#1 img).data(src); return src; }).then(function(response) { axios({ method: get, url: response, responseType: stream }) .then(function(response) { const url = response.config.url; const fileName = url.substring(url.lastIndexOf(/) + 1); response.data.pipe(fs.createWriteStream(fileName)).on(close, function() { console.log(`${fileName}下載完成`); });; }); })})
同時,我們的pixiv-login是支持async await的!
const pixivStart = async() => { try { const cookie = await pixivLogin({ username: 你的用戶名, password: 你的密碼 }); fs.writeFileSync(cookie.txt, cookie); const data = fs.readFileSync(cookie.txt, utf8); const response = await axios({ method: get, url: https://www.pixiv.net/ranking.php?mode=weekly_r18, headers: { User-Agent: USER_AGENT, Referer: https://www.pixiv.net, Cookie: cookie }, }); const $ = cheerio.load(response.data); const src = $(#1 img).data(src); const pic = await axios({ method: get, url: src, responseType: stream }); const fileName = pic.config.url.substring(pic.config.url.lastIndexOf(/) + 1); pic.data.pipe(fs.createWriteStream(fileName)).on(close, function() { console.log(`${fileName}下載完成`); });; } catch (err) { console.log(err) }};pixivStart();
參考
1. 模擬登錄pixiv.net
2. Python爬蟲入門:爬取pixiv
博客地址:
從零開始教你寫一個NPM包
推薦閱讀:
※怎麼才能成為一個nodejs大神?
※深JS(2015 JS中國開發者大會)有哪些女生參加?
※js中什麼技術能合併多個前端請求,並生成一個json文件發送?
※《深入淺出Node.js》《Node.js 實戰(雙色)》《了不起的Node.js》 這三本書那本書比較好呢?
※Node.js+Node-webkit的開發模式前景如何?