原文链接

开发工具

python版本 : 3.6.4
相关模块:
pdfkit模块;
requests模块;
以及一些Python自带的模块。
抓包工具: fiddler


环境搭建

  1. python 环境
    安装Python并添加到环境变量,pip安装需要的相关模块即可。

  2. fiddler 环境
    去官网下载最新版本的安装包直接安装。
    fiddler官网


原理简介

首先,我们打开fiddler这个抓包软件,其界面如下:
带大家写一波微信公众号的爬取 - 图1
然后,我们设置一下过滤规则以过滤掉没用的数据包,因为我们只想抓取微信相关的数据包而已,而不想其他没用的数据包干扰我们的分析,就像这样:
带大家写一波微信公众号的爬取 - 图2
接着我们在电脑端登录微信,并随便找个公众号,查看它的历史文章列表。就像这样:
带大家写一波微信公众号的爬取 - 图3
不断滚动鼠标滚轮,以查看该公众号更多的历史文章数据。此时,我们可以在fiddler里看到出现了类似如下图所示的情况:
带大家写一波微信公众号的爬取 - 图4
显然,红框里的https请求应该就是获得该微信公众号发的文章相关的数据的请求了。现在,我们来分析一下这个请求。显然,该请求的链接地址构成为:
带大家写一波微信公众号的爬取 - 图5
接着看看请求头,请求头的话在这能看到:
带大家写一波微信公众号的爬取 - 图6
感觉有个user-agent就足够了:

  1. headers = {
  2. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 QBCore/4.0.1295.400 QQBrowser/9.0.2524.400 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2875.116 Safari/537.36 NetType/WIFI MicroMessenger/7.0.5 WindowsWechat'
  3. }

接着看看cookies,在这(应该直接复制到代码里就行了):
带大家写一波微信公众号的爬取 - 图7
最后,再看看发送这个请求需要携带哪些参数吧,在这:
带大家写一波微信公众号的爬取 - 图8
即:

action
__biz
f
offset
count
is_ok
scene
uin
key
pass_ticket
wxtoken
appmsg_token
x5
经过测试,我们可以发现如下参数是可以固定的:

action
f
is_ok
scene
uin
key
wxtoken
x5
其他参数的含义我们则可以根据经验和简单的测试进行判断:

1.offset
控制翻页的偏移量参数
2.count
每页的文章数量
3.biz
公众号标识, 不同的
biz对应不同的公众号
4.pass_ticket
应该是微信登录之后返回的参数吧,
去年尝试模拟登录微信网页版的时候看到返回的参数里就好像有它,
但是现在微信网页版已经被官方封了T_T。
5.appmsg_token
应该也是微信登录之后的一个标识参数吧, 而且和阅读的微信公众号
有关,查看不同的微信公众号时该值也是不同的。
前面三个可变参数都好解决,后面两个参数似乎就比较难办了。不过经过测试,我们可以发现pass_ticket其实是一个可有可无的参数,所以我们可以不管它。而appmsg_token的有效期至少有10几个小时,这段时间足够我们爬取目标公众号的所有文章了,所以直接复制过来就可以了,没必要浪费时间分析这玩意(随便想想也应该知道白嫖腾讯肯定没那么容易的T_T)。写个代码简单测试一下:

  1. import requests
  2. session = requests.Session()
  3. session.headers.update(headers)
  4. session.cookies.update(cookies)
  5. profile_url = '前面抓包得到的请求地址'
  6. biz = 'MzAwNTA5NTYxOA=='
  7. pass_ticket = ''
  8. appmsg_token = '1055_YAmuAw2QG7dM3aTwSVZVqgtRdct6ilAMTwlz7g'
  9. params = {
  10. 'action': 'getmsg',
  11. '__biz': biz,
  12. 'f': 'json',
  13. 'offset': '0',
  14. 'count': '10',
  15. 'is_ok': '1',
  16. 'scene': '123',
  17. 'uin': '777',
  18. 'key': '777',
  19. 'pass_ticket': pass_ticket,
  20. 'wxtoken': '',
  21. 'appmsg_token': appmsg_token,
  22. 'x5': '0'
  23. }
  24. res = session.get(profile_url, params=params, verify=False)
  25. print(res.text)

