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

image

閱讀文本大概需要 10 分鐘。

01 抓取目標(biāo)

場(chǎng)景:有時(shí)候我們想爬取某個(gè)大 V 的發(fā)布的全部的文章進(jìn)行學(xué)習(xí)或者分析。

這個(gè)爬蟲任務(wù)我們需要借助「 Charles 」這個(gè)抓包工具,設(shè)置好手機(jī)代理 IP 去請(qǐng)求某個(gè)頁面,通過分析,模擬請(qǐng)求,獲取到實(shí)際的數(shù)據(jù)。

我們要爬取文章的作者、文章標(biāo)題、封面圖、推送時(shí)間、文件內(nèi)容、閱讀量、點(diǎn)贊數(shù)、評(píng)論數(shù)、文章實(shí)際鏈接等數(shù)據(jù),最后要把數(shù)據(jù)存儲(chǔ)到「 MongoDB 」數(shù)據(jù)庫中。

02 準(zhǔn)備工作

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

image

然后,手機(jī)連上同一個(gè)網(wǎng)段,并手動(dòng)設(shè)置代理 IP,端口號(hào)默認(rèn)填 8888 。最后配置 PC 和手機(jī)上的證書及 SSL Proxying,保證能順利地抓到 HTTPS 的請(qǐng)求。具體的方法可以參考下面的文章。

http://www.itdecent.cn/p/595e8b556a60?from=timeline&isappinstalled=0

image

03 爬取思路

首先我們選中一個(gè)微信公眾號(hào),依次點(diǎn)擊右上角的頭像、歷史消息,就可以進(jìn)入到全部消息的主界面。默認(rèn)展示的是前 10 天歷史消息。

然后可以查看 Charles 抓取的請(qǐng)求數(shù)據(jù),可以通過「 mp.weixin.qq.com 」去過濾請(qǐng)求,獲取到消息首頁發(fā)送的請(qǐng)求及請(qǐng)求方式及響應(yīng)內(nèi)容。

image

繼續(xù)往下滾動(dòng)頁面,可以加載到下一頁的數(shù)據(jù),同樣可以獲取到請(qǐng)求和響應(yīng)的數(shù)據(jù)。

image

爬取的數(shù)據(jù)最后要保存在 MongoDB 文檔型數(shù)據(jù)庫中,所以不需要建立數(shù)據(jù)模型,只需要安裝軟件和開啟服務(wù)就可以了。MongoDB 的使用教程可以參考下面的鏈接:

http://www.itdecent.cn/p/4c5deb1b7e7c

image

為了操作 MongoDB 數(shù)據(jù)庫,這里使用「 MongoEngine 」這個(gè)類似于關(guān)系型數(shù)據(jù)庫中的 ORM 框架來方便我們處理數(shù)據(jù)。

pip3 install mongoengine

04 代碼實(shí)現(xiàn)

從上面的分析中可以知道首頁消息、更多頁面消息的請(qǐng)求 URL 規(guī)律如下:

# 由于微信屏蔽的關(guān)鍵字, 字段 netloc + path 用 ** 代替# 首頁請(qǐng)求urlhttps://**?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# 第二頁請(qǐng)求urlhttps://**?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# 第三頁請(qǐng)求urlhttps://**?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 設(shè)置為可變數(shù)據(jù),請(qǐng)求所有頁面的數(shù)據(jù) 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 獲取到請(qǐng)求頭。由于微信的反爬機(jī)制,這里的 Cookie 和 Referer 有一定的時(shí)效性,需要定時(shí)更換。

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 去模擬發(fā)送請(qǐng)求。

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

通過 Charles 返回的數(shù)據(jù)格式可以得知消息列表的數(shù)據(jù)存儲(chǔ)在 general_msg_list 這個(gè) Key 下面。因此可以需要拿到數(shù)據(jù)后進(jìn)行解析操作。

image

