1. 程式人生 > >【Python爬蟲】微信公眾號歷史文章和文章評論API分析

【Python爬蟲】微信公眾號歷史文章和文章評論API分析

上一篇文章爬取微信公眾號文章資訊準備工作介紹了微信公眾號歷史文章和文章評論API的組成情況,歷史文章API格式:https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=MjM5NjAxOTU4MA==&f=json&offset=10&count=10&is_ok=1&scene=126&uin=777&key=777&pass_ticket=tsN5weBAV13S7TjerqBeu0m84CMPMmPz4P7lb8bvDk90y1LP%2F1j46CUzFqDsMuRj&wxtoken=&appmsg_token=986_Zxzm8ptDJ39%252BC1UbkzPrFKd_laYeOCk5cVFX9A~~&x5=1&f=json
文章評論API格式:

https://mp.weixin.qq.com/mp/appmsg_comment?action=getcomment&scene=0&__biz=MjM5NjAxOTU4MA==&appmsgid=3009217642&idx=2&comment_id=578089232589930496&offset=0&limit=100&uin=777&key=777&pass_ticket=v+7PaoESYfMrxgXJpqOkfXV4Y2+gYNPPJfSSmzPXfeiuNrNiBeEcs+8b//Yit5sd&wxtoken=777&devicetype=android-26&clientversion=2607033b&appmsg_token=986_jbuKqpV9lCZ1cb787Tem5V5n6JKpU9TrOFUZRE5esVxnBK7IR-TsZiXLRNaO1tnfx4rkIk1xyFHRlqI7&x5=1&f=json
這個兩個API有些共同的引數:__biz,pass_ticket,公共引數可以通過抓包獲取。
也有各自獨有的引數:歷史文章API中offset是一直變化的,appmsg_token也會隨著時間失效,抓包可以獲取appmsg_token,而offset是以0開始,可以通過API返回看到下一個offset是介面返回的欄位“next_offset”值。


文章評論API中的appmsgid是具體文章的圖文訊息ID,comment_id也與具體文章相關,appmsg_token每篇文章也不同。通過文章連結獲取原始碼我們可以檢視到文章評論API的三個引數comment_id,appmsgid,appmsg_token,如下圖:

歷史文章API返回的json資訊:

下面是通過格式化後並刪除一些不需要資料後的資訊,json格式

 

文章評論API返回的json資訊:

base_resp是返回狀態情況,elected_comment才是評論的資訊

elected_comment下面的詳細資訊,當評論有回覆時,reply_list有資訊

本文使用python3.6,pymysql連線mysql資料庫,具體程式碼如下:

# -!- coding: utf-8 -!-
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
#作者:cacho_37967865
#部落格:https://blog.csdn.net/sinat_37967865
#檔案:wechatArticleList.py
#日期:2018-12-08
#備註:通過Fiddler抓包,獲取微信公眾號歷史文章資訊和文章評論資訊儲存到mysql資料庫表   
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

import requests
import json
import pymysql
from datetime import datetime
import re


