爬取公众号历史推文,PDF格式保存本地。

一、需求分析

很多公众号上的原创类型推文都是作者呕心沥血创作出来的精华。例如推送的是一些生活tips、科技时讯、技术教程,又或者是推送上有你的美好回忆,每次去看的时候都要在众多公众号中寻找。
然而,有时候可能因为该公众号的停止运营或是转让,你想要的那片篇推送会被删除或者找不到了。
那么微信只支持收藏推送,怎么把它保存到本地呢?

莫慌,万物皆可爬!

二、环境准备

1.python3
2.requests
3.json
4.pdfkit
5.logging
6.手动配置wkhtmltox(window包)
7.wkhtmltox
8.fiddler

import requests
import json                              
import time
import logging
import pdfkit 

上述的环境准备也就是这些依赖库了。
特别要提醒的是pdfkit,也就是以PDF格式保存本地要用到的依赖库。windows下要手动下载和配置wkhtmltox的window包。pdfkit其中的原理是调用wkhtmltox依赖库,而wkhtmltox又是调用电脑上的wkhtmltox.exe实现的。
具体下载和配置过程这里就不废篇幅了,这里给个传送们:

https://blog.csdn.net/appleyuchi/article/details/70947138
(出处:博主Chi Yu’s Blog)

fiddler:手机抓包的一个工具,微信上的文章很多不能直接分析,所以说微信公众号算是最难爬的平台之一吧。


关于fiddler这里也不展开怎么下载安装了,比较简单。

三、流程分析

下面用流程草图来分析:
在这里插入图片描述
有了总体的操作思路就可以动手敲代码了

四、具体操作

首先手机正常的访问一下要爬取的公众号,然后在Fiddler抓取的规则分析。

在这里插入图片描述
首先点开抓取信息中js格式的内容。
在这里插入图片描述
(这是访问过程中发送的请求参数)

参考了互联网上微信开发的资料信息,分析出其中biz这个参数对应的值是该公众号的唯一标识,类似于你的微信号。offset是用于控制加载的数据量的。至于pass_ticket和appmsg_token作者也不太清楚,不过经过几次的尝试爬取公众号。发现这两个参数会在一定的时间发生变化(一般几个小时),也许这就是微信公众号的一种反爬手段。

我们再来分析js列表的东西:
在这里插入图片描述
可以看到里面有中文,好像是某些文章的标题。
其中的next_offset很熟悉,经过分析后发现就是下一次请求是要发送的offset参数的值!
can_msg_continue也跟发送请求的 is_ok参数所对应。
我们再把它展开,看其中的细节:
在这里插入图片描述
作者对比了抓取的几个js信息中得出了一下结论:
返回的list中就包含了很多我们想要的信息,像title就是一篇文章的标题、digest顾名思义就是摘要啦、比较有意思的是里面的 copyright_stat 对应的值就是区分是否为原创文章的标识。

除此之外,我们再来分析一下请求的headercookies
在这里插入图片描述
Header基本可以全复制到代码中定义了,其中cookies中的pass_ticket就是上述说到的会在一定时间发生变化的参数。
下面可以在代码中定义了:
先在手机上复制历史推文的链接,也就是右上角的三颗点:

url = 'https://mp.weixin.qq.com/mp/profile_ext' 

定义header和cookies

headers = {
        'Accept': '*/*',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'Accept-Language: zh-CN,en-US;q=0.9',
        'User-Agent': 'User-Agent: Mozilla/5.0 (Linux; Android 9; LLD-AL00 Build/HONORLLD-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/67.0.3396.87 XWEB/880 MMWEBSDK/190102 Mobile Safari/537.36 MMWEBID/6775 MicroMessenger/7.0.3.1400(0x2700033C) Process/toolsmp NetType/4G Language/zh_CN',
        'X-Requested-With': 'XMLHttpRequest'
}

