发布于 

Golang爬取qq空间说说

web #golang

第一次的Golang代码!纪念一下~~(chromedp就这么冷门吗资料都好难找…这波是被迫99%原创了(?

主要内容是:

  1. 载入网页,等待用户登录,检测到登录后开始爬取

  2. 进入说说页面先检查总页数,跳转到用户需求的起始页,开始爬取

  3. 根据我的观察(?),qq空间网页结构如下:

  1. li[@class=’feed’] 存储了每一个说说/转发单元
  2. (feed的下一层)/div[@class=’box bgr3’]
    i. /div[@class=’bd’]存储了说说本体/转发内容
         /pre[@class='content' and @style='display:inline']存储了文字信息
    
    ii. /div[@class=’md rt_content’]存储了转发信息(如果名字为’md’,就是原创内容)
         /div[@class='quote bor2']
             /div[@class='bd'] 文字信息
                 /a content是转发者信息,profileuin是转发者的QQ号
                 /pre[@style='display:inline'] 存储文字信息
             /div[@class='md'] 图片信息
                 /a href中是转发图片的直链
    
    iii. /div[@class=’ft’]
         /span[@class='c_tx3']
             /a title是时间码(形如:2023年1月3日 19:45)
    
    iv. /div[@class=’box_extra bor3’]//此处先不做了好麻烦()
         /div[@class='feed_like']
             /a 可能有多个,是列举的点赞者和人数(形如:292人)
         /div[@class='mod_comment']
             /div[@class='mod_comments']
                 /ul (以下每一组评论是一个Bor)
                     //a[@class="nickname"] content是昵称,href包含qq号信息
                     //span[@style="]
    
  1. 根据这个结构,先找到每个说说节点,即Xpath为.//div[@class='box bgr3'],然后在其子结点中分别提取说说内容、时间码即可形成简单的日志。想要过滤转发的说说,故对div[2]以class属性为md和md rt_content进行区分即可。

  2. 希望体验一下Go语言知名的Goroutine并发,故把数据的处理、处理的数据生成日志两个步骤分配给独立线程。由于qzone本身需要ck登录才能使用,所以多线爬取也会被快速关黑屋(访问人数过多),这里就没有尝试代理池x
    具体的内容就是read线程用来连续从网页爬取数据,把数据塞进textChannel,output线程时刻监听textChannel的动向、每收集50条说说(如果读取到结束信号,则不满50条说说也操作)就启动一个go线程(toFile)生成一个文件。

  3. 就效果来说感觉相当满意了!大约120页左右还是被关了小黑屋就是了(小声)可能多sleep会更安全吧~不过120页我觉得是个能接受的程度了~速度的话也比Python-Selenium快很多!
    不足的话就是没有做超时刷新,一旦卡住就会出问题(不过chromedp稳定性还是挺不错的),等有空了就加上x

  4. 总体的体验就是:这库是真难用啊啊啊!不过写这个还是很有趣~明后天就来做漫画爬取的golang版本~动态网页还是爬取上比较麻烦呢(叹气)
    细说这个难用,啊啊啊啊啊啊啊啊,啊啊啊啊啊(无意义的悲鸣),你这怎么把函数放在参数表里运行当常态,痛苦
    比如说webEngine创建好了以后,python-Selenium可以直接webEngine.get(url),就是get()是引擎类的类函数,但是这里所有相关engine的操作都是要“嵌套”的,这里要用chromedp.run(webEngine, xxxxxxx,xxxxxxx)(xxxx就是所有要运行的函数)。这样看还好,但是导致一个问题,这些函数的返回值都是函数参数,比如我查找一个元素它就不能直接把这个元素返回到左值,而是要先定义一个元素变量把地址传给找元素的函数……带来很多没必要的麻烦(我并不能体会到这种写法的好处……还是更喜欢把这种常用的、有返回值、和类紧密相关的函数写成类函数)
    不过最后还是写完了!很开心!总之就是非常开心!>w<

package main

import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"sync"
"time"

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

// 全局变量
const (
qqID string = "2498742177"
savePath string = "F:/Temp"
docRange int = 50
)

var (
ctx context.Context
wg sync.WaitGroup
) //每篇容量为50条说说,如果完成则不满50条说说也输出

func main() {
url := "https://user.qzone.qq.com/" + qqID + "/311"
fmt.Println("please log in") //正在加载页面
visit(url) //初次加载
deal(254, 999) //需要加载的范围(第二个值如果过大,会自动修正为最后一页)
wg.Wait()
}

func deal(start int, end int) {

pageNum := ini_load()

//规范化输入
if start < 1 {
start = 1
}
if end > pageNum || end < start {
end = pageNum
}

textChan := make(chan string, 100000)
wg.Add(1)
go output(textChan) //创建一个进程,用来处理输出的文本

turnToPage(start)

for i := start; i <= end; i++ {
load()
read(textChan)
time.Sleep(1 * time.Second)
nextPage()

if i == end {
fmt.Println("all pages have been read. ")
textChan <- "end"
}
}
}

func visit(url string) {
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitVisible(`#app_canvas_frame`, chromedp.ByID))
check(err)
}

func ini_load() int {
time.Sleep(2 * time.Second)
i := 0
for {
i++
var str string
err := chromedp.Run(ctx,
chromedp.Evaluate("window.scrollBy(0,1000)", nil),
chromedp.Sleep(time.Second),
chromedp.TextContent(`.//a[@title="末页"]`, &str)) //找到最大页数
check(err)
if len(str) != 0 {
fmt.Println("total page: " + str)
fmt.Println("initializing completed")
pageNum, _ := strconv.Atoi(str)
return pageNum
} else {
if i > 10 {
err = fmt.Errorf("scroll error: can't find pageEnd")
break
}
}
}
return -1
}

func load() {
time.Sleep(2 * time.Second)
i := 0
for {
i++
var str string
err := chromedp.Run(ctx,
chromedp.Evaluate("window.scrollBy(0,1000)", nil),
chromedp.Sleep(time.Second),
chromedp.TextContent(`.//p[@class="mod_pagenav_main"]/span[@class="current"]/span`, &str))
check(err)
if len(str) != 0 {
fmt.Println("* current page: " + str)
fmt.Println("loading completed")
break
} else {
if i > 10 {
err = fmt.Errorf("scroll error: can't find pageEnd")
break
}
}
}
}

func read(textChan chan string) {
fmt.Println("reading...")

var nodes []*cdp.Node

err := chromedp.Run(ctx,
// chromedp.Evaluate(`document.getElementsByClassName('app_canvas_frame')[0].contentWindow.document.body.outerHTML;`, nil),
chromedp.Sleep(time.Second),
chromedp.Nodes(`.//div[@class='box bgr3']`, &nodes),
)

check(err)

fmt.Println("total: ", len(nodes))

for _, node := range nodes { //对于每个找到的说说节点

path := node.FullXPath()
path = path[strings.Index(path, "//")+2:] //去除iframe前面的内容

var text string
var share string
var date string

textPath := path + "/div[2]/pre" //子节点地址2:text
sharePath := path + "/div[3]" //子节点地址3:share(转发则该元素class为md rt_content)
datePath := path + "/div[4]/div/span/a" //子节点地址4:date

err = chromedp.Run(ctx,
chromedp.TextContent(textPath, &text),
chromedp.AttributeValue(sharePath, "class", &share, nil),
chromedp.AttributeValue(datePath, "title", &date, nil),
)
check(err)

wg.Add(1) //启动进程进行登记
go func() {
defer wg.Done() //进程结束进行登记
if text != "" && share == "md" { //如果文本为空或来源于转发,则跳过以下步骤
textChan <- "<!-- node " + date + "-->\n\n" + text + "\n\n" //开一个进程进行输出
}
}()

}
}

func turnToPage(i int) {
err := chromedp.Run(ctx,
chromedp.SendKeys(`.//span[@class="mod_pagenav_turn"]/input`, strconv.Itoa(i)+kb.Enter),
// chromedp.Click(`//a[@title='下一页']`),
)
check(err)
}

func nextPage() {
err := chromedp.Run(ctx,
chromedp.Click(`//a[@title='下一页']`),
)
check(err)
}

func output(textChan chan string) {
//该进程唯一(保证顺序)
defer wg.Done()

doc := ""
k := 0
for i := 0; ; {
item, _ := <-textChan //等待从通道中获取值
if item == "end" {
k++
wg.Add(1)
go toFile(doc, &k)
break
}
if i < 49 {
i++
doc = doc + item
} else {
k++
wg.Add(1)
go toFile(doc, &k)
doc = ""
i = 0 //初始化
}
}
}

func toFile(doc string, k *int) {
//该进程可多发
defer wg.Done()

var fileSavePath string

for {
fileSavePath = savePath + "/qzone(" + strconv.Itoa(*k) + ").md"
_, err := os.Stat(fileSavePath)
if err != nil {
break
} else {
*k++
}
} //如果文件重名,则一直尝试到不重名

doc = "---\ntitle: qzone(" + strconv.Itoa(*k) + ")\nlayout: wiki\nwiki: dynamic\ntype: dynamic\norder:\n---\n" + "{% timeline %}\n\n" + doc + "{% endtimeline %}"

file, err := os.OpenFile(fileSavePath, os.O_WRONLY|os.O_CREATE, 0644) // 以写|创建的方式打开目标文件
check(err)

defer file.Close()

file.WriteString(doc)

fmt.Println("- file " + strconv.Itoa(*k) + " has been written. ")
}

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)...) //打开,但是什么都不干
}

func check(err error) {
if err != nil {
log.Fatal(err)
}
}