從零開始教你寫一個NPM包

前言

本文主要記錄我開發一個npm包:pixiv-login 時的心得體會,其中穿插了一些平常開發的流程和技巧,但願對新手有所啓發,大佬們看看就好_(:3」∠)javascript

2018-11-8 更新php

  • pixiv已經被牆了,因此你可能登不上去
  • 圖片好像掛了,能夠跳轉到該地址看 點我

pixiv-login

pixiv-login的功能就是模擬用戶登陸網站pixiv,獲取cookie
源碼 npmhtml

安裝:前端

npm install --save pixiv-login

使用:java

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命令。node

初始化項目

mkdir pixiv-login
cd pixiv-login
npm init

一路回車就好linux

安裝依賴

要模擬登錄,咱們就須要一個http庫,這裏我選擇了axios,同時獲取的html字符串咱們須要解析,cheerio就是首選了ios

npm i axios cheerio --save

debug

畢業參加工做也有幾個月了,其中學到了很重要的一個技能就是debug。說出來也不怕你們笑,在大學時,debug就是console.log大法好,基本就不用斷點來追蹤,其實用好斷點,效率比console.log要高不少。還記得當初看到同事花式debug時,心中不由感慨:爲何大家會這麼熟練啊!git

使用vscode進行node調試是很是方便的github

首先新建一個index.js文件,項目結構以下(我本地的npm版本是5.x,因此會多一個package-lock.json文件,npm 3.x的沒有該文件):
pic
而後點擊左側第4個圖標,添加配置
pic

配置文件以下:

{
    // 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啓動調試模式,若是一切正常,那麼效果以下:
pic

能夠看到,程序卡在了第8行
若是你把鼠標移到response變量上,能夠發現,vscode會自動顯示該變量的值,這比直接console.log(response)清晰簡潔多了
pic

pic

若是想繼續執行程序,能夠接着按下F5或者右上角的綠色箭頭
pic

程序執行完成,控制檯打出了pixiv首頁的title值
pic

除了使用debugger語句打斷點,你也能夠直接點擊代碼的行數打斷點
pic

好比上圖,我就在第8行處打了一個斷點,效果是同樣的

還有一個小技巧,在debug模式下,你能夠隨意修改變量的值,好比如今程序卡在了第8行,這時你在控制檯修改title的值
pic

按下回車,而後繼續執行代碼,這時控制檯輸出的title值就是'deepred',而不是真正的title值
pic

這個技巧,在平時開發過程當中,當須要繞過某些驗證時,很是有用

正式開始

雖然咱們最後是要寫一個npm包,可是首先,咱們先把獲取cookie的功能實現了,而後再思考怎麼封裝爲一個npm包,供其餘人使用。

進入登陸頁面 https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index,咱們先登陸一次,看看前端向後臺發送了哪些數據
pic

這裏須要特別注意,咱們要勾選preserve log,這樣,即便頁面刷新跳轉了,http請求記錄仍然會記錄下來
pic
pic
pic
能夠看到,post_key是登陸的關鍵點,P站使用了該值來防止CSRF
post_key怎麼獲取呢?
通過頁面分析,發如今登陸頁面,有個隱藏表單域(後來發現,其實在首頁就已經寫出來了):

pic

能夠清楚看到,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運行代碼
pic

pic
注意:打開註冊頁時,註冊頁會返回一些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

若是一切正常,那麼效果以下:
pic

其中的PHPSESSID和device_token就是服務器端返回的登陸標識,說明咱們登陸成功了

程序運行的同時,你也極可能收到P站的登陸郵件
pic

好了,目前爲止,咱們已經成功獲取到了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的首頁
pic

新建一個README.md,填寫相關信息
pic

有時,咱們會看到一些npm包有很漂亮的版本號圖標:
pic

這些圖標,其實能夠在https://shields.io/ 上製做
登陸該網站,下拉到最下面
pic
輸入你想要的文字,版本號,顏色, 而後點擊按鈕
pic
就能夠獲得圖片的訪問地址了
pic
修改剛纔的README.md,加上咱們的版本號吧!
pic

gitignore

咱們如今的文件夾目錄應該以下所示:
pic

其實node_modules以及.vscode是徹底不用上傳的,因此爲了防止發佈時帶上這些文件夾,咱們要新建一個.gitignore

.vscode/
node_modules/

註冊

https://www.npmjs.com/ 上註冊一個帳號
而後在終端輸入

npm adduser

輸入用戶名,密碼,郵箱便可登入成功
這裏還有一個坑!
若是你的npm使用的是淘寶鏡像,那麼是沒法登錄成功的
最簡單的解決方法:

npm i nrm -g
nrm use npm

nrm是個npm鏡像管理工具,能夠很方便的切換鏡像源

登錄成功後,輸入

npm whoami

pic

若是出現了你的用戶名,說明你已經成功登錄了

發佈

特別注意
由於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
相關文章
相關標籤/搜索