运行之后可以发现返回的数据如下:
带大家写一波微信公众号的爬取 - 图9
看来是没啥问题,重新调整封装一下代码,就可以爬取该公众号所有文章的链接啦。具体而言,核心代码实现如下:

  1. '''获得所有文章的链接'''
  2. def __getArticleLinks(self):
  3. print('[INFO]: 正在获取目标公众号的所有文章链接...')
  4. fp = open('links_tmp.json', 'w', encoding='utf-8')
  5. article_infos = {}
  6. params = {
  7. 'action': 'getmsg',
  8. '__biz': self.cfg.biz,
  9. 'f': 'json',
  10. 'offset': '0',
  11. 'count': '10',
  12. 'is_ok': '1',
  13. 'scene': '123',
  14. 'uin': '777',
  15. 'key': '777',
  16. 'pass_ticket': self.cfg.pass_ticket,
  17. 'wxtoken': '',
  18. 'appmsg_token': self.cfg.appmsg_token,
  19. 'x5': '0'
  20. }
  21. while True:
  22. res = self.session.get(self.profile_url, params=params, verify=False)
  23. res_json = res.json()
  24. can_msg_continue = res_json.get('can_msg_continue', '')
  25. next_offset = res_json.get('next_offset', 10)
  26. general_msg_list = json.loads(res_json.get('general_msg_list', '{}'))
  27. params.update({'offset': next_offset})
  28. for item in general_msg_list['list']:
  29. app_msg_ext_info = item.get('app_msg_ext_info', {})
  30. if not app_msg_ext_info: continue
  31. title = app_msg_ext_info.get('title', '')
  32. content_url = app_msg_ext_info.get('content_url', '')
  33. if title and content_url:
  34. article_infos[title] = content_url
  35. if app_msg_ext_info.get('is_multi', '') == 1:
  36. for article in app_msg_ext_info.get('multi_app_msg_item_list', []):
  37. title = article.get('title', '')
  38. content_url = article.get('content_url', '')
  39. if title and content_url:
  40. article_infos[title] = content_url
  41. if can_msg_continue != 1: break
  42. else: time.sleep(1+random.random())
  43. json.dump(article_infos, fp)
  44. fp.close()
  45. print('[INFO]: 已成功获取目标公众号的所有文章链接, 数量为%s...' % len(list(article_infos.keys())))

运行之后,我们就可以获得目标公众号的所有文章链接啦:
带大家写一波微信公众号的爬取 - 图10
带大家写一波微信公众号的爬取 - 图11
现在,我们只需要根据这些文章链接来爬取文章内容就行啦。这里我们借助python的第三方包pdfkit来实现将每篇文章都保存为pdf格式的文件。具体而言,核心代码实现如下:

  1. '''下载所有文章'''
  2. def __downloadArticles(self):
  3. print('[INFO]: 开始爬取目标公众号的所有文章内容...')
  4. if not os.path.exists(self.savedir):
  5. os.mkdir(self.savedir)
  6. fp = open('links_tmp.json', 'r', encoding='utf-8')
  7. article_infos = json.load(fp)
  8. for key, value in article_infos.items():
  9. print('[INFO]: 正在抓取文章 ——> %s' % key)
  10. pdfkit.from_url(value, os.path.join(self.savedir, key+'.pdf'), configuration=pdfkit.configuration(wkhtmltopdf=self.cfg.wkhtmltopdf_path))
  11. print('[INFO]: 已成功爬取目标公众号的所有文章内容...')

注意,使用pdfkit前需要先安装wkhtmltox。如下图所示:
带大家写一波微信公众号的爬取 - 图12
运行的效果大概是这样子的:
带大家写一波微信公众号的爬取 - 图13
带大家写一波微信公众号的爬取 - 图14


全部源码

