分享

欢度国庆⭐️共享爬虫之美⭐️基于 Python 实现微信公众号爬虫(Python无所不能爬)

 zhulin1028 2021-12-29

此文章撰写于国庆假期,以此纪念。祝大家万事大吉,心想事成,家和万事兴!国庆快乐!


微信公众号爬虫的基本原理

网上关于爬虫的教程多如牛毛,但很少有看到微信公众号爬虫教程,要有也是基于搜狗微信的,不过搜狗提供的数据有诸多弊端,比如文章链接是临时的,文章没有阅读量等指标,所以我想写一个比较系统的关于如何通过手机客户端利用 Python 爬微信公众号文章的教程,并对公众号文章做数据分析,为更好的运营公众号提供决策。

爬虫的基本原理

所谓爬虫就是一个自动化数据采集工具,你只要告诉它要采集哪些数据,丢给它一个 URL,就能自动地抓取数据了。其背后的基本原理就是爬虫程序向目标服务器发起 HTTP 请求,然后目标服务器返回响应结果,爬虫客户端收到响应并从中提取数据,再进行数据清洗、数据存储工作。

爬虫的基本流程

爬虫流程也是一个 HTTP 请求的过程,以浏览器访问一个网址为例,从用户输入 URL 开始,客户端通过 DNS 解析查询到目标服务器的 IP 地址,然后与之建立 TCP 连接,连接成功后,浏览器构造一个 HTTP 请求发送给服务器,服务器收到请求之后,从数据库查到相应的数据并封装成一个 HTTP 响应,然后将响应结果返回给浏览器,浏览器对响应内容进行数据解析、提取、渲染并最终展示在你面前。

HTTP 协议的请求和响应都必须遵循固定的格式,只有遵循统一的 HTTP 请求格式,服务器才能正确解析不同客户端发的请求,同样地,服务器遵循统一的响应格式,客户端才得以正确解析不同网站发过来的响应。

HTTP 请求格式

HTTP 请求由请求行、请求头、空行、请求体组成。

 请求行由三部分组成:

  1. 第一部分是请求方法,常见的请求方法有 GET、POST、PUT、DELETE、HEAD
  2. 第二部分是客户端要获取的资源路径
  3. 第三部分是客户端使用的 HTTP 协议版本号

请求头是客户端向服务器发送请求的补充说明,比如 User-Agent 向服务器说明客户端的身份。

请求体是客户端向服务器提交的数据,比如用户登录时需要提高的账号密码信息。请求头与请求体之间用空行隔开。请求体并不是所有的请求都有的,比如一般的GET都不会带有请求体。

上图就是浏览器登录豆瓣时向服务器发送的HTTP POST 请求,请求体中指定了用户名和密码。

HTTP 响应格式

HTTP 响应格式与请求的格式很相似,也是由响应行、响应头、空行、响应体组成。

响应行也包含三部分,分别是服务端的 HTTP 版本号、响应状态码、状态说明,响应状态码常见有 200、400、404、500、502、304 等等,一般以 2 开头的表示服务器正常响应了客户端请求,4 开头表示客户端的请求有问题,5 开头表示服务器出错了,没法正确处理客户端请求。状态码说明就是对该状态码的一个简短描述。

第二部分就是响应头,响应头与请求头对应,是服务器对该响应的一些附加说明,比如响应内容的格式是什么,响应内容的长度有多少、什么时间返回给客户端的、甚至还有一些 Cookie 信息也会放在响应头里面。

第三部分是响应体,它才是真正的响应数据,这些数据其实就是网页的 HTML 源代码。

使用 Requests 实现一个简单网页爬虫

Python 提供了非常多工具去实现 HTTP 请求,但第三方开源库提供的功能更丰富,你无需从 socket 通信开始写,比如使用Pyton内建模块 urllib 请求一个 URL 代码示例如下:

import ssl from urllib.request;
import Request from urllib.request;
import urlopen context = ssl._create_unverified_context() 
# HTTP 请求 
request = Request(url="https:///pip.html", method="GET", headers={"Host": ""}, data=None) # HTTP 响应 
response = urlopen(request, context=context) headers = response.info() # 响应头 
content = response.read() # 响应体 
code = response.getcode() # 状态码

发起请求前首先要构建请求对象 Request,指定 url 地址、请求方法、请求头,这里的请求体 data 为空,因为你不需要提交数据给服务器,所以你也可以不指定。urlopen 函数会自动与目标服务器建立连接,发送 HTTP 请求,该函数的返回值是一个响应对象 Response,里面有响应头信息,响应体,状态码之类的属性。

