本文主要记录我开发一个npm包:pixiv-login
时的心得体会,其中穿插了一些平常开发的流程和技巧,但愿对新手有所启发,大佬们看看就好_(:3」∠)javascript
2018-11-8 更新 :php
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就是console.log
大法好,基本就不用断点来追踪,其实用好断点,效率比console.log
要高不少。还记得当初看到同事花式debug时,心中不由感慨:为何大家会这么熟练啊!git
使用vscode进行node调试是很是方便的github
首先新建一个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包,供其余人使用。
进入登陆页面 https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index,咱们先登陆一次,看看前端向后台发送了哪些数据
这里须要特别注意,咱们要勾选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); }); });
登陆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包,通常都须要配一段介绍文字,来告诉使用者如何安装使用,好比lodash的首页
新建一个README.md,填写相关信息
有时,咱们会看到一些npm包有很漂亮的版本号图标:
这些图标,其实能够在https://shields.io/ 上制做
登陆该网站,下拉到最下面
输入你想要的文字,版本号,颜色, 而后点击按钮
就能够获得图片的访问地址了
修改刚才的README.md,加上咱们的版本号吧!
咱们如今的文件夹目录应该以下所示:
其实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
若是出现了你的用户名,说明你已经成功登录了
特别注意:
由于pixiv-login
这个名字已经被我占用了,因此你须要改为其余名字
修改pacakge.json文件的name
字段
npm publish
便可发布成功啦!
发布成功后,咱们就能够下载本身的包了
npm i 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();