从零开始教你写一个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-login
cd pixiv-login
npm 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的没有该文件):


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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容

  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,180评论 22 257
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,070评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 这本来是我为了过稿写的一篇文章,但是没有后续,我就把它放在了简书上。希望自己重拾写作的热情,慢慢来吧。 十年前,当...
    帅气的炮阅读 368评论 0 2
  • 一、料器造假南红鉴别 料器是以一种熔点较低的玻璃为原料制作,这种造假料器造假南红玛瑙虽然在外观上与天然南红玛瑙很像...
    小雨点41阅读 1,768评论 0 0