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

image

阅读文本大概需要 10 分钟。

01 抓取目标

场景:有时候我们想爬取某个大 V 的发布的全部的文章进行学习或者分析。

这个爬虫任务我们需要借助「 Charles 」这个抓包工具,设置好手机代理 IP 去请求某个页面,通过分析,模拟请求,获取到实际的数据。

我们要爬取文章的作者、文章标题、封面图、推送时间、文件内容、阅读量、点赞数、评论数、文章实际链接等数据,最后要把数据存储到「 MongoDB 」数据库中。

02 准备工作

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

image

然后,手机连上同一个网段,并手动设置代理 IP,端口号默认填 8888 。最后配置 PC 和手机上的证书及 SSL Proxying,保证能顺利地抓到 HTTPS 的请求。具体的方法可以参考下面的文章。

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

image

03 爬取思路

首先我们选中一个微信公众号,依次点击右上角的头像、历史消息,就可以进入到全部消息的主界面。默认展示的是前 10 天历史消息。

然后可以查看 Charles 抓取的请求数据,可以通过「 mp.weixin.qq.com 」去过滤请求,获取到消息首页发送的请求及请求方式及响应内容。

image

继续往下滚动页面,可以加载到下一页的数据,同样可以获取到请求和响应的数据。

image

爬取的数据最后要保存在 MongoDB 文档型数据库中,所以不需要建立数据模型,只需要安装软件和开启服务就可以了。MongoDB 的使用教程可以参考下面的链接:

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

image

为了操作 MongoDB 数据库,这里使用「 MongoEngine 」这个类似于关系型数据库中的 ORM 框架来方便我们处理数据。

pip3 install mongoengine

04 代码实现

从上面的分析中可以知道首页消息、更多页面消息的请求 URL 规律如下:

# 由于微信屏蔽的关键字, 字段 netloc + path 用 ** 代替# 首页请求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# 第二页请求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# 第三页请求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 设置为可变数据,请求所有页面的数据 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 下面。因此可以需要拿到数据后进行解析操作。

image

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 datetimefrom mongoengine import connectfrom mongoengine import DateTimeFieldfrom mongoengine import Documentfrom mongoengine import IntFieldfrom mongoengine import StringFieldfrom 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)  # 数据最后更新时间

使用命令行开启数据库服务,然后就可以往数据库写入数据了。

image
  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 数据库,可以查看到公号文章数据已经全部保存到数据库中。

image

本文首发于公众号「 AirPython 」,后台回复「 公号1 」即可获取完整代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容