发布于 

Golang爬取picaComic

web #golang

第二次的Golang代码!纪念一下~~
也是第二次的picaComic爬虫,是之前python程序的再开,但也不算再开,因为做了很多很多的优化,比之前那个性能和稳定性都好得多

又是一次被迫99%在自己写的代码()CV工程师已经名不副实了!!

程序思路复盘

粗体为比起上次的python程序添加的内容

在写的过程中,进一步巩固了部分chromedp的函数,学习了chanel和进度条,有机会进一步使用了goroutine(可惜线程池使用失败了,下次再试试),对爬虫的技巧也有了更好一点的理解

  1. login
    1. 自动填写json文件里的用户名的和密码,如果没有就用户手填
    2. 检测到appcasule框架表示进入了主页,进入下一环节
  2. 选择画质
    1. 本来想做自动选择的但是失败了,弹窗也不能点,原因不明(我怀疑是因为元素在后台的时候是可查询但不可点击的,但在chromedp里似乎没有waitClickable的选项)
    2. 所以放20秒给用户自己点(进入一个漫画页面的左上角就可以改设置)
    3. 没办法做是否选完的检测,除非要求用户发一个信号,太麻烦了不如定时
  3. 检查idlist的每一本书,对于每一本书:
    1. 进入书本主页,获取书本名字,标准化后在savePath(json中设置)中建立一个对应名字的文件夹
    2. 获取章节数章节名字,建立子文件夹
    3. 对于每一个章节:
      1. 进入章节页面,下拉到页面底部,重复2-3次检查顶端progressBar的值是否改变(事实上,页面和图片都使用了懒加载,但是图片的懒加载没关系,在标签里可以查询到真实链接),直到到达真正的页面尾部
      2. 查询所有图片元素,获取真实链接,将书的序号、章的序号、图的序号、图的链接绑成一个结构体,把指针送进下载通道picChan里
  4. 下载程序:
    1. 初始化时就开始运行,监听下载通道中是否有内容,一旦出现内容就创建一个图片下载进程,命名按结构体中的序号值来确定
  5. 进度条:vscode自带终端不能在同一行内刷新,cmd运行时正常。
    本来想试试做多个进度条同时加载(每一章节的图片下载情况),但是考虑到go-progressbar的原理(上下行切换和读取),所以只能用一个条,显示的是浏览器爬取图片真实src的进度而不是图片下载的进度

感想

  • 运行效果很棒!多线程下载也非常给力>w<(虽然没有用线程池,是go自带的线程管理,据说会block)
  • 比python快,而且封包只有13M,震惊
  • 不能处理弹窗,有点遗憾,不过不处理弹窗并不影响图片链接的获取,这个还不错(x)
  • 做了很多上次程序没有做的事情,很开心
  • 没办法把pdf合成也用go做比较遗憾

别的

  • 感觉这次程序的结构可以说是依托答辩,应该多分一点文件的,太长了太乱了!!!下次试试

源代码

main.go
package main

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"

"github.com/schollz/progressbar"

"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
"github.com/chromedp/chromedp/kb"
)

// 全局变量
const (
homeUrl string = "https://manhuabika.com/pLogin/"
pageUrl string = "https://manhuabika.com/pcomicview/?cid="
pageUrl2 string = "https://manhuabika.com/pchapter/?cid="
)

var (
account string
password string
idList []string
savePath string
PROXY string //以上从json读取
ctx context.Context
wg sync.WaitGroup
NmList [100][100]string //对于漫画i,[i][0]为书的path,[i][1]-[i][n]为章节的path
picChan chan *pic
)

type pic struct { //图片中包含书编号,章编号,图编号
bkNum int
chNum int
picNum int
src string
}

func main() {
if len(idList) == 0 {
fmt.Println("no books")
return
}
Login() //登录

wg.Add(1)
go PicsDownload() //开启图片监听线程

for index, id := range idList {
LoadBook(index, id)
}
close(picChan)

wg.Wait()
}

