【golang】微信公众号批量下载为PDF

起因前天女朋友说想下载一些公众号的文章为PDF,但是一个一个用浏览器打开,再打印的太麻烦了,网上找的又需要付费,那我自然…

由于最近golang使用的多所以我使用了chromedp,git示例地址

ExampleDescription
clickuse a selector to click on an element
cookieset a HTTP cookie on requests
download_filedo headless file downloads
download_imagedo headless image downloads
emulateemulate a specific device such as an iPhone
evalevaluate javascript and retrieve the result
fastextract and render data from a page
forecastextract and render data from a page
geoipextract and render data from a page
headersadd extra HTTP headers to browser requests
keyssend key events to an element
latlonretrieve the latitude/longitude from google maps, using the browser’s target events
logicmore complex logic beyond simple actions
multiuse headless-shell and a container (Docker, Podman, other)
pdfcapture a pdf of a page
proxyauthenticate a proxy server which requires authentication
remoteconnect to an existing Chrome DevTools instance using a remote WebSocket URL
screenshottake a screenshot of a specific element and of the entire browser viewport
submitfill out and submit a form
subtreepopulate and travel a subtree of the DOM
textextract text from a specific element
uploadupload a file on a form
visiblewait until an element is visible

关于pdf的:

// Command pdf is a chromedp example demonstrating how to capture a pdf of a
// page.
package main