class wechatArticle:

    def __init__(self,_biz,_pass_ticket,_appmsg_token,_cookie,_offset=0):
        self.offset = _offset       # 不同公眾號不一樣
        self.biz = _biz
        self.pass_ticket = _pass_ticket
        self.appmsg_token = _appmsg_token
        self.headers = {
            'cookie':_cookie,
            'User-Agent':'Mozilla/5.0 (Linux; Android 8.0; FRD-AL00 Build/HUAWEIFRD-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132'
        }
        self.db = pymysql.connect(
            host="localhost",
            user="root",
            password="123456",
            port=3306,
            use_unicode=True,
            #charset="utf8",
            database="sunshine")
        self.cursor = self.db.cursor()


    def get_article_list(self):
        offset = self.offset
        while True:
            api = 'https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz={0}&f=json&offset={1}&count=10&is_ok=1&scene=126&uin=777&key=777&pass_ticket={2}&wxtoken=&appmsg_token={3}&x5=1&f=json'.format(self.biz, offset, self.pass_ticket, self.appmsg_token)
            resp = requests.get(api, headers=self.headers).json()
            print(type(resp), resp)  # 字典型別
            ret, status = resp.get('ret'), resp.get('errmsg')     # 狀態資訊
            if ret == 0 or status == 'ok':
                offset = resp['next_offset']
                general_msg_list = resp['general_msg_list']
                #print(type(general_msg_list),general_msg_list)    # json型別
                msg_list = json.loads(general_msg_list)['list']    # 先轉化為字典型別再獲取列表型別
                for msg in msg_list:
                    comm_msg_info = msg['comm_msg_info']           # 字典型別,每次推送的訊息(一次三篇)
                    msg_id = comm_msg_info['id']                   # 推送訊息的id
                    post_time = datetime.fromtimestamp(comm_msg_info['datetime'])     # 釋出時間
                    try:
                        app_msg_ext_info = msg['app_msg_ext_info']  # 字典型別,文章資訊(一次三篇)
                        first_article_id = app_msg_ext_info['fileid']
                        first_article_title = app_msg_ext_info['title']  # 本次推送的首條文章標題
                        first_article_digest = app_msg_ext_info['digest']  # 本次推送的首條文章摘要
                        first_article_url = app_msg_ext_info['content_url']
                        self.get_article_detail(first_article_id,first_article_url)
                        first_url = first_article_url.replace('amp;', '').split('&chksm')[0]
                        self.article_to_mysql(msg_id, first_article_id, first_article_title, first_article_digest,first_url, post_time)
                        multi_app_msg_item_list = app_msg_ext_info.get('multi_app_msg_item_list')
                        for article in multi_app_msg_item_list:
                            article_id = article['fileid']
                            multi_article_title = article['title']
                            multi_article_digest = article['digest']
                            multi_article_url = article['content_url']
                            self.get_article_detail(article_id,multi_article_url)
                            multi_url = multi_article_url.replace('amp;', '').split('&chksm')[0]
                            self.article_to_mysql(msg_id, article_id, multi_article_title, multi_article_digest,multi_url, post_time)

                    except Exception as f:
                        print(str(f))


    def get_article_detail(self,article_id,content_url):
        try:
            url = content_url.replace('amp;', '').replace('#wechat_redirect', '').replace('http', 'https')
            html = requests.get(url, headers=self.headers).text
            #print(html)
        except:
            print('獲取評論失敗' + content_url)
        else:
            str_comment = re.search(r'var comment_id = "(.*)" \|\| "(.*)" \* 1;', html)
            str_msg = re.search(r"var appmsgid = '' \|\| '(.*)'\|\|", html)   # 文章的id
            str_token = re.search(r'window.appmsg_token = "(.*)";', html)

            if str_comment and str_msg and str_token:
                comment_id = str_comment.group(1)  # 評論id(固定)
                app_msg_id = str_msg.group(1)      # 票據id(非固定)
                appmsg_token = str_token.group(1)  # 票據token(非固定)

                # 缺一不可
                if comment_id and app_msg_id and appmsg_token:
                    print("爬取評論的連結:" + url,html)
                    self.get_article_comments(app_msg_id,comment_id,appmsg_token,article_id)


    def get_article_comments(self,app_msg_id,comment_id,appmsg_token,article_id):
        api = 'https://mp.weixin.qq.com/mp/appmsg_comment?action=getcomment&scene=0&__biz={0}&appmsgid={1}&idx=2&comment_id={2}&offset=0&limit=100&uin=777&key=777&pass_ticket={3}&wxtoken=777&devicetype=android-26&clientversion=2607033b&appmsg_token={4}&x5=1&f=json'.format(
            self.biz, app_msg_id, comment_id, self.pass_ticket, appmsg_token)
        resp = requests.get(api, headers=self.headers).json()
        ret, status = resp['base_resp']['ret'], resp['base_resp']['errmsg']
        if ret =='0' or status == 'ok':
            elected_comment = resp['elected_comment']
            for comment in elected_comment:
                content_id = comment.get('content_id') # 評論ID
                nick_name = comment.get('nick_name')  # 評論人暱稱
                like_num = comment.get('like_num')     # 點贊
                comment_time = datetime.fromtimestamp(comment.get('create_time'))  # 評論時間
                content = comment.get('content')       # 評論內容
                #print("評論內容文章:",article_id,nick_name)
                self.comment_to_mysql(article_id,content_id,comment_time,nick_name,like_num,content)


    def create_article_table(self):
        sql1 = 'drop table if exists mnyd_article;'
        sql2 = 'create table mnyd_article(No INT(11) NOT NULL AUTO_INCREMENT,msg_id VARCHAR(15),article_id VARCHAR(15),post_time timestamp(2),title VARCHAR(200),digest VARCHAR(200),article_url varchar(300),PRIMARY KEY (No));'
        self.cursor.execute(sql1)
        self.cursor.execute(sql2)
        self.db.commit()

    def article_to_mysql(self,msg_id, article_id,title,digest,article_url,post_time):
        sql = "insert into mnyd_article(msg_id,article_id,title,digest,article_url,post_time) values('%s','%s','%s','%s','%s','%s')" % (msg_id,article_id,title, digest,article_url,post_time)
        try:
            # 使用 cursor() 方法建立一個遊標物件 cursor
            self.cursor.execute(sql)
        except Exception as e:
            # 發生錯誤時回滾
            self.db.rollback()
            print(str(e))
        else:
            self.db.commit()  # 事務提交
            print('事務處理成功')



    def create_comment_table(self):
        sql1 = 'drop table if exists mnyd_comment;'
        sql2 = "create table mnyd_comment(No INT(11) NOT NULL AUTO_INCREMENT,article_id VARCHAR(15),content_id VARCHAR(20),comment_time timestamp(2),nick_name VARCHAR(50),like_num int,content varchar(1000),PRIMARY KEY (No)) COLLATE='utf8mb4_unicode_ci';"
        self.cursor.execute(sql1)
        self.cursor.execute(sql2)
        self.db.commit()

    def comment_to_mysql(self,article_id,content_id,comment_time,nick_name,like_num,content):
        sql = "insert into mnyd_comment(article_id,content_id,comment_time,nick_name,like_num,content) values('%s','%s','%s','%s','%i','%s')" % (article_id,content_id,comment_time, nick_name,like_num,content)
        try:
            # 使用 cursor() 方法建立一個遊標物件 cursor
            self.cursor.execute(sql)
        except Exception as e:
            # 發生錯誤時回滾
            self.db.rollback()
            print(str(e))
        else:
            self.db.commit()  # 事務提交
            print('事務處理成功')