func standarlizeName(oriStr string) string {
replacer := strings.NewReplacer("/", "", "|", "", "\\", "", ":", " ", "*", "", "?", "", "<", "[", ">", "]", ":", " ")
return replacer.Replace(oriStr)
}

func PicsDownload() {
defer wg.Done()
picChan = make(chan *pic, 100000)
for picture := range picChan {
wg.Add(1)
go PicDownload(picture)
}
}

func PicDownload(picture *pic) {
defer wg.Done()
//HTTP代理
proxyAddress, _ := url.Parse(PROXY)
client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyAddress)}}

//打开链接
req, _ := http.NewRequest("GET", picture.src, nil)
resp, err := client.Do(req)
Check(err)

//读取内容
body, err := ioutil.ReadAll(resp.Body)
Check(err)
defer resp.Body.Close()

//检查路径,保存
filePath := NmList[picture.bkNum][picture.chNum] + "/" + fmt.Sprintf("%02d", picture.picNum) + ".jpg"

// 以写|创建的方式打开目标文件(因为文件夹在创建时就已经建好,所以k可以不用检查)
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644)
Check(err)
defer f.Close()

f.Write(body)
}

func Load(url string) {
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitVisible(`//div[@class='appBottomMenu']`))
Check(err)
time.Sleep(2 * time.Second) //无论如何,等待2秒再进行下一步操作
}

func LoadBook(index int, id string) {
bookNm := ""
var contentNode []*cdp.Node

Load(pageUrl + id)
err := chromedp.Run(ctx,
chromedp.TextContent(`//div[@class='comic-title text-start']`, &bookNm, chromedp.BySearch),
chromedp.Nodes(`//div[@class="col-3 m-0 mb-1"]`, &contentNode))
Check(err)

fmt.Printf("\nStart to process book [%d]%s \n", index+1, bookNm)

NmList[index][0] = savePath + "/" + standarlizeName(bookNm)

//章节数
chaps := len(contentNode)
fmt.Printf("\nTotal: %d Chapters \n", chaps)

//章节base1
for i := 1; i <= chaps; i++ {
chapNm := ""
chromedp.Run(ctx, chromedp.TextContent(contentNode[chaps-i].PartialXPath(), &chapNm))
chapNm = standarlizeName(chapNm)
fmt.Printf("[%d] %s \n", i, chapNm)
NmList[index][i] = NmList[index][0] + "/[" + fmt.Sprintf("%02d", i) + "] " + chapNm //章节数补全2位,加上章节名

if !pathExists(NmList[index][i]) { //建好文件夹
err = os.MkdirAll(NmList[index][i], 0766)
Check(err)
}
}
fmt.Printf("\n")
bar := progressbar.New(chaps) //显示一个进度条,用来展示每本书的进度
for i := 1; i <= chaps; i++ {
LoadChap(index, i, bar)
}
}

func LoadChap(bookNum int, chapNum int, bar *progressbar.ProgressBar) {
//分为两步,下拉
Load(pageUrl2 + idList[bookNum] + "&chapter=" + strconv.Itoa(chapNum))
ScrollDown()

var picNodes []*cdp.Node
err := chromedp.Run(ctx,
chromedp.Nodes(`//div[@class='chapter-images wide-block pt-2 pb-2 my-bg-white']/img`, &picNodes))
Check(err)

// println("total Picnum = ", len(picNodes))

for i := 0; i < len(picNodes); i++ {
picture := &pic{bookNum, chapNum, i, picNodes[i].AttributeValue("data-src")}
picChan <- picture
}

bar.Add(1) //标记这章节已经完成
}

func ScrollDown() {
var prgsBar string
for i := 0; ; {
err := chromedp.Run(ctx,
chromedp.Evaluate("window.scrollBy(0,document.body.scrollHeight)", nil),
chromedp.Sleep(2*time.Second),
chromedp.TextContent(`.//div[@class="w-100 text-center text-black-50 my-read-tip"]`, &prgsBar))
Check(err)
if prgsBar == "100 % (點擊可跳轉)" {
if i > 2 {
break
} else {
i++
}
} else {
if i != 0 {
i = 0
}
}
}
}