import (
	"context"
	"fmt"
	"log"
	"os"

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

func main() {
	// create context
	ctx, cancel := chromedp.NewContext(context.Background())
	defer cancel()

	// capture pdf
	var buf []byte
	if err := chromedp.Run(ctx, printToPDF(`https://www.google.com/`, &buf)); err != nil {
		log.Fatal(err)
	}

	if err := os.WriteFile("sample.pdf", buf, 0o644); err != nil {
		log.Fatal(err)
	}
	fmt.Println("wrote sample.pdf")
}

// print a specific pdf page.
func printToPDF(urlstr string, res *[]byte) chromedp.Tasks {
	return chromedp.Tasks{
		chromedp.Navigate(urlstr),
		chromedp.ActionFunc(func(ctx context.Context) error {
			buf, _, err := page.PrintToPDF().WithPrintBackground(false).Do(ctx)
			if err != nil {
				return err
			}
			*res = buf
			return nil
		}),
	}
}

更换成需要下载的url后的效果![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/4e515638ec64475a9913310af9a189c3.png在这里插入图片描述

我一开始以为成了,发给我宝,我宝直呼,你到底干了什么?第二页的图片呢?你这样我怎么用来下载PDF,我淡淡一笑,很简单,我来改bug不就行了,说罢,我的气息不再掩饰,四年编程修为!顷刻炼化…咳咳咳

很简单,直接浏览器打开链接

在这里插入图片描述

经过略微观察,发现其实是懒加载的问题,图片不是一次加载进来的,那怎么办呢,简单 翻翻文档,有个eval 执行JS的方法,而且有返回值,那我只需要执行一个JS的滑动动作即可。再通过返回值的特性,去获取到页面的高度,用高度除偏移量即可得出滑动次数,再乘时间,即可得到等待时间(一开始没写等待时间,导致还是没图…)代码:

jsStr := `var i = 2
    var element = document.documentElement
    element.scrollTop = 0;  // 不管他在哪里,都让他先回到最上面
 
    // 设置定时器,时间即为滚动速度
    function main() {
        if (element.scrollTop + element.clientHeight == element.scrollHeight) {
            clearInterval(interval)
            console.log('已经到底部了')
        } else {
            // 300 代表每次移动300px
            element.scrollTop += 300;
            console.log(i);
            i += 1;
        }
    }
    // 定义ID 200代表300毫秒滚动一次
    interval = setInterval(main, 100)`

	lengthStr := `var element = document.documentElement
	element.scrollTop = 0;
	element.scrollHeight
	`

	lengthInt := 0
	err := chromedp.Run(ctx,
		chromedp.Evaluate(lengthStr, &lengthInt),
	)
	if err != nil {
		log.Fatal("123:", err)
	}

	//循环滚轮实现
	if lengthInt > 0 {
		fmt.Println("页高度:", lengthInt)
		err = chromedp.Run(ctx,
			chromedp.Evaluate(jsStr, nil),
		)

		sleepDuration := time.Duration(lengthInt/300*100) * time.Millisecond
		fmt.Println("翻页时间:", sleepDuration)
		time.Sleep(sleepDuration)
		//无效代码 纯闲得 看看滚动条位置
		topInt := 0
		err = chromedp.Run(ctx,
			chromedp.Evaluate(`element.scrollTop`, &topInt),
		)
	}

完整代码

package main

import (
	"context"
	"fmt"
	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/chromedp"
	"log"
	"os"
	"strings"
	"time"
)

var title string

func main() {
	listByte, err := os.ReadFile("./url.txt")
	if err != nil {
		return
	}
	list := strings.Split(string(listByte), "\n")
	fmt.Println("共", len(list), "个")
	//var wg sync.WaitGroup
	for i, s := range list {
		//wg.Add(1) The filename, directory name, or volume label syntax is incorrect.
		//go func(s string) {
		// create context
		url := strings.TrimPrefix(s, "")
		ctx, cancel := chromedp.NewContext(context.Background())
		ctx, cancelTimeout := context.WithTimeout(ctx, 30*time.Second)
		// capture pdf
		fmt.Println("第", i+1, "个")
		var buf []byte
		if err := chromedp.Run(ctx, printToPDF(
			url,
			&buf, &title)); err != nil {
			log.Fatal(err)
		}
		title = strings.ReplaceAll(title, "/", "-")
		title = strings.ReplaceAll(title, ":", ":")
		title = strings.ReplaceAll(title, "*", "-")
		title = strings.ReplaceAll(title, "?", "?")
		title = strings.ReplaceAll(title, `"`, "'")
		title = strings.ReplaceAll(title, "<", "《")
		title = strings.ReplaceAll(title, ">", "》")
		if err := os.WriteFile(title+".pdf", buf, 0o644); err != nil {
			log.Fatal(err)
		}
		fmt.Println("写入 ", title, ".pdf")
		ctx.Done()
		cancel()
		cancelTimeout()
		//wg.Done()
		//}(s)
	}
	//wg.Wait()
	fmt.Println("宝,结束辣")
	time.Sleep(50 * time.Second)
}

// slowScrollToBottom 缓慢滚动到页面底部的操作
func slowScrollToBottomctx(ctx context.Context) {
	jsStr := `var i = 2
    var element = document.documentElement
    element.scrollTop = 0;  // 不管他在哪里,都让他先回到最上面
 
    // 设置定时器,时间即为滚动速度
    function main() {
        if (element.scrollTop + element.clientHeight == element.scrollHeight) {
            clearInterval(interval)
            console.log('已经到底部了')
        } else {
            // 300 代表每次移动300px
            element.scrollTop += 300;
            console.log(i);
            i += 1;
        }
    }
    // 定义ID 200代表300毫秒滚动一次
    interval = setInterval(main, 100)`

	lengthStr := `var element = document.documentElement
	element.scrollTop = 0;
	element.scrollHeight
	`

	lengthInt := 0
	err := chromedp.Run(ctx,
		chromedp.Evaluate(lengthStr, &lengthInt),
	)
	if err != nil {
		log.Fatal("123:", err)
	}

	//循环滚轮实现
	if lengthInt > 0 {
		fmt.Println("页高度:", lengthInt)
		err = chromedp.Run(ctx,
			chromedp.Evaluate(jsStr, nil),
		)

		sleepDuration := time.Duration(lengthInt/300*100) * time.Millisecond
		fmt.Println("翻页时间:", sleepDuration)
		time.Sleep(sleepDuration)
		//无效代码 纯闲得 看看滚动条位置
		topInt := 0
		err = chromedp.Run(ctx,
			chromedp.Evaluate(`element.scrollTop`, &topInt),
		)
	}

}

// printToPDF 打印特定的 PDF 页面
func printToPDF(urlstr string, res *[]byte, title *string) chromedp.Tasks {
	return chromedp.Tasks{
		chromedp.Navigate(urlstr),
		chromedp.ActionFunc(func(ctx context.Context) error {
			//获取长度
			slowScrollToBottomctx(ctx)
			return nil
		}),
		chromedp.Text(`#activity-name`, title, chromedp.NodeVisible),
		chromedp.ActionFunc(func(ctx context.Context) error {
			var err error
			*res, _, err = page.PrintToPDF().WithPrintBackground(true).Do(ctx)
			return err
		}),
	}
}

我git上有已经打包好的,安装一个谷歌浏览器,谷歌浏览器下载地址就可以直接用,

打包好的exe: https://github.com/cydxin/WeChat-public-account-article-to-PDF

直接下载run.exe和url.txt,把你需要下载的链接粘贴到url.txt里面,可能需要把自动换行关了,不然有些很长,会导致错误的url,毕竟我是直接按行访问的
### 实现微信登录功能 #### OAuth2授权流程 为了使用户能够在微信公众号内置浏览器中完成登录操作,需遵循OAuth2协议。具体来说,在前端页面加载时应重定向至特定的微信授权链接[^3]: ```javascript let redirect_uri = encodeURI(window.location.href).split('#')[0], appid = "YOUR_APP_ID"; // 替换成实际应用ID let wx_url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect`; window.location.href = wx_url; ``` 此代码片段会引导用户的设备访问由`wx_url`指定的位置,从而触发微信服务器向第三方应用程序发送临时凭证(即`code`),该凭证稍后可用于交换更持久性的令牌。 #### 后端处理逻辑 一旦接收到带有`code`参数的回调请求,服务端应当立即利用这个一次性码去换取access_token以及其他必要的个人信息。以下是基于Golang编写的简化版控制器函数来构建这样的响应机制[^2]: ```go // AuthLogin @Title 授权URL // @router /authLogin [post,get] func (u *UserController) AuthLogin() { url := fmt.Sprintf("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=200#wechat_redirect", appid, baseUrl) u.Success(0, url, "成功") } ``` 请注意替换其中的变量值以匹配具体的项目环境配置。 #### JS-SDK初始化与权限声明 为了让网页能够调用微信提供的JavaScript API,开发者还需要提前准备好一系列准备工作,比如注册并启用相应的API接口名称列表。对于地理位置相关的交互而言,则至少要包含如下所示的内容[^1]: ```json { "jsApiList": ["openLocation","getLocation"] } ``` 同时确保已按照官方指引完成了JS-SDK的安全域名备案工作,并通过有效的签名算法验证当前页面合法性之后再执行任何敏感的操作。 #### 注意事项 - **安全性考量**:在整个过程中务必妥善保管各类密钥信息,防止泄露给未经授权方;另外建议采用HTTPS加密传输方式保障数据安全。 - **调试工具的应用**:可以借助微信公众平台提供的在线沙盒账号来进行初步的功能测试,方便快捷地获取到所需的AppId和AppSecret用于本地模拟运行期间。 - **多团队协作管理**:当存在多名成员共同参与同一个项目的开发周期内,记得各自独立创建专属的小程序实例以便于分工合作而不至于混淆彼此之间的资源文件版本控制等问题的发生。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值