查看: 490|回复: 13

[油猴脚本]Pixiv系列小说自动爬虫

[复制链接]
发表于 2022-7-25 09:53 | 显示全部楼层 |阅读模式
禁止求评分、诱导评分、互刷评分、互刷悬赏值,违规者封号处理。
禁止发布推广、邀请码、邀请链接、二维码或者有利益相关的任何推广行为。
所有非原创软件请发布在【精品软件区】,发帖必须按照本版块版规格式发帖。

【简介】
用来练手的爬虫脚本,放弃了引入第三方脚本,选择使用油猴内置函数,因此依赖油猴。
不需要设置cookies,只需要用浏览器登陆Pixiv保证对目标小说有访问权限即可。
下载内容存储在油猴脚本中,可通过控制台命令将指定小说导出到本地。

由于GM_xmlhttpRequest没有跨域限制,实际上可以将
// @match https://www.pixiv.net/novel/series/*
替换为
// @include *
从而在任意页面使用本脚本

【爬虫命令】
startMain(seriesID,"Charpters",isForce)
启动爬虫
--{number}  seriesID:  必填,系列小说ID,获取位置为:https://www.pixiv.net/novel/series/系列小说ID (数字,不含?)
--{string}  Charpters: 可选,指定章节,默认为全部,具体指定规则见后文
--{boolean} isForce:   可选,对于已存在章节是否强制下载,默认为否


downloadList()
获取已下载内容列表
无参数,返回2个内容,分别为全部章节和全部小说,全部小说中文本可用于download()参数中的NovelName


download(NovelName,Charpters)
导出已下载内容
--{string} NovelName: 必填,系列小说名,可通过downloadList()获取
--{string} Charpters: 可选,指定章节,默认为全部,具体指定规则见后文


Charpters 规则:
规则1:单一数字/^\d+$/       添加单一数字章节到下载队列
规则2:数字范围/^\d+-\d+$/   添加数字范围章节到下载队列
规则3:^单一数字/^\^\d+$/    从下载队列中删除单一数字章节

多个规则用,分隔
例:"1,3-10,^5,15" 代表下载第1、3、4、6、7、8、9、10、15章
例:"^5,3-10,1,15" 代表下载第1、3、4、5、6、7、8、9、10、15章
[mw_shl_code=javascript,true]// ==UserScript==
// @name         Pixiv系列小说自动爬虫
// @version      1.0
// @Description  根据Pixiv系列小说seriesID自动爬取指定章节或整本小说并可导出成.txt
// @Author       DreamNya
// @match        https://www.pixiv.net/novel/series/*
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_download
// @connect      pixiv.net
// @license      MIT
// @namespace https://greasyfork.org/users/809466
// ==/UserScript==

//自定义章节分割名 用以小说阅读器自动拆分章节
function CustomCharpter(index) {
        return `第${index}章 `
}

function GMget(url) {
        return new Promise((resolve) => {
                GM_xmlhttpRequest({
                        method: "GET",
                        url: url,
                        onload: resolve
                })
        })
}

//获取所有章节信息
async function startCharpter(ID) {
        let CharpterRes = await GMget(`https://www.pixiv.net/ajax/novel/series/${ID}/content_titles`)
        if(CharpterRes.status != 200) {
                console.log(CharpterRes)
                throw new Error(JSON.parse(CharpterRes.responseText).message)
        }
        return JSON.parse(CharpterRes.responseText).body
}

//获取章节内容
async function getCharpter(Charpter, CharpterIndex) {
        let CharpterPage = await GMget("https://www.pixiv.net/novel/show.php?id=" + Charpter.id)
        let CharpterDoc = new DOMParser().parseFromString(CharpterPage.responseText, "text/html")
        let CharpterNovel = JSON.parse(CharpterDoc.querySelector("#meta-preload-data").content).novel
        let result = CharpterNovel[Object.getOwnPropertyNames(CharpterNovel)].content
        return CharpterIndex + Charpter.title + "\n" + result //自动补一行章节名用以分割章节
}

