使用Python爬取公號文章(上)

閱讀文本大概須要 10 分鐘。數據庫


01 抓取目標

場景:有時候咱們想爬取某個大 V 的發佈的所有的文章進行學習或者分析。json

這個爬蟲任務咱們須要藉助「 Charles 」這個抓包工具,設置好手機代理 IP 去請求某個頁面,經過分析,模擬請求,獲取到實際的數據。bash

咱們要爬取文章的做者、文章標題、封面圖、推送時間、文件內容、閱讀量、點贊數、評論數、文章實際連接等數據,最後要把數據存儲到「 MongoDB 」數據庫中。微信


02 準備工做

首先,在 PC 上下載 Charles,並獲取本地的 IP 地址。cookie


而後,手機連上同一個網段,並手動設置代理 IP,端口號默認填 8888 。最後配置 PC 和手機上的證書及 SSL Proxying,保證能順利地抓到 HTTPS 的請求。具體的方法能夠參考下面的文章。session


「https://www.jianshu.com/p/595e8b556a60?from=timeline&isappinstalled=0 」app



03 爬取思路

首先咱們選中一個微信公衆號,依次點擊右上角的頭像、歷史消息,就能夠進入到所有消息的主界面。默認展現的是前 10 天曆史消息。框架

而後能夠查看 Charles 抓取的請求數據,能夠經過「 mp.weixin.qq.com 」去過濾請求,獲取到消息首頁發送的請求及請求方式及響應內容。ide

繼續往下滾動頁面,能夠加載到下一頁的數據,一樣能夠獲取到請求和響應的數據。工具


爬取的數據最後要保存在 MongoDB 文檔型數據庫中,因此不須要創建數據模型,只須要安裝軟件和開啓服務就能夠了。MongoDB 的使用教程能夠參考下面的連接:


「 https://www.jianshu.com/p/4c5deb1b7e7c 」



爲了操做 MongoDB 數據庫,這裏使用「 MongoEngine 」這個相似於關係型數據庫中的 ORM 框架來方便咱們處理數據。

pip3 install mongoengine
複製代碼


04 代碼實現

從上面的分析中能夠知道首頁消息、更多頁面消息的請求 URL 規律以下:

# 因爲微信屏蔽的關鍵字, 字段 netloc + path 用 ** 代替
# 首頁請求url
https://**?action=home&__biz=MzIxNzYxMTU0OQ==&scene=126&bizpsid=0&sessionid=1545633855&subscene=0&devicetype=iOS12.1.2&version=17000027&lang=zh_CN&nettype=WIFI&a8scene=0&fontScale=100&pass_ticket=U30O32QRMK6dba2iJ3ls6A3PRbrhksX%2B7D8pF3%2Bu3uXSKvSAa1hnHzfsSClawjKg&wx_header=1

# 第二頁請求url
https://**?action=getmsg&__biz=MzIxNzYxMTU0OQ==&f=json&offset=10&count=10&is_ok=1&scene=126&uin=777&key=777&pass_ticket=U30O32QRMK6dba2iJ3ls6A3PRbrhksX%2B7D8pF3%2Bu3uXSKvSAa1hnHzfsSClawjKg&wxtoken=&appmsg_token=988_rETfljlGIZqE%252F6MobN1rEtqBx5Ai9wBDbbH_sw~~&x5=0&f=json

# 第三頁請求url
https://**?action=getmsg&__biz=MzIxNzYxMTU0OQ==&f=json&offset=21&count=10&is_ok=1&scene=126&uin=777&key=777&pass_ticket=U30O32QRMK6dba2iJ3ls6A3PRbrhksX%2B7D8pF3%2Bu3uXSKvSAa1hnHzfsSClawjKg&wxtoken=&appmsg_token=988_rETfljlGIZqE%252F6MobN1rEtqBx5Ai9wBDbbH_sw~~&x5=0&f=json
複製代碼

能夠經過把 offset 設置爲可變數據,請求全部頁面的數據 URL能夠寫成下面的方式:

https://**?action=getmsg&__biz=MzIxNzYxMTU0OQ==&f=json&offset={}&count=10&is_ok=1&scene=126&uin=777&key=777&pass_ticket=U30O32QRMK6dba2iJ3ls6A3PRbrhksX%2B7D8pF3%2Bu3uXSKvSAa1hnHzfsSClawjKg&wxtoken=&appmsg_token=988_rETfljlGIZqE%252F6MobN1rEtqBx5Ai9wBDbbH_sw~~&x5=0&f=json
複製代碼

另外,經過 Charles 獲取到請求頭。因爲微信的反爬機制,這裏的 Cookie 和 Referer 有必定的時效性,須要定時更換。

self.headers = {
            'Host': 'mp.weixin.qq.com',
            'Connection': 'keep-alive',
            'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16B92 MicroMessenger/6.7.4(0x1607042c) NetType/WIFI Language/zh_CN',
            'Accept-Language': 'zh-cn',
            'X-Requested-With': 'XMLHttpRequest',
            'Cookie': 'devicetype=iOS12.1; lang=zh_CN; pass_ticket=fXbGiNdtFY050x9wsyhMnmaSyaGbSIXNzubjPBqiD+c8P/2GyKpUSimrtIKQJsQt; version=16070430; wap_sid2=CMOw8aYBElx2TWQtOGJfNkp3dmZHb3dyRnpRajZsVlVGX0pQem4ycWZSNzNFRmY3Vk9zaXZUM0Y5b0ZpbThVeWgzWER6Z0RBbmxqVGFiQ01ndFJyN01LNU9PREs3OXNEQUFBfjC409ngBTgNQJVO; wxuin=349984835; wxtokenkey=777; rewardsn=; pac_uid=0_f82bd5abff9aa; pgv_pvid=2237276040; tvfe_boss_uuid=05faefd1e90836f4',
            'Accept': '*/*',
            'Referer': 'https://**?action=home&__biz=MzIxNzYxMTU0OQ==&scene=126&sessionid=1544890100&subscene=0&devicetype=iOS12.1&version=16070430&lang=zh_CN&nettype=WIFI&a8scene=0&fontScale=100&pass_ticket=pg%2B0C5hdqENXGO6Fq1rED9Ypx20C2vuodaL8DCwZwVe22sv9OtWgeL5YLjUujPOR&wx_header=1'
        }