根据自己的抓包结果修改cfg.py文件:

  1. ## cfg.py
  2. # 目标公众号标识
  3. biz = 'MzAwNTA5NTYxOA=='
  4. # 微信登录后的一些标识参数
  5. pass_ticket = ''
  6. appmsg_token = '1055_YAmuAw2QG7dM3aTwSVZVqgtRdct6ilAMTwlz7g~~'
  7. # 安装的wkhtmltopdf.exe文件路径
  8. wkhtmltopdf_path = r'D:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe'
  1. ## articlesSpider.py
  2. import os
  3. import time
  4. import json
  5. import pdfkit
  6. import random
  7. import requests
  8. import warnings
  9. warnings.filterwarnings('ignore')
  10. '''微信公众号文章爬取类'''
  11. class articlesSpider(object):
  12. def __init__(self, cfg, **kwargs):
  13. self.cfg = cfg
  14. self.session = requests.Session()
  15. self.__initialize()
  16. '''外部调用'''
  17. def run(self):
  18. self.__getArticleLinks()
  19. self.__downloadArticles()
  20. '''获得所有文章的链接'''
  21. def __getArticleLinks(self):
  22. print('[INFO]: 正在获取目标公众号的所有文章链接...')
  23. fp = open('links_tmp.json', 'w', encoding='utf-8')
  24. article_infos = {}
  25. params = {
  26. 'action': 'getmsg',
  27. '__biz': self.cfg.biz,
  28. 'f': 'json',
  29. 'offset': '0',
  30. 'count': '10',
  31. 'is_ok': '1',
  32. 'scene': '123',
  33. 'uin': '777',
  34. 'key': '777',
  35. 'pass_ticket': self.cfg.pass_ticket,
  36. 'wxtoken': '',
  37. 'appmsg_token': self.cfg.appmsg_token,
  38. 'x5': '0'
  39. }
  40. while True:
  41. res = self.session.get(self.profile_url, params=params, verify=False)
  42. res_json = res.json()
  43. can_msg_continue = res_json.get('can_msg_continue', '')
  44. next_offset = res_json.get('next_offset', 10)
  45. general_msg_list = json.loads(res_json.get('general_msg_list', '{}'))
  46. params.update({'offset': next_offset})
  47. for item in general_msg_list['list']:
  48. app_msg_ext_info = item.get('app_msg_ext_info', {})
  49. if not app_msg_ext_info: continue
  50. title = app_msg_ext_info.get('title', '')
  51. content_url = app_msg_ext_info.get('content_url', '')
  52. if title and content_url:
  53. article_infos[title] = content_url
  54. if app_msg_ext_info.get('is_multi', '') == 1:
  55. for article in app_msg_ext_info.get('multi_app_msg_item_list', []):
  56. title = article.get('title', '')
  57. content_url = article.get('content_url', '')
  58. if title and content_url:
  59. article_infos[title] = content_url
  60. if can_msg_continue != 1: break
  61. else: time.sleep(1+random.random())
  62. json.dump(article_infos, fp)
  63. fp.close()
  64. print('[INFO]: 已成功获取目标公众号的所有文章链接, 数量为%s...' % len(list(article_infos.keys())))
  65. '''下载所有文章'''
  66. def __downloadArticles(self):
  67. print('[INFO]: 开始爬取目标公众号的所有文章内容...')
  68. if not os.path.exists(self.savedir):
  69. os.mkdir(self.savedir)
  70. fp = open('links_tmp.json', 'r', encoding='utf-8')
  71. article_infos = json.load(fp)
  72. for key, value in article_infos.items():
  73. print('[INFO]: 正在抓取文章 ——> %s' % key)
  74. key = key.replace('\\', '').replace('/', '').replace(':', '').replace(':', '') \
  75. .replace('*', '').replace('?', '').replace('?', '').replace('“', '') \
  76. .replace('"', '').replace('<', '').replace('>', '').replace('|', '_')
  77. pdfkit.from_url(value, os.path.join(self.savedir, key+'.pdf'), configuration=pdfkit.configuration(wkhtmltopdf=self.cfg.wkhtmltopdf_path))
  78. print('[INFO]: 已成功爬取目标公众号的所有文章内容...')
  79. '''类初始化'''
  80. def __initialize(self):
  81. self.headers = {
  82. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36 QBCore/4.0.1295.400 QQBrowser/9.0.2524.400 Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2875.116 Safari/537.36 NetType/WIFI MicroMessenger/7.0.5 WindowsWechat'
  83. }
  84. self.cookies = {
  85. 'wxuin': '913366226',
  86. 'devicetype': 'iPhoneiOS13.3.1',
  87. 'version': '17000c27',
  88. 'lang': 'zh_CN',
  89. 'pass_ticket': self.cfg.pass_ticket,
  90. 'wap_sid2': 'CNK5w7MDElxvQU1fdWNuU05qNV9lb2t3cEkzNk12ZHBsNmdXX3FETlplNUVTNzVfRmwyUUtKZzN4QkxJRUZIYkMtMkZ1SDU5S0FWQmtSNk9mTTQ1Q1NDOXpUYnJQaDhFQUFBfjDX5LD0BTgNQJVO'
  91. }
  92. self.profile_url = 'https://mp.weixin.qq.com/mp/profile_ext'
  93. self.savedir = 'articles'
  94. self.session.headers.update(self.headers)
  95. self.session.cookies.update(self.cookies)
  96. '''run'''
  97. if __name__ == '__main__':
  98. import cfg
  99. spider = articlesSpider(cfg)
  100. spider.run()