func Login() {
err := chromedp.Run(ctx,
chromedp.Navigate(homeUrl),
chromedp.WaitVisible("#email1", chromedp.ByID),
chromedp.SendKeys("#email1", account+kb.Tab+password, chromedp.ByID),
chromedp.Click(`.//button[@type="submit"]`, chromedp.BySearch),
chromedp.WaitVisible(`//div[@class='appBottomMenu']`))
Check(err)
fmt.Println("login complete")

fmt.Println("\n...you've 15 second to change the picture quality...")
time.Sleep(100)
Load(pageUrl2 + idList[0] + "&chapter=1")
time.Sleep(10 * time.Second)
fmt.Println("processing start")

}

func pathExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}

func init() { //chromedp初始化
headlessFlag := chromedp.Flag("headless", false) //有头模式
opts := append(
chromedp.DefaultExecAllocatorOptions[:],
chromedp.NoDefaultBrowserCheck, //不检查默认浏览器
headlessFlag, //无头
chromedp.IgnoreCertErrors, //忽略错误
chromedp.Flag("blink-settings", "imagesEnabled=false"), //不加载gif图像 因为有可能会卡住
chromedp.DisableGPU, //关闭GPU渲染
chromedp.NoSandbox, //不适用谷歌的sanbox模式运行
chromedp.NoFirstRun, //设置网站不是首次运行
chromedp.Flag("disable-web-security", true), //禁用网络安全标志
chromedp.Flag("disable-extensions", true), //关闭插件支持
chromedp.Flag("disable-default-apps", true), //关闭默认浏览器检查
chromedp.WindowSize(1280, 1024), //初始大小
chromedp.Flag("run-all-compositor-stages-before-draw", true), //在呈现所有数据之前防止创建Pdf
chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`), //设置UserAgent
)

allocCtx, _ := chromedp.NewExecAllocator(context.Background(), opts...)
ctx, _ = chromedp.NewContext(
allocCtx,
chromedp.WithLogf(log.Printf),
)
chromedp.Run(ctx, make([]chromedp.Action, 0, 1)...) //打开,但是什么都不干
fmt.Println("-- chromedp start success --")
}

func init() { //customsettings初始化
type custom struct {
Account string `json:"Account"`
Password string `json:"Password"`
IDList []string `json:"IDList"` //漫画列表
SavePath string `json:"SavePath"`
PROXY string `json:"PROXY"`
}

_, projectPath, _, _ := runtime.Caller(0)
filePath := path.Dir(projectPath) + "/custom.json"

fmt.Println(">fetch customInfo from", filePath)

jsonFile, err := os.Open(filePath)
Check(err)
defer jsonFile.Close()

jsonData, err := ioutil.ReadAll(jsonFile)
Check(err)

var customInfo custom
json.Unmarshal(jsonData, &customInfo)
account, password, idList, savePath, PROXY = customInfo.Account, customInfo.Password, customInfo.IDList, customInfo.SavePath, customInfo.PROXY

if PROXY == "" {
PROXY = "http://127.0.0.1:1080/"
}

fmt.Println("-- custom.json loadng success --")
fmt.Println("Welcome!", account)
fmt.Println("your savePath:", savePath)
fmt.Println("your cidList:", idList)
fmt.Println("your proxy:", idList)
}

func Check(err error) {
if err != nil {
log.Fatal(err)
}
}
./custom.json
  • 放在.exe或者.go的同目录下
{

    "Account": "账号,空置则手动填写(不着急,检定登陆成功才会开始下一步)",

    "Password": "密码,空置则手动填写,建议不要空置",

    "IdList": ["63944d19d6ffb644413e139f漫画的cid列表,用逗号分割"],

    "SavePath": "G:/Comic/2023.02,文件目录",

    "PROXY": "http://127.0.0.1:1080/默认端口,空置也行"

}