但是,Python 提供的这个内建模块过于低级,需要写很多代码,使用简单爬虫可以考虑 Requests,Requests 在GitHub 有近30k的Star,是一个很Pythonic的框架。先来简单熟悉一下这个框架的使用方式

安装 requests

pip install requests

GET 请求

>>> r = requests.get("https:///ip") 
>>> r 
<Response [200]> # 响应对象 
>>> r.status_code # 响应状态码 200 
>>> r.content # 响应内容 
'{\n "origin": "183.237.232.123"\n}\n'

POST 请求

>>> r = requests.post('http:///post', data = {'key':'value'})

自定义请求头

这个经常会用到,服务器反爬虫机制会判断客户端请求头中的User-Agent是否来源于真实浏览器,所以,我们使用Requests经常会指定UA伪装成浏览器发起请求

>>> url = 'https:///headers' 
>>> headers = {'user-agent': 'Mozilla/5.0'}
>>> r = requests.get(url, headers=headers)

参数传递

很多时候URL后面会有一串很长的参数,为了提高可读性,requests 支持将参数抽离出来作为方法的参数(params)传递过去,而无需附在 URL 后面,例如请求 url http:///get?key=val ,可使用

>>> url = "http:///get" 
>>> r = requests.get(url, params={"key":"val"}) 
>>> r.url u'http:///get?key=val'

指定Cookie

Cookie 是web浏览器登录网站的凭证,虽然 Cookie 也是请求头的一部分,我们可以从中剥离出来,使用 Cookie 参数指定

>>> s = requests.get('http:///cookies', cookies={'from-my': 'browser'})
>>> s.text 
u'{\n "cookies": {\n "from-my": "browser"\n }\n}\n'

设置超时

当发起一个请求遇到服务器响应非常缓慢而你又不希望等待太久时,可以指定 timeout 来设置请求超时时间,单位是秒,超过该时间还没有连接服务器成功时,请求将强行终止。

r = requests.get('https://google.com', timeout=5)

设置代理

一段时间内发送的请求太多容易被服务器判定为爬虫,所以很多时候我们使用代理IP来伪装客户端的真实IP。

import requests 
proxies = { 'http': 'http://127.0.0.1:1080', 'https': 'http://127.0.0.1:1080', } 
r = requests.get('http://www./free/', proxies=proxies, timeout=2)

Session

如果想和服务器一直保持登录(会话)状态,而不必每次都指定 cookies,那么可以使用 session,Session 提供的API和 requests 是一样的。

import requests 
s = requests.Session() 
s.cookies = requests.utils.cookiejar_from_dict({"a": "c"}) 
r = s.get('http:///cookies') 
print(r.text) 
# '{"cookies": {"a": "c"}}' 
r = s.get('http:///cookies') 
print(r.text) 
# '{"cookies": {"a": "c"}}'

小试牛刀

现在我们使用Requests完成一个爬取知乎专栏用户关注列表的简单爬虫为例,找到任意一个专栏,打开它的关注列表。用 Chrome 找到获取粉丝列表的请求地址:https://www.zhihu.com/api/v4/columns/pythoneer/followers?include=data%5B%2A%5D.follower_count%2Cgender%2Cis_followed%2Cis_following&limit=10&offset=20。 我是怎么找到的?就是逐个点击左侧的请求,观察右边是否有数据出现,那些以 .jpgjscss 结尾的静态资源可直接忽略。

现在我们用 Requests 模拟浏览器发送请求给服务器,写程序前,我们要先分析出这个请求是怎么构成的,请求URL是什么?请求头有哪些?查询参数有哪些?只有清楚了这些,你才好动手写代码,掌握分析方法很重要,否则一头雾水。

回到前面那个URL,我们发现这个URL是获取粉丝列表的接口,然后再来详细分析一下这个请求是怎么构成的。

  • 请求URL:https://www.zhihu.com/api/v4/columns/pythoneer/followers
  • 请求方法:GET
  • user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
  • 查询参数:
    • include: data[*].follower_count,gender,is_followed,is_following
    • offset: 0
    • limit: 10

利用这些请求数据我们就可以用requests这个库来构建一个请求,通过Python代码来抓取这些数据。

import requests


