Go语言实战-爬取整部小说

Go语言的魅力这里就不多说了,这接上代码?

先简单介绍下爬虫用到的第三方库:

  1. github.com/gocolly/colly:轻量而又强大的爬虫框架;
  2. github.com/PuerkitoBio/goquery:Go语言版的jQuery;
  3. github.com/mozillazg/request:一个简单、优雅的请求库。

 

接下来,根据小说名和作者爬取笔趣阁整部小说,并保存在TXT文本文件中。

笔趣阁:https://www.xbiquge.la/modules/article/waps.php

小说首页:

 

找到每章小说的下一章链接: 

 

 

以上几个链接为爬虫的关键点,colly框架需要startURL来启动爬虫,需要nextURL来驱动爬虫往下爬取。

详细代码:

package main

import (
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"github.com/gocolly/colly"
	"github.com/gocolly/colly/extensions"
	"github.com/mozillazg/request"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

func main() {
	startUrl, err := findStartUrl("圣墟", "辰东")
	if err != nil {
		log.Fatal(err)
	}
	println(startUrl)

	crawl(startUrl, "test.txt")
}

// findStartUrl 根据小说名和作者找到 colly 的 startUrl
func findStartUrl(name string, author string) (string, error) {

	var targetUrl string
	var Err error

	// request请求库常规用法,用来发起请求,获取响应
	c := new(http.Client)
	req := request.NewRequest(c)
	req.Headers = map[string]string{
		"Accept-Encoding": "gzip,deflate,sdch",
		"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
	}
	req.Data = map[string]string{
		"searchkey": name,
	}
	url := "https://www.xbiquge.la/modules/article/waps.php"
	resp, _ := req.Post(url)
	defer resp.Body.Close()  // Don't forget close the response body

	html, err := resp.Text()
	if err != nil {
		Err = err
		log.Fatal(err)
	}

	// goquery 常规用法
	dom, _ := goquery.NewDocumentFromReader(strings.NewReader(html))
	dom.Find("tr").EachWithBreak(func(i int, s *goquery.Selection) bool {
		_title := s.Find("a").Eq(0).Text()
		_author := s.Find("td").Eq(2).Text()
		// 找到匹配的链接
		if _title == name && _author == author {
			url, exists := s.Find("a").Eq(0).Attr("href")
			if !exists {
				Err = nil
				log.Fatal(exists)
			}
			targetUrl = url
		}
		return true
	})

	// 根据小说的首页,获取第一个章节的页面的链接
	resp2, err2 := req.Get(targetUrl)
	if err2 != nil {
		log.Fatalln("get start url request fail...")
	}
	text, _ := resp2.Text()
	dom2, _ := goquery.NewDocumentFromReader(strings.NewReader(text))
	startUrl, ok:= dom2.Find("#list").Find("a").Eq(0).Attr("href")
	if !ok {
		log.Fatalln("get start url fail...")
	}

	return "https://www.xbiquge.la" + startUrl, Err
}

// 爬取小说内容并写入text文件
func crawl(startUrl string, fileName string) {
	c := colly.NewCollector(
		// allowed domain
		colly.AllowedDomains("www.xbiquge.la", "www.coursera.org"),
		//colly.AllowURLRevisit(),
	)

	// 使用随机user-agent
	extensions.RandomUserAgent(c)

	// HTTP 的配置
	c.WithTransport(&http.Transport{
		Proxy: http.ProxyFromEnvironment, // 使用代理
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second, // 超时时间
			KeepAlive: 30 * time.Second, // keepAlive 超时时间
			DualStack: true,
		}).DialContext,
		MaxIdleConns:          100,              // 最大空闲连接数
		IdleConnTimeout:       90 * time.Second, // 空闲连接超时
		TLSHandshakeTimeout:   10 * time.Second, // TLS 握手超时
		ExpectContinueTimeout: 1 * time.Second,
	})

	// 设置请求信息
	c.OnRequest(func(r *colly.Request) {
		r.Headers.Set("Host", "www.xbiquge.la")
		r.Headers.Set("X-Requested-With", "XMLHttpRequest")
		r.Headers.Set("Referer", "https://www.xbiquge.la/13/13959/24775996.html")
		r.Headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0")

		log.Println("Visiting", r.URL)
	})

	c.OnResponse(func(r *colly.Response) {
		log.Println("response received", r.StatusCode)
	})

	// 解析章节内容 并 写入文本文件
	c.OnHTML("div[id='content']", func(e *colly.HTMLElement) {
		// title
		title := e.DOM.ParentsUntil("body").Find(".bookname h1").Text() + "\n\n"

		println(title)

		// content
		e.DOM.Find("p").Remove()
		content := e.DOM.Text()
		str := title + content

		writeToText(str, fileName)
	})

	// 翻页
	c.OnHTML("a[href]", func(e *colly.HTMLElement) {
		e.DOM.EachWithBreak(func(i int, s *goquery.Selection) bool {
			if "下一章" == s.Text() {
				baseUrl, _ := s.Attr("href")
				url := "https://www.xbiquge.la" + baseUrl

				c.Visit(url)
				return false
			}
			return true
		})
	})

	c.OnError(func(r *colly.Response, err error) {
		log.Println("error:", r.StatusCode, err)
	})

	c.Visit(startUrl)

}

