package main import ( "fmt" "io/ioutil" "log" "os" "path/filepath" "strings" "time" "github.com/wechat-crawler/configs" "github.com/wechat-crawler/pkg/utils" "github.com/wechat-crawler/pkg/wechat" ) func main() { fmt.Println("微信公众号文章爬取工具 v1.0") fmt.Println("==============================") // 初始化配置 cfg := configs.NewConfig() // 检查命令行参数 if len(os.Args) > 1 && os.Args[1] != "" { // 如果提供了文章链接参数,使用新的方法 articleLink := os.Args[1] fmt.Printf("\n从文章链接开始操作: %s\n", articleLink) // 直接获取公众号主页链接,不依赖cookie,不进行后续爬取 officialAccountLink, err := GetOfficialAccountLinkFromArticleOnly(articleLink) if err != nil { log.Fatalf("获取公众号主页链接失败: %v", err) } fmt.Printf("公众号主页链接: %s\n", officialAccountLink) return } // 显示功能菜单 showMenu(cfg) fmt.Println("\n操作完成!") } func startCrawling(cfg *configs.Config) { // 读取cookie信息 cookiePath := filepath.Join(cfg.RootPath, "cookie.txt") cookieContent, err := ioutil.ReadFile(cookiePath) if err != nil { fmt.Printf("读取cookie失败: %v,请确保cookie.txt文件存在\n", err) return } // 解析cookie获取必要参数 cookieStr := string(cookieContent) biz, uin, key, passTicket, err := parseCookieInfo(cookieStr) if err != nil { fmt.Printf("解析cookie信息失败: %v\n", err) return } // 创建爬虫实例 crawler, err := wechat.NewWechatCrawler(biz, uin, key, passTicket, cfg) if err != nil { fmt.Printf("创建爬虫实例失败: %v\n", err) return } // 获取公众号名称 nickname, err := crawler.GetOfficialAccountName() if err != nil { fmt.Printf("获取公众号名称失败: %v\n", err) return } fmt.Printf("\n正在爬取公众号: %s\n", nickname) // 创建公众号目录 officialPath := filepath.Join(cfg.RootPath, "data", nickname) err = utils.CreateDir(officialPath) if err != nil { fmt.Printf("创建目录失败: %v\n", err) return } // 获取所有文章列表 fmt.Println("\n开始获取文章列表...") articleList := [][]string{} page := 0 for { fmt.Printf("正在获取第 %d 页文章...\n", page+1) result, err := crawler.GetNextList(page) if err != nil { fmt.Printf("获取第 %d 页文章失败: %v\n", page+1, err) break } if result["m_flag"].(int) == 0 { fmt.Printf("已获取全部文章,共 %d 页\n", page) break } passageList := result["passage_list"].([][]string) articleList = append(articleList, passageList...) fmt.Printf("第 %d 页获取成功,新增 %d 篇文章\n", page+1, len(passageList)) page++ // 防止请求过于频繁 time.Sleep(2 * time.Second) } fmt.Printf("\n共获取到 %d 篇文章\n", len(articleList)) // 保存文章列表 err = crawler.SaveArticleListToExcel(officialPath, articleList, nickname) if err != nil { fmt.Printf("保存文章列表失败: %v\n", err) } // 转换文章链接(这个方法不需要单独调用,GetArticleList已经内部调用了) // 文章列表已经在GetArticleList方法中转换过了 // 读取文章链接 links, err := crawler.ReadArticleLinksFromExcel(filepath.Join(officialPath, "文章列表(article_list)_原始链接.txt")) if err != nil { // 如果读取失败,直接使用articleList中的链接 links = []string{} for _, article := range articleList { links = append(links, article[3]) } } // 获取文章详情 fmt.Println("\n开始获取文章详情...") for i, link := range links { fmt.Printf("正在获取文章 %d/%d: %s\n", i+1, len(links), link) // 转换链接 transformedLink := utils.TransformLink(link) // 获取文章内容 content, err := crawler.GetOneArticle(transformedLink) if err != nil { fmt.Printf("获取文章内容失败: %v\n", err) continue } // 提取文章标题 title := "" for _, article := range articleList { if article[3] == link { title = article[2] break } } // 提取创建时间 createTime := "" for _, article := range articleList { if article[3] == link { createTime = article[1] break } } // 提取评论ID commentID, _ := utils.ExtractFromRegex(content, "comment_id = '(.*?)';") // 生成req_id reqID := fmt.Sprintf("%d", time.Now().UnixNano()) // 获取文章统计信息 stats, err := crawler.GetArticleStats(link, title, commentID, reqID, createTime) if err != nil { fmt.Printf("获取文章统计信息失败: %v\n", err) // 设置默认值 stats = map[string]string{ "read_num": "0", "old_like_num": "0", "share_num": "0", "show_read": "0", } } // 获取文章评论 comments, commentLikes, err := crawler.GetArticleComments(commentID) if err != nil { fmt.Printf("获取文章评论失败: %v\n", err) comments = []string{} commentLikes = []string{} } // 解析文章正文内容(这里简化处理,实际需要使用goquery进行HTML解析) articleContent := parseArticleContent(content) // 创建文章详情对象 detail := &wechat.ArticleDetail{ LocalTime: utils.GetCurrentTime(), CreateTime: createTime, Title: title, Link: transformedLink, Content: articleContent, ReadCount: stats["read_num"], LikeCount: stats["old_like_num"], ShareCount: stats["share_num"], ShowRead: stats["show_read"], Comments: comments, CommentLikes: commentLikes, CommentID: commentID, } // 保存文章详情 err = crawler.SaveArticleDetailToExcel(detail, officialPath) if err != nil { fmt.Printf("保存文章详情失败: %v\n", err) } // 防止请求过于频繁 time.Sleep(3 * time.Second) } fmt.Printf("\n公众号 %s 爬取完成!\n", nickname) } func parseCookieInfo(cookieStr string) (string, string, string, string, error) { // 从cookie中提取必要的参数 biz, err := utils.ExtractFromRegex(cookieStr, "__biz=(.*?);") if err != nil { return "", "", "", "", fmt.Errorf("未找到__biz参数") } uin, err := utils.ExtractFromRegex(cookieStr, "uin=(.*?);") if err != nil { return "", "", "", "", fmt.Errorf("未找到uin参数") } key, err := utils.ExtractFromRegex(cookieStr, "key=(.*?);") if err != nil { return "", "", "", "", fmt.Errorf("未找到key参数") } passTicket, err := utils.ExtractFromRegex(cookieStr, "pass_ticket=(.*?);") if err != nil { return "", "", "", "", fmt.Errorf("未找到pass_ticket参数") } return biz, uin, key, passTicket, nil } func parseArticleContent(content string) []string { // 这里简化处理,实际需要使用goquery进行HTML解析 // 提取文章正文内容 var result []string // 尝试提取body内容 bodyContent, err := utils.ExtractFromRegex(content, "]*>(.*?)") if err != nil { result = append(result, "无法解析文章内容") return result } // 简单去除HTML标签 txt := strings.ReplaceAll(bodyContent, "<", " <") txt = strings.ReplaceAll(txt, ">", "> ") // 按行分割并过滤空白行 lines := strings.Split(txt, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line != "" && !strings.HasPrefix(line, "<") { result = append(result, line) // 限制内容长度 if len(result) > 100 { break } } } return result } // 仅从文章链接获取公众号主页链接,不依赖cookie,不进行后续爬取 func GetOfficialAccountLinkFromArticleOnly(articleLink string) (string, error) { // 创建一个简单的爬虫实例,不需要cookie信息 crawler := wechat.NewSimpleCrawler() // 从文章链接获取公众号主页链接 officialAccountLink, err := crawler.GetOfficialAccountLinkFromArticle(articleLink) if err != nil { return "", fmt.Errorf("获取文章内容失败: %v", err) } return officialAccountLink, nil } // 从文章链接开始爬取 func startCrawlingFromArticleLink(cfg *configs.Config, articleLink string) { // 读取cookie信息 cookiePath := filepath.Join(cfg.RootPath, "cookie.txt") cookieContent, err := ioutil.ReadFile(cookiePath) if err != nil { fmt.Printf("读取cookie失败: %v,请确保cookie.txt文件存在\n", err) return } // 解析cookie获取必要参数 cookieStr := string(cookieContent) _, uin, key, passTicket, err := parseCookieInfo(cookieStr) if err != nil { fmt.Printf("解析cookie信息失败: %v\n", err) return } // 创建爬虫实例(biz将通过文章链接获取) crawler, err := wechat.NewWechatCrawler("", uin, key, passTicket, nil) if err != nil { fmt.Printf("创建爬虫实例失败: %v\n", err) return } // 1. 从文章链接获取公众号主页链接 officialAccountLink, err := crawler.GetOfficialAccountLinkFromArticle(articleLink) if err != nil { log.Fatalf("获取公众号主页链接失败: %v", err) } fmt.Printf("获取到公众号主页链接: %s\n", officialAccountLink) // 2. 获取公众号名称 officialAccountName, err := crawler.GetOfficialAccountName() if err != nil { log.Printf("警告: 获取公众号名称失败: %v,将使用默认名称", err) officialAccountName = "公众号_" + time.Now().Format("20060102150405") } fmt.Printf("获取到公众号名称: %s\n", officialAccountName) // 3. 创建输出目录 outputDir := filepath.Join(cfg.RootPath, "data", officialAccountName) err = utils.CreateDir(outputDir) if err != nil { log.Fatalf("创建输出目录失败: %v", err) } // 4. 获取文章列表(使用我们新实现的GetArticleList方法) fmt.Println("\n开始获取文章列表...") articleList, err := crawler.GetArticleList() if err != nil { log.Fatalf("获取文章列表失败: %v", err) } // 5. 保存文章列表到Excel excelPath := filepath.Join(outputDir, "文章列表.xlsx") if err := crawler.SaveArticleListToExcel(outputDir, articleList, officialAccountName); err != nil { log.Fatalf("保存文章列表失败: %v", err) } fmt.Printf("文章列表已保存到: %s\n", excelPath) fmt.Printf("共获取到 %d 篇文章\n", len(articleList)) // 6. 获取文章详情(使用我们新实现的GetDetailList方法) fmt.Println("\n开始获取文章详情...") if err := crawler.GetDetailList(articleList, outputDir); err != nil { log.Fatalf("获取文章详情失败: %v", err) } fmt.Printf("\n公众号 %s 爬取完成!\n", officialAccountName) } // 显示功能菜单 func showMenu(cfg *configs.Config) { screenText := `请输入数字键! 数字键1:仅获取公众号主页链接(输入公众号下任意一篇已发布的文章链接即可,无需cookie) 数字键2:使用cookie信息爬取公众号文章 数字键3:通过access_token和pages获取文章链接列表 数字键4:根据先前生成的文章链接列表下载文件 数字键5:根据公众号名称或链接,从文件读取文章列表并下载内容 输入其他任意字符退出!` fmt.Println(screenText) for { text := "" fmt.Print("请输入功能数字:") fmt.Scanln(&text) if text == "1" { articleLink := "" fmt.Print("请输入公众号下任意一篇已发布的文章链接:") fmt.Scanln(&articleLink) if articleLink == "" { fmt.Println("链接不能为空,请重新输入") continue } officialAccountLink, err := GetOfficialAccountLinkFromArticleOnly(articleLink) if err != nil { fmt.Printf("获取失败: %v\n", err) } else { fmt.Printf("\n成功获取公众号主页链接:\n%s\n", officialAccountLink) } fmt.Println("\n" + screenText) } else if text == "2" { // 使用原有的方法 fmt.Println("\n使用cookie中的信息爬取") startCrawling(cfg) break } else if text == "3" { // 通过access_token和pages获取文章链接列表 fmt.Println("\n通过access_token和pages获取文章链接列表") startGettingArticleListByAccessToken(cfg) fmt.Println("\n" + screenText) } else if text == "4" { // 根据先前生成的文章链接列表下载文件 fmt.Println("\n根据先前生成的文章链接列表下载文件") fmt.Println("功能4执行完成") fmt.Println("\n" + screenText) } else if text == "5" { // 根据公众号名称或链接,从文件读取文章列表并下载内容 fmt.Println("\n开始执行:根据公众号名称或链接,从文件读取文章列表并下载内容") crawler, err := wechat.NewWechatCrawler("", "", "", "", cfg) if err != nil { fmt.Printf("创建爬虫实例失败: %v\n", err) continue } // 获取用户输入 fmt.Print("请输入公众号名称或文章链接:") var nameLink string fmt.Scanln(&nameLink) // 设置是否保存图片和内容 imgSaveFlag := false contentSaveFlag := true fmt.Print("是否保存图片?(y/n,默认n):") var imgChoice string fmt.Scanln(&imgChoice) if imgChoice == "y" || imgChoice == "Y" { imgSaveFlag = true } // 执行功能 err = crawler.GetListArticleFromFile(nameLink, imgSaveFlag, contentSaveFlag) if err != nil { fmt.Printf("功能执行失败: %v\n", err) } else { fmt.Println("功能5执行完成") } fmt.Println("\n" + screenText) } else { break } } } // 通过access_token和pages获取文章链接列表 func startGettingArticleListByAccessToken(cfg *configs.Config) { // 提示用户输入从fiddler获取的链接 fmt.Println("【accessToken获取方式提示】") fmt.Println("1. 使用Fiddler等抓包工具,设置代理监听浏览器流量") fmt.Println("2. 打开微信公众号文章列表页面,向下滚动加载更多文章") fmt.Println("3. 在Fiddler中找到包含\"list\"和\"access_token\"参数的请求") fmt.Println("4. 复制完整的请求URL作为输入") fmt.Println("5. 文章页数为想要获取的历史文章数量,通常每10篇文章为1页") fmt.Println() accessToken := "" fmt.Print("请输入从fiddler获取的链接(包含access_token等参数):") fmt.Scanln(&accessToken) if accessToken == "" { fmt.Println("链接不能为空,请重新输入") return } // 提示用户输入文章页数 pagesStr := "" fmt.Print("请输入要获取的文章页数(0表示全部):") fmt.Scanln(&pagesStr) // 转换页数为整数 pages := 0 if pagesStr != "" { _, err := fmt.Sscanf(pagesStr, "%d", &pages) if err != nil || pages < 0 { fmt.Println("页数输入错误,将使用默认值0(全部)") pages = 0 } } // 从access_token中提取必要参数 biz, uin, key, passTicket, err := parseAccessTokenParams(accessToken) if err != nil { fmt.Printf("解析access_token失败: %v\n", err) return } // 创建爬虫实例 crawler, err := wechat.NewWechatCrawler(biz, uin, key, passTicket, cfg) if err != nil { fmt.Printf("创建爬虫实例失败: %v\n", err) return } // 获取公众号名称 nickname, err := crawler.GetOfficialAccountName() if err != nil { fmt.Printf("获取公众号名称失败: %v,将使用默认名称\n", err) nickname = "公众号_" + time.Now().Format("20060102150405") } fmt.Printf("\n正在获取公众号 '%s' 的文章列表...\n", nickname) // 创建公众号目录 officialPath := filepath.Join(cfg.RootPath, "data", nickname) err = utils.CreateDir(officialPath) if err != nil { fmt.Printf("创建目录失败: %v\n", err) return } // 获取文章列表 articleList := [][]string{} offset := 0 pageCount := 0 for { fmt.Printf("正在获取第 %d 页文章...\n", pageCount+1) result, err := crawler.GetNextList(offset) if err != nil { fmt.Printf("获取第 %d 页文章失败: %v\n", pageCount+1, err) break } mFlag, ok := result["m_flag"].(int) if !ok || mFlag == 0 { fmt.Printf("已获取全部文章,共 %d 页\n", pageCount) break } passageList, ok := result["passage_list"].([][]string) if !ok { fmt.Printf("文章列表格式错误\n") break } articleList = append(articleList, passageList...) fmt.Printf("第 %d 页获取成功,新增 %d 篇文章\n", pageCount+1, len(passageList)) pageCount++ offset += 10 // 检查是否达到指定页数 if pages > 0 && pageCount >= pages { fmt.Printf("已获取指定的 %d 页文章\n", pages) break } // 防止请求过于频繁 time.Sleep(2 * time.Second) } fmt.Printf("\n共获取到 %d 篇文章\n", len(articleList)) // 保存文章列表 err = crawler.SaveArticleListToExcel(officialPath, articleList, nickname) if err != nil { fmt.Printf("保存文章列表失败: %v\n", err) } else { fmt.Printf("文章列表已保存到: %s\n", filepath.Join(officialPath, "文章列表(article_list)_原始链接.xlsx")) } // 转换链接并保存直连链接 transformedList := transformLinks(articleList) err = crawler.SaveArticleListToExcel(officialPath, transformedList, nickname) if err != nil { fmt.Printf("保存直连链接失败: %v\n", err) } else { fmt.Printf("直连链接已保存到: %s\n", filepath.Join(officialPath, "文章列表(article_list)_直连链接.xlsx")) } } // 转换链接列表中的链接 func transformLinks(articleList [][]string) [][]string { transformedList := make([][]string, len(articleList)) for i, article := range articleList { transformedArticle := make([]string, len(article)) copy(transformedArticle, article) // 转换链接,移除amp; if len(article) > 3 { transformedArticle[3] = strings.ReplaceAll(article[3], "amp;", "") } transformedList[i] = transformedArticle } return transformedList } // 从access_token中提取必要参数 func parseAccessTokenParams(accessToken string) (string, string, string, string, error) { // 从URL中提取必要的参数 biz, err := utils.ExtractFromRegex(accessToken, "__biz=([^&]*)") if err != nil { return "", "", "", "", fmt.Errorf("未找到__biz参数") } uin, err := utils.ExtractFromRegex(accessToken, "uin=([^&]*)") if err != nil { return "", "", "", "", fmt.Errorf("未找到uin参数") } key, err := utils.ExtractFromRegex(accessToken, "key=([^&]*)") if err != nil { return "", "", "", "", fmt.Errorf("未找到key参数") } passTicket, err := utils.ExtractFromRegex(accessToken, "pass_ticket=([^&]*)") if err != nil { return "", "", "", "", fmt.Errorf("未找到pass_ticket参数") } return biz, uin, key, passTicket, nil }