阅读本篇大概需要 11 分钟。
接上篇文章使用Python快速获取公众号文章定制电子书(一)。我们现在已经成功的将公众号历史消息的前十条文章给爬取了出来,使用 content_url 这个关键字段,我们便可以轻易的获取文章具体内容,并将文章保存到本地文件中。实际上上面这些东西已经是我们实现爬取公号文章的核心功能了,剩下的就是如何通过某种方式将公众号的所有文章一次性爬取出来。
我们用手机在公众号的历史消息界面中作上拉加载操作,在 Charles 中爬取接口,像之前一样,我们通过 response 找到了我们需要的接口,这次的接口返回很漂亮,是一个 JSON 数据。
看返回,我们发现 general_msg_list 字段类似于上篇文章中的 msgList 的内容,里面是十条文章的数据列表,具体的处理方式和之前也大差不差,而且这里本身就是 JSON 返回,处理起来显然更容易。
我们来看看这个请求接口的 URL 形式:
url ="https://..." \
"action=getmsg&" \
"__biz=MjM5ODIyMTE0MA==&" \
"f=json&" \
"offset=11&" \
"count=10&" \
"is_ok=1&" \
"scene=124&" \
"uin=777&" \
"key=777&" \
"pass_ticket=Pu%2FH3aPR7f%2FzgA52T%2Bv4fU9wSWkY5tgGGgAXWewji2dqpMDrjaxUbBR%2Fmo2e%2FaMX&" \
"wxtoken=&" \
"appmsg_token=956_cnSiifKearMa6Um6cS3fcmXnu1AfKSYN5dSOSA~~&" \
"x5=1&" \
"f=json"
offset 这个字段我们划重点,它代表的就是从我们这次上拉加载之后第一个要获取的文章的偏离值,结合我们刚刚拿到的 response 中字段 can_msg_continue 和 next_offset,我们的思路就有了,当 can_msg_continue 为动态的改变 url 的 offset 参数,将每次接口返回的 next_offset 字段作为下次请求的 offset 参数,不断递归调用,这样理论上就可以一次性爬取完该公众号的所有文章。下面是核心逻辑的具体源码。
def crawl_msg(offset=0):
"""
爬取更多文章
"""
url = "https://..." \
"action=getmsg&" \
"__biz=MjM5ODIyMTE0MA==&" \
"f=json&" \
"offset={offset}&" \
"count=10&" \
"is_ok=1&" \
"scene=124&" \
"uin=777&" \
"key=777&" \
"pass_ticket=Pu%2FH3aPR7f%2FzgA52T%2Bv4fU9wSWkY5tgGGgAXWewji2dqpMDrjaxUbBR%2Fmo2e%2FaMX&" \
"wxtoken=&" \
"appmsg_token=956_cnSiifKearMa6Um6cS3fcmXnu1AfKSYN5dSOSA~~&" \
"x5=1&" \
"f=json".format(offset=offset)
# url 反转义
url = html.unescape(url)
headers = """
Host: ...
Connection: keep-alive
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; NX531J Build/MMB29M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/6.2 TBS/044030 Mobile Safari/537.36 MicroMessenger/6.6.5.1280(0x26060532) NetType/WIFI Language/zh_CN
Accept: */*
Referer: ...action=home&__biz=MjM5ODIyMTE0MA==&scene=124&devicetype=android-23&version=26060532&lang=zh_CN&nettype=WIFI&a8scene=3&pass_ticket=Pu%2FH3aPR7f%2FzgA52T%2Bv4fU9wSWkY5tgGGgAXWewji2dqpMDrjaxUbBR%2Fmo2e%2FaMX&wx_header=1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,en-US;q=0.8
Cookie: sd_userid=96301522585723838; sd_cookie_crttime=1522585723838; pgv_pvid=1151171120; tvfe_boss_uuid=92a513a6354c3896; rewardsn=; wxtokenkey=777; wxuin=1336178621; devicetype=android-23; version=26060532; lang=zh_CN; pass_ticket=Pu/H3aPR7f/zgA52T+v4fU9wSWkY5tgGGgAXWewji2dqpMDrjaxUbBR/mo2e/aMX; wap_sid2=CL3vkf0EEnBSWHFYWmVoZVpOMjU0cnBpSUhiLWF2cmZHVVVLVWZrWUp4QVRlclVVOTRwS1hmMGNUR0VJaXp1RlVzbGViM2wtZnVfakZVd21RcGxxbzI3U3R3cmtYYlUycXpLU0FzcGJFSm1ESkZsYVhzSzhBd0FBMLbQ5dcFOA1AlU4=
Q-UA2: QV=3&PL=ADR&PR=WX&PP=com.tencent.mm&PPVN=6.6.5&TBSVC=43603&CO=BK&COVC=044030&PB=GE&VE=GA&DE=PHONE&CHID=0&LCID=9422&MO= NX531J &RL=1080*1920&OS=6.0.1&API=23
Q-GUID: 6a875f18ea5ba76bb6afb9ca13b788cb
Q-Auth: 31045b957cf33acf31e40be2f3e71c5217597676a9729f1b
"""
headers = headers_to_dict(headers)
response = requests.get(url, headers=headers, verify=False)
if '<title>验证</title>' in response.text:
raise Exception("获取微信公众号文章失败,可能是因为你的请求参数有误,请重新获取")
result = response.json()
if result.get("ret") == 0:
msg_list = result.get("general_msg_list")
data = json.loads(msg_list)
articles = data.get("list")
articles_lists = dict()
for item in articles:
if item.get("app_msg_ext_info"):
articles_lists[item["app_msg_ext_info"]["title"]] = item["app_msg_ext_info"]["content_url"]
rex = r'\\/'
for item in articles_lists:
pattern = re.sub(rex, '/', html.unescape(articles_lists.get(item)))
response = requests.get(pattern, headers=headers, verify=False)
parser_text_to_file(item, response.text)
print("抓取数据: offset=%s, data=%s" % (offset, msg_list))
has_next = result.get("can_msg_continue")
if has_next == 1:
next_offset = result.get("next_offset")
time.sleep(2) # 每两秒爬取一次
crawl_msg(next_offset)
else:
print("无法正确获取内容,请重新从Fiddler/Charles获取请求参数和请求头")
exit()
代码比较细,笔者讲挑重点解析下。首先我们通过 format() 来动态的更换 url 的offset 参数,从而在递归过程中不断变更 offset,最下面的 crawl_msg(next_offset) 就是将该次请求返回的 next_offset 作为 offset 参数继续调用。
代码中有两块验证逻辑,公众号的相关接口在爬取过程中,cookie 会有过期的现象,有可能是因为时间过期,也有可能是因为使用次数达到过期上限。这个时候,粗暴的解决方式就是重新再进入一次历史消息界面,再把最新的 Header替换到代码上。当然,网上也有一些方式可以完全的全自动解决cookie过期问题,这个优化项,大家可以自行研究,我这里的源码就没有涉及到了。
值得注意的是,上段代码中,有很多的url处理操作,比如通过 html.unescape() 来进行 url 反转义,或者使用正则表达式来处理 content_url 中的一些干扰字符。我们在做爬取的过程中,数据和解析上可能有很多奇奇怪怪的问题,这个时候应该有耐心,通过慢慢的尝试,去接近最终的解决方法。
结合我们上一节的代码,我们先获取前十条,再从第十一条开始获取后面的上拉加载更多数据,下面是我爬取「小道消息」公众号的结果。大家可以参考下。
当然了,笔者的 demo 很粗陋,很简单,很多细节也没考虑到,但核心思想和方式是有的。如果各位看官能通过我的文章,有所启发,进行个性化的拓展,这对笔者来说就是最大的激励。
笔者认为,每个人的需求是不同的,但遇到需求,能够通过某种思路,举一反三,用自己的方式来达成目的,并在达成目的的过程中,解决问题。
这是我们技术人应该要做到,也是我在这两篇文章中,想表达的东西。
参考资料
掘金小册:基于Python实现微信公众号爬虫--刘志军