// 将字符串,写入文本文件
func writeToText(str string, fileName string) {
	wd, _ := os.Getwd()
	filePath := wd + "/src/fiction-master/file/" + fileName  // 存放小说的TXT文件路径
	f, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0644)  // 以追加方式写入
	if err != nil {
		log.Fatal(err)
	}
	defer func() {
		if err = f.Close(); err != nil {
			log.Fatal(err)
		}
	}()
	_, err = fmt.Fprintln(f, str)
	if err != nil {
		log.Fatal(err)
	}
}

 最终截图:

 整部小说也就是几分钟的事

 

Go语言的魅力远不止于此,本人也是刚学习Go语言,且行且探索

记录一下一个特殊的时间2021年5月1日

 

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用 Go 语言的 `goroutine` 和 `channel` 实现多线程爬取分页接口的数据。 首先,需要定义一个 `struct` 类型来存储每个分页的信息: ```go type Page struct { Number int Data []byte } ``` 然后,需要定义一个函数来获取某一页的数据: ```go func getPageData(pageNumber int) ([]byte, error) { // 发送 HTTP 请求并返回响应数据 } ``` 接下来,可以使用 `goroutine` 来并发地获取多个分页的数据。首先,需要创建一个 `channel` 来存储分页的信息: ```go pages := make(chan Page) ``` 然后,可以使用 `for` 循环来启动多个 `goroutine`,每个 `goroutine` 获取一个分页的数据,并将其发送到 `pages` `channel` 中: ```go for i := 1; i <= numPages; i++ { go func(pageNumber int) { data, err := getPageData(pageNumber) if err != nil { log.Printf("Error getting page %d: %v\n", pageNumber, err) return } pages <- Page{Number: pageNumber, Data: data} }(i) } ``` 在上面的代码中,`numPages` 表示总共有多少个分页。每个 `goroutine` 获取一个分页的数据,并将其封装成一个 `Page` 结构体,然后将其发送到 `pages` `channel` 中。 最后,可以使用 `for` 循环从 `pages` `channel` 中获取所有分页的数据: ```go var results []Page for i := 1; i <= numPages; i++ { page := <-pages results = append(results, page) } ``` 在上面的代码中,`results` 是一个 `[]Page` 类型的数组,用来存储所有分页的数据。通过 `<-pages` 语句可以从 `pages` `channel` 中获取一个分页的数据,并将其添加到 `results` 数组中。 完整的示例代码如下: ```go package main import ( "log" ) type Page struct { Number int Data []byte } func getPageData(pageNumber int) ([]byte, error) { // 发送 HTTP 请求并返回响应数据 } func main() { numPages := 10 pages := make(chan Page) for i := 1; i <= numPages; i++ { go func(pageNumber int) { data, err := getPageData(pageNumber) if err != nil { log.Printf("Error getting page %d: %v\n", pageNumber, err) return } pages <- Page{Number: pageNumber, Data: data} }(i) } var results []Page for i := 1; i <= numPages; i++ { page := <-pages results = append(results, page) } // 处理结果 } ``` 注意,在实际使用中,可能需要对 `getPageData()` 函数进行一些优化,例如使用连接池来复用 HTTP 连接,避免频繁地创建和销毁连接。另外,也需要考虑一些错误处理和超时机制,以保证程序的稳定性和健壮性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值