has_next_page 字段可以判斷是否存在下一頁的數(shù)據(jù);如果有下一頁的數(shù)據(jù),可以繼續(xù)爬取,否則終止爬蟲程序。

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')            # 保存數(shù)據(jù)            self._save(msg_list)            self.logger.info("獲取到一頁數(shù)據(jù)成功, data=%s" % (msg_list))            # 獲取下一頁數(shù)據(jù)            has_next_page = result.get('can_msg_continue')            if has_next_page == 1:                # 繼續(xù)爬取寫一頁的數(shù)據(jù)【通過next_offset】                next_offset = result.get('next_offset')                # 休眠2秒,繼續(xù)爬下一頁                time.sleep(2)                self.spider_more(next_offset)            else:  # 當(dāng) has_next 為 0 時(shí),說明已經(jīng)到了最后一頁,這時(shí)才算爬完了一個(gè)公眾號(hào)的所有歷史文章                print('爬取公號(hào)完成!')        else:            self.logger.info('無法獲取到更多內(nèi)容,請(qǐng)更新cookie或其他請(qǐng)求頭信息')

由于獲取到的列表數(shù)據(jù)是一個(gè)字符串,需要通過 json 庫去解析,獲取有用的數(shù)據(jù)。

def _save(self, msg_list):        """        數(shù)據(jù)解析        :param msg_list:        :return:        """        # 1.去掉多余的斜線,使【鏈接地址】可用        msg_list = msg_list.replace("\/", "/")        data = json.loads(msg_list)        # 2.獲取列表數(shù)據(jù)        msg_list = data.get("list")        for msg in msg_list:            # 3.發(fā)布時(shí)間            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中獲取數(shù)據(jù)插入;反之直接從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:                # 非圖文消息                # 轉(zhuǎn)換為字符串再打印出來                self.logger.warning(u"此消息不是圖文推送,data=%s" % json.dumps(msg.get("comm_msg_info")))

最后一步是將數(shù)據(jù)保存保存到 MongoDB 數(shù)據(jù)庫中。首先要?jiǎng)?chuàng)建一個(gè) Model 保存我們需要的數(shù)據(jù)。

from datetime import datetimefrom mongoengine import connectfrom mongoengine import DateTimeFieldfrom mongoengine import Documentfrom mongoengine import IntFieldfrom mongoengine import StringFieldfrom mongoengine import URLField__author__ = 'xag'# 權(quán)限連接數(shù)據(jù)庫【數(shù)據(jù)庫設(shè)置了權(quán)限,這里必須指定用戶名和密碼】response = connect('admin', host='localhost', port=27017,username='root', password='xag')class Post(Document):    """    文章【模型】    """    title = StringField()  # 標(biāo)題    content_url = StringField()  # 文章鏈接    source_url = StringField()  # 原文鏈接    digest = StringField()  # 文章摘要    cover = URLField(validation=None)  # 封面圖    p_date = DateTimeField()  # 推送時(shí)間    author = StringField()  # 作者    content = StringField()  # 文章內(nèi)容    read_num = IntField(default=0)  # 閱讀量    like_num = IntField(default=0)  # 點(diǎn)贊數(shù)    comment_num = IntField(default=0)  # 評(píng)論數(shù)    reward_num = IntField(default=0)  # 點(diǎn)贊數(shù)    c_date = DateTimeField(default=datetime.now)  # 數(shù)據(jù)生成時(shí)間    u_date = DateTimeField(default=datetime.now)  # 數(shù)據(jù)最后更新時(shí)間

使用命令行開啟數(shù)據(jù)庫服務(wù),然后就可以往數(shù)據(jù)庫寫入數(shù)據(jù)了。

image
  def _insert(self, msg_info, p_date):        """        數(shù)據(jù)插入到 MongoDB 數(shù)據(jù)庫中        :param msg_info:        :param p_date:        :return:        """        keys = ['title', 'author', 'content_url', 'digest', 'cover', 'source_url']        # 獲取有用的數(shù)據(jù),構(gòu)建數(shù)據(jù)模型        data = sub_dict(msg_info, keys)        post = Post(**data)        # 時(shí)間格式化        date_pretty = datetime.fromtimestamp(p_date)        post["p_date"] = date_pretty        self.logger.info('save data %s ' % post.title)        # 保存數(shù)據(jù)        try:            post.save()        except Exception as e:            self.logger.error("保存失敗 data=%s" % post.to_json(), exc_info=True)

05 爬取結(jié)果

推薦使用工具 Robo3T 連接 MongoDB 數(shù)據(jù)庫,可以查看到公號(hào)文章數(shù)據(jù)已經(jīng)全部保存到數(shù)據(jù)庫中。

image

本文首發(fā)于公眾號(hào)「 AirPython 」,后臺(tái)回復(fù)「 公號(hào)1 」即可獲取完整代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容