cookies = {
    'devicetype': 'android-28',
    'lang':'zh_CN',
    'pass_ticket': 'rmiwGYhz6h2OtRZeeEsl675W1Gtt4pn21m4TU5Lq+celnObNLbOyxna6UdHLOvan',
    'version':'2700033c',
    'wap_sid2': 'CO+a8aAEElxldEt6MTRiWjVPWWowRWgzRV9EeHFSeGJqVXlYcThzUHdqb3RvY1AwdFk2NklaQ29udVpQX0QzLTZLNlFJdXVxWklYMU9Qd09CaHRUdE1pMFVrNTN2dm9EQUFBfjCoysXpBTgNQJVO',
    'wxuin': '1142705519',
    'wxtokenkey':'777',
    'sd_userid':'18411554876722376',
    'sd_cookie_crttime': '1554876722376'

}

代码中的cookies跟图片分析的不一样,因为里面的一些参数是会变化的。作者也尝试了别的公众号。

然后定义一个函数,控制offset

def control_offset(offset):

    params = {
        'action': 'getmsg',
        '__biz': 'MzU1NTUwMjY3Mg==',
        'f': 'json',
        'offset': '{}'.format(offset),              #控制每次加载的数量
        'count':'10',
        'is_ok': '1',
        'scene': '126',
        'uin': '777',
        'key': '777',
        'pass_ticket': 'rmiwGYhz6h2OtRZeeEsl675W1Gtt4pn21m4TU5Lq+celnObNLbOyxna6UdHLOvan',
        'wxtoken': '',
        'appmsg_token': '1018_0SVv2SIUtk9El9dQRrS2cTrHNDw4-AIhd64BSQ~~',
        'x5': '0',
        'f': 'json'
    }
    return params

请求的参数定义好了,然后就可以用request访问了:

def get_data(offset):
    res = requests.get(url,headers = headers,cookies = cookies,params= control_offset(offset),verify = False)
    # verify就是关闭网页证书验证的
    data = json.loads(res.text)                #读取js
    can_msg_continue = data['can_msg_continue']   # js里面的一些参数  类似标签
    next_offset = data['next_offset']
    general_msg_list = data['general_msg_list']
    list_data = json.loads(general_msg_list)['list']

把访问的js信息通过json读取,并且挖出我们想要的参数信息。
然后通过上面挖出的参数试着将我们要的公众号文章标题和链接打印出来:

    for data in list_data:
        try:
            if data['app_msg_ext_info']['copyright_stat'] ==11:   
                msg_info = data['app_msg_ext_info']
                title = msg_info['title']
                content_url = msg_info['content_url']
                print("获取原创文章:%s :%s"%(title,content_url))
        except:
           	    print("非原创文章不爬取")

在这里插入图片描述
这个就是成功爬取文章标题和链接的效果图。
为了完整的爬取所有文章,但是又不想用循环,那么就可以利用上面说到的next_offset 来递归调用函数啦

  if can_msg_continue == 1:          # 控制递归
        time.sleep(2)
        get_data(next_offset)

经过上述步骤,已经完成了流程图中的第四步了。
接下来就是将爬到的信息转为PDF存在本地了

 pdfkit.from_url(content_url,'D:/spider/'+title+'.pdf')  

就是这么一行代码,这里的路径最好设置绝对路径,因为相对路径可能会找不到确切的路径而报错。

上面提到的logging似乎没有用到

import logging

其实没用到这个依赖库的时候,报了这样一个错
requests.exceptions.SSLError: HTTPSConnectionPool
这个错误是啥意思呢?

简单的解释就是,有时候我们用浏览器访问一些网站会弹出一个警告“您的链接不是私密链接”。
是因为这个网站的证书没有得到官方的CA机构认证。就是一种证书错误。典型的网址就有“12306.com”

那么python访问的网页为什么会有这样的错误呢?
其实是因为开了fiddler抓包工具,其中的代理证书就是没有得到CA机构认证的。
可以关闭fiddler抓包工具来解决问题,但是要是修改代码的话,又得重新抓取一次数据。所以可以通过以下代码来关闭代理

logging.captureWarnings(True)
 res = requests.get(url,headers = headers,cookies = cookies,params= control_offset(offset),verify = False)

参数verify = False 就是关闭代理的意思

下面是完整的代码:

import requests
import json                              # 类似BeautifulSoup,加载js的
import time
import logging
import pdfkit                             # url转pdf,不能直接跑,要配置wkhtmltox windows包