複製代碼

最後經過 requests 去模擬發送請求。

response = requests.get(current_request_url, headers=self.headers, verify=False)
result = response.json()
複製代碼

經過 Charles 返回的數據格式能夠得知消息列表的數據存儲在 general_msg_list 這個 Key 下面。所以能夠須要拿到數據後進行解析操做。


has_next_page 字段能夠判斷是否存在下一頁的數據;若是有下一頁的數據,能夠繼續爬取,不然終止爬蟲程序。

ps:因爲 Wx 反爬作的很完善,因此儘可能下降爬取的速度。


response = requests.get(current_request_url, headers=self.headers, verify=False)
        result = response.json()

        if result.get("ret") == 0:
            msg_list = result.get('general_msg_list')

            # 保存數據
            self._save(msg_list)
            self.logger.info("獲取到一頁數據成功, data=%s" % (msg_list))

            # 獲取下一頁數據
            has_next_page = result.get('can_msg_continue')
            if has_next_page == 1:
                # 繼續爬取寫一頁的數據【經過next_offset】
                next_offset = result.get('next_offset')

                # 休眠2秒,繼續爬下一頁
                time.sleep(2)
                self.spider_more(next_offset)
            else:  # 當 has_next 爲 0 時,說明已經到了最後一頁,這時纔算爬完了一個公衆號的全部歷史文章
                print('爬取公號完成!')
        else:
            self.logger.info('沒法獲取到更多內容,請更新cookie或其餘請求頭信息')
複製代碼

因爲獲取到的列表數據是一個字符串,須要經過 json 庫去解析,獲取有用的數據。

def _save(self, msg_list):
        """ 數據解析 :param msg_list: :return: """
        # 1.去掉多餘的斜線,使【連接地址】可用
        msg_list = msg_list.replace("\/", "/")
        data = json.loads(msg_list)

        # 2.獲取列表數據
        msg_list = data.get("list")
        for msg in msg_list:
            # 3.發佈時間
            p_date = msg.get('comm_msg_info').get('datetime')

            # 注意:非圖文消息沒有此字段
            msg_info = msg.get("app_msg_ext_info")

            if msg_info:  # 圖文消息
                # 若是是多圖文推送,把第二條第三條也保存
                multi_msg_info = msg_info.get("multi_app_msg_item_list")

                # 若是是多圖文,就從multi_msg_info中獲取數據插入;反之直接從app_msg_ext_info中插入
                if multi_msg_info:
                    for multi_msg_item in multi_msg_info:
                        self._insert(multi_msg_item, p_date)
                else:
                    self._insert(msg_info, p_date)
            else:
                # 非圖文消息
                # 轉換爲字符串再打印出來
                self.logger.warning(u"此消息不是圖文推送,data=%s" % json.dumps(msg.get("comm_msg_info")))
複製代碼

最後一步是將數據保存保存到 MongoDB 數據庫中。

首先要建立一個 Model 保存咱們須要的數據。

from datetime import datetime

from mongoengine import connect
from mongoengine import DateTimeField
from mongoengine import Document
from mongoengine import IntField
from mongoengine import StringField
from mongoengine import URLField

__author__ = 'xag'

# 權限鏈接數據庫【數據庫設置了權限,這裏必須指定用戶名和密碼】
response = connect('admin', host='localhost', port=27017,username='root', password='xag')

class Post(Document):
    """ 文章【模型】 """
    title = StringField()  # 標題
    content_url = StringField()  # 文章連接
    source_url = StringField()  # 原文連接
    digest = StringField()  # 文章摘要
    cover = URLField(validation=None)  # 封面圖
    p_date = DateTimeField()  # 推送時間
    author = StringField()  # 做者

    content = StringField()  # 文章內容

    read_num = IntField(default=0)  # 閱讀量
    like_num = IntField(default=0)  # 點贊數
    comment_num = IntField(default=0)  # 評論數
    reward_num = IntField(default=0)  # 點贊數

    c_date = DateTimeField(default=datetime.now)  # 數據生成時間
    u_date = DateTimeField(default=datetime.now)  # 數據最後更新時間
複製代碼

使用命令行開啓數據庫服務,而後就能夠往數據庫寫入數據了。


def _insert(self, msg_info, p_date):
        """ 數據插入到 MongoDB 數據庫中 :param msg_info: :param p_date: :return: """
        keys = ['title', 'author', 'content_url', 'digest', 'cover', 'source_url']

        # 獲取有用的數據,構建數據模型
        data = sub_dict(msg_info, keys)
        post = Post(**data)

        # 時間格式化
        date_pretty = datetime.fromtimestamp(p_date)
        post["p_date"] = date_pretty

        self.logger.info('save data %s ' % post.title)

        # 保存數據
        try:
            post.save()
        except Exception as e:
            self.logger.error("保存失敗 data=%s" % post.to_json(), exc_info=True)
複製代碼

05 爬取結果

推薦使用工具 Robo3T 鏈接 MongoDB 數據庫,能夠查看到公號文章數據已經所有保存到數據庫中。

本文首發於公衆號「 AirPython 」,後臺回覆「公號1」便可獲取完整代碼。

相關文章
相關標籤/搜索