//主函数 启动爬虫
async function startMain(ID, Charpters = "ALL", force = false) {
        let mainRes = await GMget("https://www.pixiv.net/ajax/novel/series/" + ID)
        if(mainRes.status != 200) {
                console.log(mainRes)
                throw new Error(JSON.parse(mainRes.responseText).message)
        }
        let mainJSON = JSON.parse(mainRes.responseText).body
        let NovelName = mainJSON.title
        let NovelTotal = mainJSON.total
        let res = Charpters != "ALL" ? CharptersParse(Charpters) : "ALL"
        if(res != "ALL" && res[res.length - 1] > NovelTotal) throw new Error(`欲爬取章节${res[res.length-1]}大于可爬取最大章节数${NovelTotal}`)
        let CharpterInfos = await startCharpter(ID)
        if(res == "ALL") res = CharpterInfos.map((item, index) => index + 1)
        for(let item of res) {
                let index = item - 1
                let CharpterInfo = CharpterInfos[index]
                let CharpterIndex = CustomCharpter(item)
                if(CharpterInfo.available != true) {
                        console.log(`${CharpterIndex}无访问权限 无法爬取 已跳过 available!=true`, CharpterInfo)
                        continue
                }
                let CharpterKey = `${NovelName} # ${CharpterIndex}`
                if(force == false && GM_getValue(CharpterKey)) {
                        console.log(CharpterInfo.title + "已存在,已跳过爬取,如需强制爬取,请调用startMain(ID,Charpters,true)")
                        continue
                }
                let CharpterResult = await getCharpter(CharpterInfo, CharpterIndex)
                GM_setValue(CharpterKey, CharpterResult)
                console.log(CharpterInfo.title + "爬取成功")
        }
        console.log(ID, `${NovelName} ${Charpters} "已爬取完毕"`)
}
unsafeWindow.startMain = startMain

/*
Charpters 规则:
规则1:单一数字/^\d+$/       添加该单一数字章节到下载队列
规则2:数字范围/^\d+-\d+$/   添加该数字范围章节到下载队列
规则3:^单一数字/^\^\d+$/    从下载队列中删除该单一数字

多个规则用,分隔
例:"1,3-10,^5,15" 代表下载第1、3、4、6、7、8、9、10、15章
例:"^5,3-10,1,15" 代表下载第1、3、4、5、6、7、8、9、10、15章
*/
//导出已下载内容
function download(NovelName, Charpters) {
        let keys = Charpters ? CharptersParse(Charpters) : "ALL"
        let NovelValues = []
        if(keys == "ALL") {
                let pattern = new RegExp("^" + NovelName + " # ")
                let Allkeys = GM_listValues().filter(i => pattern.test(i))
                if(Allkeys.length == 0) {
                        throw new Error(NovelName + "未下载任何章节")
                }
                NovelValues = Allkeys.map(i => GM_getValue(i))
                Charpters = "ALL"
        } else {
                for(let item of keys) {
                        let _value = GM_getValue(`${NovelName} # ${CustomCharpter(_value)}`)
                        if(_value == void 0) {
                                throw new Error(`${NovelName} # ${CustomCharpter(_value)} 未下载,无法保存到本地`)
                        }
                        NovelValues.push(_value)
                }
        }
        GM_download(URL.createObjectURL(new Blob([NovelValues.join("\n\n")])), `${NovelName} ${Charpters}.txt`)
}
unsafeWindow.download = download

//获取已下载内容列表
function downloadList() {
        let list = GM_listValues()
        console.log("全部章节", list)
        console.log("全部小说", [...new Set(list.map(i => i.split(" # ")[0]))])
}
unsafeWindow.downloadList = downloadList

//指定章节格式化
function CharptersParse(Charpters) {
        let res = []
        for(let item of Charpters.split(",")) {
                switch (true) {
                        case /^\d+$/.test(item):
                                res.push(item * 1)
                                break
                        case /^\d+-\d+$/.test(item):
                                {
                                        let con = item.split("-")
                                        for(let i = con[0] * 1; i <= con[1] * 1; i++) res.push(i)
                                        break
                                }
                        case /^\^\d+$/.test(item):
                                {
                                        let non = item.replace("^", "")
                                        res = res.filter(i => i != non)
                                        break
                                }
                        default:
                                throw new Error(item + "格式有误")
                }
        }
        return [...new Set(res)].sort()
}[/mw_shl_code]

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
发表于 2022-7-25 10:08 | 显示全部楼层
先收藏了,万一用到呢
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-25 10:50 | 显示全部楼层
谢谢大佬分享
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-25 10:51 | 显示全部楼层
谢谢@Thanks!
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-25 10:51 | 显示全部楼层
谢谢大佬
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-25 11:40 | 显示全部楼层
6666
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-25 11:47 | 显示全部楼层

支持楼主,谢谢分享。
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-25 11:57 | 显示全部楼层

感谢楼主的分享
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-25 11:58 | 显示全部楼层
多谢楼主分享
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

发表于 2022-7-26 07:15 | 显示全部楼层
谢谢分享!
如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心值】和【牛币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!
回复 有用 没用

使用道具 举报

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

关注公众号

相关侵权、举报、投诉及建议等,请发 E-mail:admin@discuz.vip

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.

在本版发帖
关注公众号
返回顶部