url = 'https://mp.weixin.qq.com/mp/profile_ext'      # 手机进入公众号历史消息复制的链接
logging.captureWarnings(True)                  
headers = {
        'Accept': '*/*',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'Accept-Language: zh-CN,en-US;q=0.9',
        'User-Agent': 'User-Agent: Mozilla/5.0 (Linux; Android 9; LLD-AL00 Build/HONORLLD-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/67.0.3396.87 XWEB/880 MMWEBSDK/190102 Mobile Safari/537.36 MMWEBID/6775 MicroMessenger/7.0.3.1400(0x2700033C) Process/toolsmp NetType/4G Language/zh_CN',
        'X-Requested-With': 'XMLHttpRequest'
}

cookies = {
    'devicetype': 'android-28',
    'lang':'zh_CN',
    'pass_ticket': 'rmiwGYhz6h2OtRZeeEsl675W1Gtt4pn21m4TU5Lq+celnObNLbOyxna6UdHLOvan',
    'version':'2700033c',
    'wap_sid2': 'CO+a8aAEElxldEt6MTRiWjVPWWowRWgzRV9EeHFSeGJqVXlYcThzUHdqb3RvY1AwdFk2NklaQ29udVpQX0QzLTZLNlFJdXVxWklYMU9Qd09CaHRUdE1pMFVrNTN2dm9EQUFBfjCoysXpBTgNQJVO',
    'wxuin': '1142705519',
    'wxtokenkey':'777',
    'sd_userid':'18411554876722376',
    'sd_cookie_crttime': '1554876722376'

}

def control_offset(offset):

    params = {
        'action': 'getmsg',
        '__biz': 'MzU1NTUwMjY3Mg==',
        'f': 'json',
        'offset': '{}'.format(offset),              #控制每次加载的数量
        'count':'10',
        'is_ok': '1',
        'scene': '126',
        'uin': '777',
        'key': '777',
        'pass_ticket': 'rmiwGYhz6h2OtRZeeEsl675W1Gtt4pn21m4TU5Lq+celnObNLbOyxna6UdHLOvan',
        'wxtoken': '',
        'appmsg_token': '1018_0SVv2SIUtk9El9dQRrS2cTrHNDw4-AIhd64BSQ~~',
        'x5': '0',
        'f': 'json'
    }
   
    return params

def get_data(offset):
    res = requests.get(url,headers = headers,cookies = cookies,params= control_offset(offset),verify = False)
    # verify就是关闭网页证书验证的
    data = json.loads(res.text)                #读取js
    can_msg_continue = data['can_msg_continue']   # js里面的一些参数  类似标签
    next_offset = data['next_offset']
    general_msg_list = data['general_msg_list']
    list_data = json.loads(general_msg_list)['list']

    for data in list_data:
        try:
            if data['app_msg_ext_info']['copyright_stat'] ==11:   
                msg_info = data['app_msg_ext_info']
                title = msg_info['title']
                content_url = msg_info['content_url']
                print("获取原创文章:%s :%s"%(title,content_url))
                pdfkit.from_url(content_url,'D:/spider/'+title+'.pdf')     #转pdf

        except:
            print("非原创文章不爬取")
    if can_msg_continue == 1:          # 控制递归
        time.sleep(2)
        get_data(next_offset)

get_data(1)    

这里的代码学习了很多博主的思想,或许因为其中的方法很多,遇到的坑也很多。不过也都一一解决了。
这份代码还有一个坑就是保存的PDF数量跟不上程序运行的速度,PDF里面一些图片显示不出来。
作者分析了一下,解决的方案有:给程序加延时,不过这也影响了爬取的速率,如果读者有更好的方法可以评论区一起探讨学习。
至于PDF图片看不到的原因是因为涉及到HTML中的懒加载和直接加载的知识。这个坑还没填,下次再来完善。有建议或者想法的可以在评论区一起交流。

篇幅很长,感谢细读!

代码文件链接:https://github.com/yikongchang/-PDF-

发布了15 篇原创文章 · 获赞 16 · 访问量 5761

猜你喜欢

转载自blog.csdn.net/weixin_42768004/article/details/96496263