├── dist
├── html
│ └── readme.txt
└── md
│ └── readme.txt
├── bin
└── juejinxiaoce
├── .editorconfig
├── .eslintrc.js
├── constant.js
├── package.json
├── LICENSE
├── .gitignore
├── utils.js
├── CODE_OF_CONDUCT.md
├── readme.md
└── app.js
/dist/html/readme.txt:
--------------------------------------------------------------------------------
1 | 爬取的 html 文件目录
2 |
--------------------------------------------------------------------------------
/dist/md/readme.txt:
--------------------------------------------------------------------------------
1 | markdown 文件的输出目录
2 |
--------------------------------------------------------------------------------
/bin/juejinxiaoce:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | require('../app.js')
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = false
8 | insert_final_newline = true
9 | end_of_line = crlf
10 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "es6": true,
4 | "node": true
5 | },
6 | "extends": "eslint:recommended",
7 | "parserOptions": {
8 | "ecmaVersion": 2018
9 | },
10 | "rules": {
11 | "indent": [
12 | "error",
13 | 2
14 | ],
15 | "linebreak-style": [
16 | "error",
17 | "windows"
18 | ],
19 | "quotes": [
20 | "error",
21 | "single"
22 | ],
23 | "semi": [
24 | "error",
25 | "never"
26 | ]
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/constant.js:
--------------------------------------------------------------------------------
1 | const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
2 | const URL_HOSTNAME = 'juejin.im'
3 | const URL_LOGIN_EMAIL = '/auth/type/email'
4 | const URL_LOGIN_PHONENUMBER = '/auth/type/phoneNumber'
5 |
6 | const URL_BOOK_HOSTNAME = 'xiaoce-cache-api-ms.juejin.im'
7 | const URL_BOOK_LIST_SECTION = '/v1/getListSection'
8 | const URL_BOOK_SECTION = '/v1/getSection'
9 |
10 | module.exports = {
11 | USER_AGENT,
12 | URL_HOSTNAME,
13 | URL_LOGIN_EMAIL,
14 | URL_LOGIN_PHONENUMBER,
15 | URL_BOOK_HOSTNAME,
16 | URL_BOOK_LIST_SECTION,
17 | URL_BOOK_SECTION
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@oliyg/juejinxiaoce",
3 | "version": "2.2.4",
4 | "description": "",
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "node app.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/oliyg/juejinxiaoce.git"
12 | },
13 | "keywords": [],
14 | "bin": {
15 | "juejinxiaoce": "bin/juejinxiaoce"
16 | },
17 | "author": "OliverYoung ",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/oliyg/juejinxiaoce/issues"
21 | },
22 | "homepage": "https://github.com/oliyg/juejinxiaoce#readme",
23 | "devDependencies": {
24 | "eslint": "^5.12.1"
25 | },
26 | "dependencies": {
27 | "turndown": "^5.0.3"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2019 OliverYoung
3 |
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
21 | OR OTHER DEALINGS IN THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 | .env.test
60 |
61 | # parcel-bundler cache (https://parceljs.org/)
62 | .cache
63 |
64 | # next.js build output
65 | .next
66 |
67 | # nuxt.js build output
68 | .nuxt
69 |
70 | # vuepress build output
71 | .vuepress/dist
72 |
73 | # Serverless directories
74 | .serverless/
75 |
76 | # FuseBox cache
77 | .fusebox/
78 |
79 | # DynamoDB Local files
80 | .dynamodb/
81 |
82 |
83 |
84 |
85 | # dist
86 | dist/html/*.html
87 | dist/md/*.md
88 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | const https = require('https')
2 | const fs = require('fs')
3 | const path = require('path')
4 | const process = require('process')
5 |
6 | const mkdir = dirname => {
7 | const outputdir = path.resolve(process.env.PWD, dirname)
8 | fs.existsSync(outputdir) || fs.mkdirSync(outputdir)
9 | }
10 |
11 | const rmfile = ext => {
12 | const outputdir = path.resolve(__dirname, 'dist', ext)
13 | const fileList = fs.readdirSync(outputdir).filter(item => path.extname(item) === '.' + ext)
14 | fileList.forEach(file => {
15 | fs.unlinkSync(path.join(outputdir, file))
16 | })
17 | }
18 |
19 | const sleep = async(wait = 1000) => {
20 | return new Promise(res => {
21 | setTimeout(() => {
22 | res(1)
23 | }, wait)
24 | })
25 | }
26 |
27 | function sendGet(hostname, path, headers) {
28 | return new Promise((resolve, reject) => {
29 | const req = https.request({
30 | hostname, port: 443, path, method: 'GET'
31 | }, res => {
32 | let data = ''
33 | res.on('data', chunk => { data = data + chunk.toString() })
34 | res.on('error', reject)
35 | res.on('end', () => {
36 | resolve({
37 | headers: res.headers,
38 | data
39 | })
40 | })
41 | })
42 | setHeaders(req, headers)
43 | req.on('error', reject)
44 | req.end()
45 | })
46 | }
47 |
48 | function sendPost(hostname, path, data, headers) {
49 | return new Promise((resolve, reject) => {
50 | const req = https.request({
51 | hostname, path, port: 443, method: 'POST'
52 | }, res => {
53 | let data = ''
54 | res.on('data', chunk => { data = data + chunk.toString() })
55 | res.on('end', () => { resolve({
56 | res,
57 | data
58 | }) })
59 | res.on('error', reject)
60 | })
61 |
62 | setHeaders(req, headers)
63 | req.on('error', reject)
64 | req.write(data)
65 | req.end()
66 | })
67 | }
68 |
69 | function setHeaders(req, obj) {
70 | for (const key in obj) {
71 | if (obj.hasOwnProperty(key)) {
72 | req.setHeader(key, obj[key])
73 | }
74 | }
75 | }
76 |
77 | function getCookieObj(cookie_arr) {
78 | cookie_arr = cookie_arr.map(item => item.split('; path=')[0])
79 | cookie_arr = cookie_arr.map(item => item.split('='))
80 | let result = {}
81 | cookie_arr.forEach(item => {
82 | result[item[0]] = item[1]
83 | })
84 | return result
85 | }
86 |
87 | function getCookieArr(cookie_obj) {
88 | const result = []
89 | for (const key in cookie_obj) {
90 | if (cookie_obj.hasOwnProperty(key)) {
91 | const value = cookie_obj[key]
92 | result.push(key + '=' + value)
93 | }
94 | }
95 | return result
96 | }
97 |
98 | module.exports = {
99 | setHeaders, getCookieObj, getCookieArr, sendPost, sendGet, sleep, rmfile, mkdir
100 | }
101 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at billyangg@qq.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
78 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Foliyg%2Fjuejinxiaoce?ref=badge_shield)
2 |
3 | # 🔥 掘金小册 markdown 转换器
4 |
5 | 
6 |
7 | [github 仓库欢迎 star](https://github.com/oliyg/juejinxiaoce)
8 |
9 | 采用 node https 模块,获取已购买小册 html 代码,并将 html 代码转换为 markdown 格式文件保存本地。
10 |
11 | **注意:目前本项目有两个版本,v2 不需要使用 chromium 作为无头浏览器;v1 则使用 chromi 作为无头浏览器模拟用户登录网站;**
12 |
13 | 根据需要选择不同版本
14 |
15 | - v2:
16 | - [latest](https://github.com/oliyg/juejinxiaoce/releases)
17 | - v1 不再维护:
18 | - [release v1](https://github.com/oliyg/juejinxiaoce/releases/tag/1.1.2)
19 |
20 | ## 使用方法
21 |
22 | **⚠️ 注意:掘金不支持境外网络访问,因此不要使用代理**
23 |
24 | ### 方法一:npx 直接执行
25 |
26 | 在本地某目录中执行 `npx @oliyg/juejinxiaoce` 按照提示输入用户名密码以及小册 ID 当提示 all done 完成
27 |
28 | ```
29 | ➜ Desktop npx @oliyg/juejinxiaoce
30 | npx: 98 安装成功,用时 10.748 秒
31 | email: 输入你的用户名密码
32 | password: 输入你的用户名密码
33 | bookId: 小册 ID
34 | ===navagating to main page
35 | ===login...
36 | ===getting book section list
37 | ===getting book HTML content
38 | 面试常用技巧
39 | ===writing html...
40 | ===getting book HTML content
41 | ===write html file success
42 | ===writing markdown...
43 | ===write markdown file success
44 | 前方的路,让我们结伴同行
45 | ===writing html...
46 | ===write html file success
47 | ===writing markdown...
48 | ===write markdown file success
49 |
50 | ======
51 | All Done...Enjoy.
52 | ======
53 | ```
54 |
55 | 在执行命令的这个目录中可以找到一个名为 md xxx 的文件夹,内包含 md 文档;在上面这个例子中,我们在 Desktop 桌面目录执行命令,因此在桌面目录中会生成这个文件夹:
56 |
57 | ```shell
58 | ➜ md 1548483715543 ls -al
59 | total 40
60 | drwxr-xr-x 4 oli staff 128 1 26 14:22 .
61 | drwx------+ 9 oli staff 288 1 26 14:21 ..
62 | -rw-r--r-- 1 oli staff 4915 1 26 14:21 面试常用技巧.md
63 | -rw-r--r-- 1 oli staff 8465 1 26 14:22 前方的路,让我们结伴同行.md
64 | ```
65 |
66 | ### 方法二:npm i 命令
67 |
68 | 使用 `npm i -g` 安装,并使用 `juejinxiaoce` 命令执行:
69 |
70 | ```
71 | ➜ Desktop npm i -g @oliyg/juejinxiaoce
72 | /Users/oli/.nvm/versions/node/v8.12.0/bin/juejinxiaoce -> /Users/oli/.nvm/versions/node/v8.12.0/lib/node_modules/@oliyg/juejinxiaoce/bin/juejinxiaoce
73 | + @oliyg/juejinxiaoce@2.2.1
74 | added 98 packages from 201 contributors in 5.89s
75 | ➜ Desktop juejinxiaoce
76 | email:
77 | password:
78 | bookId:
79 | ===navagating to main page
80 | ===login...
81 | ...
82 | ...
83 | ```
84 |
85 | > 小册ID见 URL 链接:
86 | >
87 | > 
88 |
89 | 执行后等待出现消息 `all done. enjoy.` 完成转换,效果如下:
90 |
91 | 
92 |
93 | 
94 |
95 | ## 更新日志
96 |
97 | - v2.2.4 修改文件名
98 | - v2.2.0 增加命令行模式
99 | - v2.0.0 使用 node 原生 https 模块,发送请求数据获取内容,不需要安装 chromium,没有软件权限问题
100 | - v1.1.2 使用谷歌 puppeteer 作为无头浏览器获取内容,需要安装 chromium,macOS 中可能有权限问题
101 |
102 | ## 常见问题
103 |
104 | - v1.1.2
105 | - 报错:spawn EACCES
106 | - 常见于 macOS,请保证 chromium 已被正常安装
107 |
108 | ## 开源贡献
109 |
110 | - 感谢 `@yangchendoit` 提交的 pr 完善手机号码登录功能
111 |
112 | ## 免责
113 |
114 | - 不提供用户名和密码,需使用用户自己的账号密码登录
115 | - 仅作为技术讨论,学习和研究使用
116 |
117 | ## 隐私
118 |
119 | - 该项目不会存储和发送任何用户隐私数据
120 |
121 | ## License
122 |
123 | The MIT License (MIT)
124 | Copyright (c) 2019 OliverYoung
125 |
126 |
127 | Permission is hereby granted, free of charge, to any person obtaining a copy
128 | of this software and associated documentation files (the "Software"), to deal
129 | in the Software without restriction, including without limitation the rights
130 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
131 | copies of the Software, and to permit persons to whom the Software is
132 | furnished to do so, subject to the following conditions:
133 |
134 | The above copyright notice and this permission notice shall be included in all
135 | copies or substantial portions of the Software.
136 |
137 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
138 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
139 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
140 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
141 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
142 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
143 | OR OTHER DEALINGS IN THE SOFTWARE.
144 |
145 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Foliyg%2Fjuejinxiaoce?ref=badge_large)
146 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | const fs = require('fs')
3 | const path = require('path')
4 | const os = require('os')
5 | const readline = require('readline')
6 | const Turndown = require('turndown')
7 |
8 | const { getCookieArr, getCookieObj, sendPost, sendGet, sleep, rmfile, mkdir } = require('./utils')
9 | const { USER_AGENT, URL_HOSTNAME, URL_LOGIN_EMAIL, URL_LOGIN_PHONENUMBER, URL_BOOK_HOSTNAME, URL_BOOK_LIST_SECTION, URL_BOOK_SECTION } = require('./constant')
10 |
11 | class Juejin {
12 | constructor() {
13 | this.loginType = '0' // 0为邮箱 1为手机
14 | this.account = ''
15 | this.password = ''
16 | this.bookID = ''
17 | this.cookie = ''
18 | this.src = 'web'
19 | this.userInfo = {}
20 | this.bookSectionList = []
21 | this.count = 0
22 | this.pwd = process.env.PWD
23 | }
24 |
25 | copyToPWDDir(dirname) {
26 | mkdir(dirname)
27 |
28 | const output = path.resolve(__dirname, 'dist', 'md')
29 |
30 | const fileList = fs.readdirSync(output).filter(item => path.extname(item) === '.md')
31 | fileList.forEach(file => {
32 | fs.copyFileSync(path.join(output, file), path.resolve(process.env.PWD, dirname, file))
33 | })
34 | }
35 |
36 | async getMetaData() {
37 | const rl = readline.createInterface({
38 | input: process.stdin,
39 | output: process.stdout
40 | })
41 |
42 | const question = query => {
43 | return new Promise(resolve => {
44 | rl.question(query, resolve)
45 | })
46 | }
47 | console.warn('loginType 0:邮箱 1:手机号码')
48 | const loginType = await question('loginType: ')
49 | const account = await question('account: ')
50 | const password = await question('password: ')
51 | const bookID = await question('bookId: ')
52 |
53 | rl.close()
54 | this.loginType = loginType
55 | this.account = account
56 | this.password = password
57 | this.bookID = bookID
58 |
59 | Promise.resolve()
60 | }
61 |
62 | async mainPage() {
63 | console.warn('===navagating to main page')
64 | const headers = {
65 | 'User-Agent': USER_AGENT,
66 | 'Connection': 'keep-alive'
67 | }
68 | const response = await sendGet(URL_HOSTNAME, '/', headers)
69 | this.cookie = JSON.stringify(getCookieObj(response.headers['set-cookie']))
70 | }
71 |
72 | async login() {
73 | console.warn('===login...')
74 | const authObj = {
75 | password: this.password
76 | }
77 | let loginUrl;
78 |
79 | if(this.loginType === '0'){
80 | Object.assign(authObj,{
81 | email: this.account
82 | })
83 | loginUrl = URL_LOGIN_EMAIL;
84 | }else{
85 | Object.assign(authObj,{
86 | phoneNumber: this.account
87 | })
88 | loginUrl = URL_LOGIN_PHONENUMBER;
89 | }
90 | const auth = JSON.stringify(authObj)
91 | const headers = {
92 | 'User-Agent': USER_AGENT,
93 | 'Content-Type': 'application/json',
94 | 'Content-Length': Buffer.byteLength(auth),
95 | 'Cookie': getCookieArr(JSON.parse(this.cookie))
96 | }
97 | const response = await sendPost(URL_HOSTNAME, loginUrl, auth, headers)
98 | this.cookie = JSON.stringify(Object.assign(JSON.parse(this.cookie), getCookieObj(response.res.headers['set-cookie'])))
99 | this.userInfo = JSON.parse(response.data)
100 | return response
101 | }
102 |
103 | async getTargetBookSectionList() {
104 | console.warn('===getting book section list')
105 | const headers = {
106 | 'User-Agent': USER_AGENT,
107 | 'Connection': 'keep-alive'
108 | }
109 | const response = await sendGet(URL_BOOK_HOSTNAME, `${URL_BOOK_LIST_SECTION}?uid=${this.userInfo.userId}&client_id=${this.userInfo.user.clientId}&token=${this.userInfo.user.token}&src=${this.src}&id=${this.bookID}`, headers)
110 | const data = response.data
111 | this.bookSectionList = JSON.parse(data).d
112 | return response
113 | }
114 |
115 | async getContentHTML(callback) {
116 | console.warn('===getting book HTML content')
117 | const headers = {
118 | 'User-Agent': USER_AGENT,
119 | 'Connection': 'keep-alive'
120 | }
121 | await sleep(3000)
122 |
123 | const url = `${URL_BOOK_SECTION}?uid=${this.userInfo.userId}&client_id=${this.userInfo.clientId}&token=${this.userInfo.token}&src=${this.src}§ionId=${this.bookSectionList[this.count].sectionId}`
124 | const response = await sendGet(URL_BOOK_HOSTNAME, url, headers)
125 | let data = JSON.parse(response.data)
126 |
127 | console.log(`${this.count + 1}.${data.d.title}`)
128 | data.d.isFinished || console.log('写作中...')
129 | callback(data.d)
130 |
131 | this.count ++
132 | let maxCount = this.bookSectionList.length
133 | this.count < maxCount && await this.getContentHTML(callback)
134 | }
135 |
136 | saveHTML(d) {
137 | return new Promise((resolve, reject) => {
138 | console.log('===writing html...')
139 | const title = d.title.replace(/[/?*:|\\<>]/g, ' ')
140 | const output = path.resolve(__dirname, 'dist', 'html', title + '.html')
141 | fs.writeFile(output, d.html, { encoding: 'utf-8' }, err => {
142 | err && reject(err)
143 | console.log('===write html file success')
144 | resolve({title, output})
145 | })
146 | })
147 | }
148 |
149 | toMarkdown(title, path) {
150 | return new Promise((resolve, reject) => {
151 | const turndownService = new Turndown({
152 | headingStyle: 'atx',
153 | codeBlockStyle: 'fenced'
154 | })
155 | try {
156 | const markdown = turndownService.turndown(fs.readFileSync(path, { encoding: 'utf-8' }))
157 | resolve({ title, markdown })
158 | } catch (error) {
159 | reject(error)
160 | }
161 | })
162 | }
163 |
164 | saveMD(title, data) {
165 | return new Promise((resolve, reject) => {
166 | console.log('===writing markdown...')
167 | const output = path.resolve(__dirname, 'dist', 'md', `${this.count}.${title}.md`)
168 | fs.writeFile(output, data, { encoding: 'utf-8' }, err => {
169 | err && reject(err)
170 | console.log('===write markdown file success')
171 | resolve()
172 | })
173 | })
174 | }
175 |
176 | }
177 |
178 | {(async () => {
179 | rmfile('html')
180 | rmfile('md')
181 | const juejin = new Juejin()
182 | try {
183 | await juejin.getMetaData()
184 | await juejin.mainPage()
185 | await sleep()
186 | await juejin.login()
187 | await sleep()
188 | await juejin.getTargetBookSectionList()
189 |
190 | const dirname = 'md ' + + new Date()
191 |
192 | await juejin.getContentHTML(async d => {
193 | const { title, output } = await juejin.saveHTML(d)
194 | const { title: mdTitle, markdown: markdownData } = await juejin.toMarkdown(title, output)
195 | await juejin.saveMD(mdTitle, markdownData)
196 | juejin.copyToPWDDir(dirname)
197 | })
198 |
199 | setTimeout(() => {
200 | console.log(`${os.EOL}======${os.EOL}All Done...Enjoy.${os.EOL}======${os.EOL}`)
201 | }, 200)
202 |
203 | } catch (error) {
204 | console.log(error)
205 | }
206 | })()}
207 |
--------------------------------------------------------------------------------