if __name__ == '__main__':
    biz = 'MzIwNTc4NTEwOQ=='  # "碼農有道公眾號"   mnyd_article  mnyd_comment
    pass_ticket = 'ZS3nqLX1df5GhZ+zf/t0FYyf7Nfp52yUJ+PuyJUKvQtyln78R3QzBU21Xo528IE+'
    app_msg_token = '986_G0Sy%252FL2pNlAGA9PIXcqTRipxsKaGLurexidEyg~~'     # 歷史文章
    wap_sid2 = 'CL3qgfIFElxMOFBzZ2dZOHQ1WTcxamRQLXUyMGFiU0tvNkZzUEJmRURhZmtJTkhLcEtYWU9rNm5WYmUtd29qd3Q3UmVqbmpZXzFxS21GMG13amVjM1NEaUVPajZNZG9EQUFBfjDH8K3gBTgNQAE='
    cookie = 'wxuin=1581282621; version=2607033b; pass_ticket={}; wap_sid2={}'.format(pass_ticket, wap_sid2)
    # 以上資訊不同公眾號每次抓取都需要藉助抓包工具做修改
    wxarticles = wechatArticle(biz, pass_ticket, app_msg_token, cookie)
    wxarticles.create_article_table()         # 建立資料庫表記錄文章
    wxarticles.create_comment_table()         # 建立資料庫表記錄評論
    wxarticles.get_article_list()              # 開始爬取文章和評論

介紹一下上面的幾個函式:
create_comment_table():建立儲存評論的表,其中必須設定COLLATE='utf8mb4_unicode_ci',是為了確保能夠儲存特殊格式(mb4就是most bytes 4的意思,專門用來相容四位元組的unicode。)的微信暱稱到資料庫。
get_article_list():獲取歷史文章的資訊,存入到資料庫,並且將文章id和文章連結傳入到get_article_detail()函式
get_article_detail():根據get_article_list()函式傳入的引數獲取文章評論API的引數
get_article_comments():根據get_article_detail()函式傳入的引數獲取文章評論並存入到資料庫

此外注意以下幾個要點:
def __init__(self,_biz,_pass_ticket,_appmsg_token,_cookie,_offset=0) 初始帶cookie的引數資訊,_offset=0對引數初始化
歷史文章和文章評論API 可以通過str.format()設定引數
歷史文章返回中有欄位'app_msg_ext_info',在2017年5月前的文章是沒有的,所以使用try.. except..

這個時候我們已經獲取到了需要的資訊,後續就是對資訊進行處理並轉化為自己的東西。