class SimpleCrawler:

    def crawl(self, params=None):
        # 必须指定UA,否则知乎服务器会判定请求不合法

        url = "https://www.zhihu.com/api/v4/columns/pythoneer/followers"
        # 查询参数
        params = {"limit": 20,
                  "offset": 0,
                  "include": "data[*].follower_count, gender, is_followed, is_following"}

        headers = {
            "authority": "www.zhihu.com",
            "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36",
        }
        response = requests.get(url, headers=headers, params=params)
        print("请求URL:", response.url)
        # 你可以先将返回的响应数据打印出来,拷贝到 http://www./jsoneditor/ 分析其结构。
        print("返回数据:", response.text)

        # 解析返回的数据
        for follower in response.json().get("data"):
            print(follower)


if __name__ == '__main__':
    SimpleCrawler().crawl()

这就是一个最简单的基于 Requests 的单线程知乎专栏粉丝列表的爬虫,requests 非常灵活,请求头、请求参数、Cookie 信息都可以直接指定在请求方法中,返回值 response 如果是 json 格式可以直接调用json()方法返回 python 对象。关于 Requests 的更多使用方法可以参考官方文档:Requests: HTTP for Humans™ — Requests 2.26.0 documentation

使用 Fiddler 抓包分析公众号请求过程

上一节我们熟悉了 Requests 基本使用方法,配合 Chrome 浏览器实现了一个简单爬虫,但因为微信公众号的封闭性,微信公众平台并没有对外提供 Web 端入口,只能通过手机客户端接收、查看公众号文章,所以,为了窥探到公众号背后的网络请求,我们需要借以代理工具的辅助。

HTTP代理工具又称为抓包工具,主流的抓包工具 Windows 平台有 Fiddler,macOS 有 Charles,阿里开源了一款工具叫 AnyProxy。它们的基本原理都是类似的,就是通过在手机客户端设置好代理IP和端口,客户端所有的 HTTP、HTTPS 请求就会经过代理工具,在代理工具中就可以清晰地看到每个请求的细节,然后可以分析出每个请求是如何构造的,弄清楚这些之后,我们就可以用 Python 模拟发起请求,进而得到我们想要的数据。

Fiddler 下载地址是 Download Fiddler Web Debugging Tool for Free by Telerik,安装包就 4M 多,在配置之前,首先要确保你的手机和电脑在同一个局域网,如果不在同一个局域网,你可以买个随身WiFi,在你电脑上搭建一个极简无线路由器。安装过程一路点击下一步完成就可以了。

Fiddler 配置

选择 Tools > Fiddler Options > Connections

Fiddler 默认的端口是使用 8888,如果该端口已经被其它程序占用了,你需要手动更改,勾选 Allow remote computers to connect,其它的选择默认配置就好,配置更新后记得重启 Fiddler。一定要重启 Fiddler,否则代理无效。

接下来你需要配置手机,我们以 Android 设备为例,现在假设你的手机和电脑已经在同一个局域网(只要连的是同一个路由器就在同局域网内),找到电脑的 IP 地址,在 Fiddler 右上角有个 Online 图标,鼠标移过去就能看到IP了,你也可以在CMD窗口使用 ipconfig 命令查看到

Android 手机代理配置

进入手机的 WLAN 设置,选择当前所在局域网的 WiFi 链接,设置代理服务器的 IP 和端口,我这是以小米设备为例,其它 Android 手机的配置过程大同小异。

测试代理有没有设置成功可以在手机浏览器访问你配置的地址:http://192.168.31.236:8888/ 会显示 Fiddler 的回显页面,说明配置成功。

现在你打开任意一个HTTP协议的网站都能看到请求会出现在 Fiddler 窗口,但是 HTTPS 的请求并没有出现在 Fiddler 中,其实还差一个步骤,需要在 Fiddler 中激活 HTTPS 抓取设置。在 Fiddler 选择 Tools > Fiddler Options > HTTPS > Decrypt HTTPS traffic, 重启 Fiddler。

为了能够让 Fiddler 截取 HTTPS 请求,客户端都需要安装且信任 Fiddler 生成的 CA 证书,否则会出现“网络出错,轻触屏幕重新加载:-1200”的错误。在浏览器打开 Fiddler 回显页面 http://192.168.31.236:8888/ 下载 FiddlerRoot certificate,下载并安装证书,并验证通过。

iOS下载安装完成之后还要从 设置->通用->关于本机->证书信任设置 中把 Fiddler 证书的开关打开

Android 手机下载保存证书后从系统设置里面找到系统安全,从SD卡安装证书,如果没有安装证书,打开微信公众号的时候会弹出警告。

至此,所有的配置都完成了,现在打开微信随便选择一个公众号,查看公众号的所有历史文章列表。微信在2018年6月份对 iOS 版本的微信以及部分 Android 版微信针对公众号进行了大幅调整,改为现在的信息流方式,现在要获取某个公众号下面「所有文章列表」大概需要经过以下四个步骤:

如果你的微信版本还不是信息流方式展示的,那么应该是Android版本(微信采用的ABTest,不同的用户呈现的方式不一样)

同时观察 Fiddler 主面板

进入「全部消息」页面时,在 Fiddler 上已经能看到有请求进来了,说明公众号的文章走的都是HTTP协议,这些请求就是微信客户端向微信服务器发送的HTTP请求。

注意:第一次请求「全部消息」的时候你看到的可能是一片空白:

在Fiddler或Charles中看到的请求数据是这样的:

这个时候你要直接从左上角叉掉重新进入「全部消息」页面。

现在简单介绍一下这个请求面板上的每个模块的意义。

这样说明这个请求被微信服务器判定为一次非法的请求,这时你可以叉掉该页面重新进入「全部消息」页面。不出意外的话就能正常看到全部文章列表了,同时也能在Fiddler中看到正常的数据请求了。

我把上面的主面板划分为 7 大块,你需要理解每块的内容,后面才有可能会用 Python 代码来模拟微信请求。

1、服务器的响应结果,200 表示服务器对该请求响应成功
2、请求协议,微信的请求协议都是基 于HTTPS 的,所以前面一定要配置好,不然你看不到 HTTPS 的请求。
3、微信服务器主机名
4、请求路径
5、请求行,包括了请求方法(GET),请求协议(HTTP/1.1),请求路径(/mp/profile_ext...后面还有很长一串参数) 6、包括Cookie信息在内的请求头。
7、微信服务器返回的响应数据,我们分别切换成 TextView 和 WebView 看一下返回的数据是什么样的。

TextView 模式下的预览效果是服务器返回的 HTML 源代码

WebView 模式是 HTML 代码经过渲染之后的效果,其实就是我们在手机微信中看到的效果,只不过因为缺乏样式,所以没有手机上看到的美化效果。

如果服务器返回的是 Json格式或者是 XML,你还可以切换到对应的页面预览查看。

小结

配置好Fiddler的几个步骤主要包括指定监控的端口,开通HTTPS流量解密功能,同时,客户端需要安装CA证书。下一节我们基于Requests模拟像微信服务器发起请求。

抓取第一篇微信公众号文章

打开微信历史消息页面,我们从 Fiddler 看到了很多请求,为了找到微信历史文章的接口,我们要逐个查看 Response 返回的内容,最后发现第 11 个请求 "https://mp.weixin.qq.com/mp/profile_ext?action=home..." 就是我们要寻找的(我是怎么找到的呢?这个和你的经验有关,你可以点击逐个请求,看看返回的Response内容是不是期望的内容)

确定微信公众号的请求HOST是 mp.weixin.qq.com 之后,我们可以使用过滤器来过滤掉不相关的请求。

爬虫的基本原理就是模拟浏览器发送 HTTP 请求,然后从服务器得到响应结果,现在我们就用 Python 实现如何发送一个 HTTP 请求。这里我们使用 requests 库来发送请求。

创建一个 Pycharm 项目

我们使用 Pycharm 作为开发工具,你也可以使用其它你熟悉的工具,Python 环境是 Python3(推荐使用 Python3.6),先创建一个项目 weixincrawler

现在我们来编写一个最粗糙的版本,你需要做两件事:

  • 1:找到完整URL请求地址
  • 2:找到完整的请求头(headers)信息,Headers里面包括了cookie、User-agent、Host 等信息。

我们直接从 Fiddler 请求中拷贝 URL 和 Headers, 右键 -> Copy -> Just Url/Headers Only

最终拷贝出来的URL很长,它包含了很多的参数:

url = "https://mp.weixin.qq.com/mp/profile_ext"       "?action=home"       "&__biz=MjM5MzgyODQxMQ=="       "&scene=124"       "&devicetype=android-24"       "&version=26051633&lang=zh_CN"       "&nettype=WIFI&a8scene=3"       "&pass_ticket=MXADI5SFjXvX7DFPRuUEJhWHEWvRha2x1Re%2BoJkveUxIonMfnxY1kM9cOPmm6JRx"       "&wx_header=1"

暂且不去分析(猜测)每个参数的意义,也不知道那些参数是必须的,总之我把这些参数全部提取出来。然后把 Headers 拷贝出来,发现 Fiddler 把 请求行、响应行、响应头都包括进来了,我们只需要中间的请求头部分。

因为 requests.get 方法里面的 headers 参数必须是字典对象,所以,先要写个函数把刚刚拷贝的字符串转换成字典对象。

def headers_to_dict(headers):
    """
    将字符串
    '''
    Host: mp.weixin.qq.com
    Connection: keep-alive
    Cache-Control: max-age=
    '''
    转换成字典对象
    {
        "Host": "mp.weixin.qq.com",
        "Connection": "keep-alive",
        "Cache-Control":"max-age="
    }
    :param headers: str
    :return: dict
    """
    headers = headers.split("\n")
    d_headers = dict()
    for h in headers:
        if h:
            k, v = h.split(":", 1)
            d_headers[k] = v.strip()
    return d_headers

最终 v0.1 版本出来了,不出意外的话,公众号历史文章数据就在 response.text 中。如果返回的内容非常短,而且title标签是<title>验证</title>,那么说明你的请求参数或者请求头有误,最有可能的一种请求就是 Headers 里面的 Cookie 字段过期,从手机微信端重新发起一次请求获取最新的请求参数和请求头试试。

# v0.1
def crawl():
    url = "https://mp.weixin.qq.com/..." # 省略了
    headers = """  # 省略了
Host: mp.weixin.qq.com
Connection: keep-alive
Upgrade-Insecure-Requests: 1
    """
    headers = headers_to_dict(headers)
    response = requests.get(url, headers=headers, verify=False)
    print(response.text)
最后,我们顺带把响应结果另存为html文件,以便后面重复使用,分析里面的内容
with open("weixin_history.html", "w", encoding="utf-8") as f:
    f.write(response.text)

用浏览器打开 weixin_history.html 文件,查看该页面的源代码,搜索微信历史文章标题的关键字 "11月赠书"(就是我以往发的文章),你会发现,历史文章封装在叫 msgList 的数组中(实际上该数组包装在字典结构中),这是一个 Json 格式的数据,但是里面还有 html 转义字符需要处理

接下来我们就来写一个方法提取出历史文章数据,分三个步骤,首先用正则提取数据内容,然后 html 转义处理,最终得到一个列表对象,返回最近发布的10篇文章。

def extract_data(html_content):
    """
    从html页面中提取历史文章数据
    :param html_content 页面源代码
    :return: 历史文章列表
    """
    import re
    import html
    import json

    rex = "msgList = '({.*?})'"
    pattern = re.compile(pattern=rex, flags=re.S)
    match = pattern.search(html_content)
    if match:
        data = match.group(1)
        data = html.unescape(data)
        data = json.loads(data)
        articles = data.get("list")
        for item in articles:
            print(item)
        return articles

最终提取出来的数据总共有10条,就是最近发表的10条数据,我们看看每条数据返回有哪些字段。

article = {'app_msg_ext_info': 
                {'title': '11月赠书,总共10本,附Python书单',
                 'copyright_stat': 11,
                 'is_multi': 1,
                 'content': '',
                 'author': '刘志军',
                 'subtype': 9,
                 'del_flag': 1,
                 'fileid': 502883895,
                 'content_url': 'http:\\/\\/mp.weixin.qq.com...',
                 ''
                 'digest': '十一月份赠书福利如期而至,更多惊喜等着你',
                 'cover': 'http:\\/\\/mmbiz.qpic.cn\\...',
                 'multi_app_msg_item_list': [{'fileid': 861719336,
                                              'content_url': 'http:\\/\\/mp.weixin.qq.com',
                                              'content': '', 'copyright_stat': 11,
                                              'cover': 'http:\\/\\/mmbiz.qpic.cn',
                                              'del_flag': 1,
                                              'digest': '多数情况下,人是种短视的动物',
                                              'source_url': '',
                                              'title': '罗胖60秒:诺贝尔奖设立时,为何会被骂?',
                                              'author': '罗振宇'
                                              }],
                 'source_url': 'https:\\/\\/github.com\'
                 },
      'comm_msg_info': {'datetime': 1511827200,
                        'status': 2,
                        'id': 1000000161,
                        'fakeid': '2393828411',
                        'content': '',
                        'type': 49}}

comm_msg_info.datetimeapp_msg_ext_info中的字段信息就是第一篇文章的字段信息,分别对应:

  • title:文章标题
  • content_url:文章链接
  • source_url:原文链接,有可能为空
  • digest:摘要
  • cover:封面图
  • datetime:推送时间

后面几篇文章以列表的形式保存在 multi_app_msg_item_list 字段中。

到此,公众号文章的基本信息就抓到了,但也仅仅只是公众号的前10条推送。

资源来源于网络,纯属分享,不做商业用途,如若侵犯了您的权益和利益,请告知删除。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多