louis110

louis110 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

louis110 赞了文章 · 6月8日

如何进行手机APP的数据爬取?

作者:东哥起飞
微信公众号:Python数据科学
知乎:Python数据分析


平时我们的爬虫多是针对网页的,但是随着手机端APP应用数量的增多,相应的爬取需求也就越来越多,因此手机端APP的数据爬取对于一名爬虫工程师来说是一项必备的技能。我们知道,网页爬取的时候我经常使用F12开发者工具或者fiddler之类的工具来帮助我们分析浏览器行为。那对于手机的APP该如何使用呢?同样的,我们也可以使用fiddler来分析。好了,本篇博主将会给大家介绍如何在电脑端使用fiddler进行手机APP的抓包。

首先了解一下fiddler(百度百科):

Fiddler是一个http协议调试代理工具,它能够记录并检查所有你的电脑和互联网之间的http通讯,设置断点,查看所有的“进出”Fiddler的数据(指cookie,html,js,css等文件,这些都可以让你胡乱修改的意思)。 Fiddler 要比其他的网络调试器要更加简单,因为它不仅仅暴露http通讯还提供了一个用户友好的格式。

完成此项工作的整个流程可分为如下几个步骤。

1. 下载fiddler抓包工具

fiddler的官方下载链接:https://www.telerik.com/downl...
安装步骤没什么特别,常规下一步完成即可。

2. 设置fiddler

这里有两点需要说明一下。

  • 设置允许抓取HTTPS信息包

操作很简单,打开下载好的fiddler,找到 Tools -> Options,然后再HTTPS的工具栏下勾选Decrpt HTTPS traffic,在新弹出的选项栏下勾选Ignore server certificate errors

图片描述

  • 设置允许外部设备发送HTTP/HTTPSfiddler

相同的,在Connections选项栏下勾选Allow remote computers to connect,并记住上面的端口号8888,后面会使用到。

图片描述

好了,需要的fiddler设置就配置完成了。

3. 设置手机端

设置手机端之前,我们需要记住一点:电脑和手机需要在同一个网络下进行操作
可以使用wifi或者手机热点等来完成。

假如你已经让电脑和手机处于同一个网络下了,这时候我们需要知道此网络的ip地址,可以在命令行输入ipconfig简单的获得,如图。

图片描述

好了,下面我们开始手机端的设置。

手机APP的抓取操作对于AndroidApple系统都可用,博主使用的苹果系统,在此以苹果系统为例。

进入到手机wifi的设置界面,选择当前连接网络的更多信息,在苹果中是一个叹号。然后在最下面你会看到HTTP代理的选项,点击进入。

图片描述

进入后,填写上面记住的ip地址端口号,确定保存。

图片描述

4. 下载fiddler安全证书

在手机上打开浏览器输入一个上面ip地址和端口号组成的url:http://192.168.43.38:8888,然后点击FiddlerRoot certificate下载fiddler证书

图片描述

以上就简单完成了所有的操作,最后我们测试一下是否好用。

5. 手机端测试

就以知乎APP为例,在手机上打开 知乎APP。下面是电脑上fiddler的抓包结果。

图片描述

结果没有问题,抓到信息包。然后就可以使用我们分析网页的方法来进行后续的操作了。


原创不易,来波点赞支持。

关注微信公众号 Python数据科学,回复666获取100g学习资料。

数据科学网站:http://www.datadeepin.com/

查看原文

赞 21 收藏 32 评论 3

louis110 赞了文章 · 6月8日

【精华分享】:转行数据分析的一份学习清单

作者:xiaoyu

微信公众号:Python数据科学

知乎:python数据分析师


上一篇主要分享了博主亲身转行数据分析的经历:

本篇继上一篇将分享转行数据分析的一些经验和学习方法,看完这篇你将会解决以下几个问题:

  • 转行数据分析需要掌握哪些学习重点?
  • 转行学习数据分析有哪些好的学习资源?

注意:

  • 本篇内容是针对所有想转行数据分析人员的,内容深浅不一。若内容过于简单,可直接略过,若内容过于复杂,也不必担忧
  • 本篇涉及到的所有推荐书籍博主已经打包,文章末尾会提供获取方式

编程基础

如果你是一个对编程毫无经验的小白,那么首先你应该掌握一定的编程基础(尤其像从其它行业转行到IT行业的朋友们)。对于新手来说,博主认为Python语言是最佳的选择。作为一个解释型的动态高级语言,Python易于理解,上手简单,非常适合初学者学习。一本快速入门Python语言的书籍推荐:简明Python。这本书英文原版为《A Byte of Python》,经翻译变为《简明Python》。博主也给好多人推荐过,大家看过之后基本上都很认同,是入门Python最快效果最好的书籍

如果你已经了解了Python编程的基础用法想要继续深入学习Pyhon,那么博主推荐你去看:廖学峰Python教程。它基本上涵盖了Python编程入门到精通的所有知识,如果你能将这个看透,那么可以说你已经掌握了Python这门语言了。

学完了Python的理论知识,当然就需要应用,需要实战。博主之前分享了一篇文章非常适合Python初学者的实战项目,非常有趣,也易于实现。如果你也苦于找不到一个合适的练习项目,那么可以尝试一下这个练习项目:

数据分析基础

抛开对业务层面的基本理解,学好数据分析首先需要了解统计学,统计分析是数据分析的基础,也是灵魂。下面博主列出统计分析的几个核心内容:

  • 描述统计,统计推断,概率论;
  • 抽样,分布,估计,置信区间,假设检验;
  • 线性回归,时间序列;

博主推荐一本比较好的统计学书籍:统计学,这本书清晰的讲述了基础的统计学知识,非常经典。

数据分析工具

SQL语言

博主之前做过一个统计,就是统计招聘网站上关于数据分析师的招聘信息关键词,其中词频最高的是SQL。这就说明了一个问题:数据分析师最关键的一项技能就是会使用SQL语言操作数据库。

关于SQL的学习博主推荐两个学习路径:

  • w3school
  • SQL必知必会

这个学习没有捷径,需要一个学习规划,一般学习周期不长,两个星期就可以学一遍,但是更多的是反复练习刷题,推荐到Leetcode进行一些实践练习。

Excel基本操作

作为微软的一个出色表格处理工具,Excel也是数据分析师需要掌握的。因为公司很多其它部门非技术人员是不会使用编程工具的,而会使用相对简单的Excel来处理一些报表。这个时候就可能需要你可以在Excel中做一些数据分析工作然后反馈,但是也不必太深入,掌握核心的功能即可,比如:

  • 增删改查
  • 各类常用函数的使用
  • 各类基础图标的制作
  • 数据透视表等

能够熟练运用上面功能就可以,学习周期很短,甚至一天就能学会,主要是熟练。而对于剩下的复杂功能等遇到了再学习也不迟。

Python or R?

R语言就是为统计学而设计的语言,是统计行业中非常高效实用的工具,目前非常受欢迎。而Python作为目前非常火爆的语言,由于其出色的科学计算包pandasnumpyscikit-learn等的存在,非常适合于数据分析与数据挖掘,也是很多人的不二选择。

关于这PythonR,博主认为二者皆可,选择自己顺手和喜欢的。由于博主自己是Python爱好者,也因为它的简单易用,因此强烈推荐使用Python。在Python的基础上有更高级的交互式IPython工具,可以说这让数据分析变得更加方便了,博主推荐使用Jupyter notebook,非常好用,谁用谁知道,如果不知道怎么用,可以参考下面教程快速入门。

如何使用Python进行数据分析?

使用Python做数据分析,首先需要学会使用numpypandas包,因为它是Python数据分析的核心工具。numpy主要解决一些数学计算,矩阵变换,线性代数等问题,pandas更像是一张excel表,有行列定义,字段定义,以及数据变换和预处理等操作。两个计算包非常强大,pandas包自己就有两千多个方法,但是别慌,我们只要掌握核心方法就可以了。关于如何学习numpy和pandas,博主后续也会不断分享介绍,但是这里先贴出两张numpy和pandas学习的思维导图,总结的非常好。

  • numpy学习思维导图

  • pandas学习思维导图

除此之外,推荐一本特别好的Python数据分析书籍:利用Python进行数据分析,这本书是入门Python数据分析非常好的书籍,从numpy,pandas,数据预处理,数据重塑合并,数据变换等各种关于数据的操作,最后还介绍了Python的时间序列用法以及在金融领域上的应用。

另一本推荐的数据分析书籍是:深入浅出数据分析,这本书使用图表示意比较多,内容也很丰富,也是不错的参考资料。

Python数据可视化

Python的数据可视化工具是matplotlibmatplotlib的功能也十分强大,将它使用好会让你的数据可视化美观清晰,吸人眼球。另外一个可视化工具是seaborn,它是在matplotlib基础上封装的更高级的可视化工具,使用方便,图表非常美观,并有FaceGridPairPlotheatmap等强大的复合型可视化方法。

这两个工具的官方网站都有详细的使用说明,但如果你想快速学习核心使用方法也可以参考下面几个教程:

爬虫和机器学习

好多朋友问:数据分析岗位要求会爬虫吗?要求会机器学习吗?

首先说爬虫。其实说实话,对于数据分析而言,爬虫真不是必须的,因为一般的大公司都有专门的爬虫团队。数据分析只是将数据从数据库取出然后做数据处理和分析。不过,爬虫作为一项技能是可以在一定程度上加分的,起码在博主的面试经历中是这样的。博主之前分享过一系列爬虫技术的文章,感兴趣的朋友也可以在后台学习资源中找到,这里不赘述了。

其次是机器学习。对于机器学习,博主想说这部分还是有必要了解一下的(不是必须),因为一是可以给自己加分,另外也可以让自己清楚未来的职业方向。数据分析的发展方向一般有BI商业方向,行业分析业务方向,和机器学习数据挖掘方向。了解常用的监督和非监督模型,如朴素贝叶斯,决策树,聚类等可以让自己更加深刻得理解数据分析。

机器学习的书籍推荐:《统计学习方法》《机器学习》《机器学习实战》 三本书。

李航的统计学方法和周志华的机器学习(西瓜书)是大家最为熟知,最经典的书籍资源,两本书主要介绍机器学习的统计理论知识和公式推导,比较难啃,对于初学者其实并不建议花费大量时间深究。因为机器学习涉及的东西很多很杂,对于数学要有很强的功底,所以并不是短时间内可以全部掌握的。对于转行人员来说,时间是很宝贵的,因此博主建议这两本书可以作为参考,但不必盲目深入研究。而对于已经从事本行业的人员,这两本书无疑是最绝佳的参考资料,可以反复阅读。

机器学习实战这本书从实际应用的角度出发,更多的介绍了机器学习编程方面的使用,并附有大量源码分析,是非常具有特色的一本参考书籍,比较适合初始学习机器学习的人员。当然还有很多其它的参考资料,比如台大林轩田,Andrew Ng机器学习视频也是非常好的教学资源。

博主的建议是:先从宏观上了解各个模型的特征,优缺点及主要的应用,然后再慢慢由浅入深的学习各个模型算法的缘由和推导,因为这样不但会逐渐建立信心,也会对模型算法有更深刻的理解。总的来说,几本书各有特色,相辅相成,建议结合几本书一起学习效果最佳。当然,关于机器学习这部分,博主后面也会陆续给大家介绍。

资源获取方式

以上就是博主对于数据分析学习方法和资源的一些分享。本篇推荐的参考资料和书籍已经打包,可以通过关注微信公众号Python数据科学扫描我的二维码获取。

查看原文

赞 88 收藏 69 评论 3

louis110 关注了专栏 · 6月8日

Python数据科学

微信公众号:Python数据科学

关注 3072

louis110 收藏了文章 · 6月8日

Python爬虫实战之(五)| 模拟登录wechat

作者:xiaoyu
微信公众号:Python数据科学
知乎:Python数据分析师


不知何时,微信已经成为我们不可缺少的一部分了,我们的社交圈、关注的新闻或是公众号、还有个人信息或是隐私都被绑定在了一起。既然它这么重要,如果我们可以利用爬虫模拟登录,是不是就意味着我们可以获取这些信息,甚至可以根据需要来对它们进行有效的查看和管理。是的,没错,这完全可以。本篇博主将会给大家分享一下如何模拟登录网页版的微信,并展示模拟登录后获取的好友列表信息

微信模拟登录的过程比较复杂,当然不管怎么样方法都是万变不离其宗,我们还是使用fiddler抓包工具来模拟登录的过程。
好了,下面让我们一步一步的详细讲解一下如何实现的这个复杂的过程。

用fiddler模拟登录的请求

首先,我们在浏览器上打开微信网页版(fiddler已经在这之前打开了),然后我们会看到一个二维码的界面。

clipboard.png

然后我们使用手机微信扫描并确认,这时候网页版的微信就登陆了。

好,我们去看看fiddler都给我们抓取了什么信息包。由于过程中发出的请求有点多,这里把抓包按操作进行分解并逐一分析。

1.打开微信网页

这一步骤的抓包是这样的,发现其中login.wx.qq.com的两个链接是我们需要的。

clipboard.png

于是点开详细分析一下。

第一个链接如下,是一个get请求,可以看到uri中携带了一些参数appid、redirect_uri、fun、lang、_

GET /jslogin?appid=wx782c26e4  c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1520350213674 HTTP/1.1

clipboard.png

经过多次抓取发现appid、redirect_uri、fun、lang参数都是固定的,而_是一串变化的数字,我们在之前模拟京东商城的文章提过,它其实是一个时间戳,如果不清楚可以回顾一下Python爬虫实战之(四)| 模拟登录京东商城

知道这些参数,模拟get发送出去就可以了。那么我们为什么要模拟这一步呢?

是因为访问这个链接会有如下的响应,而其中有我们后续需要的重要信息uuid(后面步骤会提到)。

window.QRLogin.code = 200; window.QRLogin.uuid = "Idf_QdW1OQ==";

2.模拟获取二维码

微信网页提供的登录方式是扫码,我们模拟也无法避开,因此也要进行扫码验证。回到浏览器,使用开发者工具可以轻松找到二维码的链接。

clipboard.png
https://login.weixin.qq.com/q...

我们发现最后的字符串是变化的。等等,它和uuid一模一样的。没错,它就是uuid,用来保证二维码的唯一性。

因此,我们将上面提取的uuid拼接到后面就可以得到二维码图片了,然后进行扫码确认操作。

3.识别登录状态

为了识别扫码是否成功,这个步骤我们需要用到上面提到的第二个链接。

GET /cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=Idf_QdW8OQ==&tip=1&r=68288473&_=1520050213675 HTTP/1.1

这个链接也是个get请求,同样携带了一些参数。

clipboard.png

实际上在抓包过程发现只要我们不扫描二维码,这个链接就会一直重复发送直到二维码被扫描或者超时。

那么我们如何判断二维码是否被扫描或者已经登陆了呢?

还是通过响应的数据来进行判断的。经分析发现如果二维码一直没被扫,那么响应是这样的:

window.code=408;

但是如果二维码被扫描了,响应是这样的:

window.code=201;window.userAvatar = .....
window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_uoBs@qrticket_0&uuid=gbJqPdkNSQ==&lang=zh_CN&scan=1520353803";

code=201说明二维码被扫描成功了。
code=200说明是登录成功了。

4.登录

扫描了二维码之后,fiddler上会多出几个新的请求。

clipboard.png

你可能发现了,上一步骤中code=200后面有个重定向的uri,这个uri就是此步骤中跳转的登录链接。

GET https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_udBs@qrticket_0&uuid=gbJqPdfNSQ==&lang=zh_CN&scan=1520353803&fun=new&version=v2 HTTP/1.1

通过上一步骤识别登录成功的响应我们可以得到响应里面的所有参数。没错,这些参数正好可以用在正式登录(即跳转链接)的请求中。于是我们利用这些参数再进行一次get请求。携带参数如下:

clipboard.png

当然,这个登录请求同样也会返回一些响应代码,响应代码如下:

<error>
     <ret>0</ret>
     <message>OK</message>
     <skey>xxx</skey>
     <wxsid>xxx</wxsid>
     <wxuin>xxx</wxuin>
     <pass_ticket>xxx</pass_ticket>
     <isgrayscale>1</isgrayscale>
</error>

又是一堆参数,简直没完没了啊。别着急,我们已经接近成功了。获取这个响应我们一样需要将其中的参数全部提取出来供下一请求使用。

5.初始化同步

好了,终于到了最后一步了,就是微信的初始化和同步的请求了,初始化信息链接如下:

POST https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=64629109&pass_ticket=4dU5IS9EqtXt5cIV2Gni1tKG7m2V56PXk5XI%252BdjdrIk%253D HTTP/1.1

contact联系链接如下:

GET https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket=4dU5IS9EqtXt5cIV2Gni1tKG7m2V56PXk5XI%252BdjdrIk%253D&r=1520353806102&seq=0&skey=@crypt_a82dd73a_3885c878ae2f4590f7b2b5ee949dd1bd HTTP/1.1

uri中参数pass_ticket,skey在上一步的响应中已获取,直接发送请求即可完成。从这两个链接的响应中,我们就可以得到一些真实有用的信息了。

还有一个同步的请求链接,所需参数可以从上面两个链接响应中提取。但是至此我们通过上面两个链接已经可以获取我们想要的信息,因此可以不必请求这个同步链接。

GEThttps://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=1520353806125&skey=%40crypt_a82dd73a_3885c878ae2f4590f7b2b5ee949dd1bd&sid=O2Se5s2LJzPebME2&uin=254891255&deviceid=e289448639092966&synckey=1_694936977%7C2_694936979%7C3_694936982%7C1000_1520324882&_=1520353793581 HTTP/1.1

基本的登录过程就是这样,有点复杂,博主总结了个流程图供参考。

clipboard.png

代码实现

请求模拟使用requests模块完成,解析使用re。这里需要注意一下,如果运行一直报ssl的错,可以在request请求里面加上了verify=False跳过证书认证来解决。

clipboard.png

1.初始化参数

def __init__(self):
    self.session = requests.session()
    self.headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 5.1; rv:33.0) Gecko/20100101 Firefox/33.0'}
    self.QRImgPath = os.path.split(os.path.realpath(__file__))[0] + os.sep + 'webWeixinQr.jpg'
    self.uuid = ''
    self.tip = 0
    self.base_uri = ''
    self.redirect_uri = ''
    self.skey = ''
    self.wxsid = ''
    self.wxuin = ''
    self.pass_ticket = ''
    self.deviceId = 'e000000000000000'
    self.BaseRequest = {}
    self.ContactList = []
    self.My = []
    self.SyncKey = ''

定义一个类,初始化实例的所有请求参数,定义二维码的路径。

2.请求uuid

def getUUID(self):
    url = 'https://login.weixin.qq.com/jslogin'
    params = {
        'appid': 'wx782c26e4c19acffb',
        'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage',
        'fun': 'new',
        'lang': 'zh_CN',
        '_': int(time.time() * 1000),  # 时间戳
    }
    response = self.session.get(url, params=params)
    target = response.content.decode('utf-8')
    pattern = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
    ob = re.search(pattern, target)  # 正则提取uuid
    code = ob.group(1)
    self.uuid = ob.group(2)
    if code == '200':  # 判断请求是否成功
        return True
    return False

使用正则对相应进行提取获取uuid,通过code判断请求是否成功,响应如下:

window.QRLogin.code = 200; window.QRLogin.uuid = "Idf_QdW1OQ==";

3.模拟获取二维码

def showQRImage(self):
    url = 'https://login.weixin.qq.com/qrcode/' + self.uuid
    response = self.session.get(url)
    self.tip = 1
    with open(self.QRImgPath, 'wb') as f:
        f.write(response.content)
        f.close()
    # 打开二维码
    if sys.platform.find('darwin') >= 0:
        subprocess.call(['open', self.QRImgPath])  # 苹果系统
    elif sys.platform.find('linux') >= 0:
        subprocess.call(['xdg-open', self.QRImgPath])  # linux系统
    else:
        os.startfile(self.QRImgPath)  # windows系统
    print('请使用微信扫描二维码登录')

使用uuid请求二维码图片,并根据操作系统自动打开。

4.识别登录状态

def checkLogin(self):
    url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % (
        self.tip, self.uuid, int(time.time() * 1000))
    response = self.session.get(url)
    target = response.content.decode('utf-8')
    pattern = r'window.code=(\d+);'
    ob = re.search(pattern, target)
    code = ob.group(1)
    if code == '201':  # 已扫描
        print('成功扫描,请在手机上点击确认登录')
        self.tip = 0
    elif code == '200':  # 已登录
        print('正在登录中...')
        regx = r'window.redirect_uri="(\S+?)";'
        ob = re.search(regx, target)
        self.redirect_uri = ob.group(1) + '&fun=new'
        self.base_uri = self.redirect_uri[:self.redirect_uri.rfind('/')]
    elif code == '408':  # 超时
        pass
    return code

响应如下:

window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_uoBs@qrticket_0&

根据响应中的code代码识别登录状态。
408:超时
201:已扫描
200:已登录

5.登录

def login(self):
    response = self.session.get(self.redirect_uri, verify=False)
    data = response.content.decode('utf-8')
    doc = xml.dom.minidom.parseString(data)
    root = doc.documentElement
    # 提取响应中的参数
    for node in root.childNodes:
        if node.nodeName == 'skey':
            self.skey = node.childNodes[0].data
        elif node.nodeName == 'wxsid':
            self.wxsid = node.childNodes[0].data
        elif node.nodeName == 'wxuin':
            self.wxuin = node.childNodes[0].data
        elif node.nodeName == 'pass_ticket':
            self.pass_ticket = node.childNodes[0].data
    if not all((self.skey, self.wxsid, self.wxuin, self.pass_ticket)):
        return False
    self.BaseRequest = {
        'Uin': int(self.wxuin),
        'Sid': self.wxsid,
        'Skey': self.skey,
        'DeviceID': self.deviceId,
    }
    return True

请求跳转的登录链接,提取响应代码参数,响应如下:

<error>
    <ret>0</ret>
    <message>OK</message>
    <skey>xxx</skey>
    <wxsid>xxx</wxsid>
    <wxuin>xxx</wxuin>
    <pass_ticket>xxx</pass_ticket>
    <isgrayscale>1</isgrayscale>
</error>

6.初始化获取信息

def webwxinit(self):
    url = self.base_uri + \
          '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % (
              self.pass_ticket, self.skey, int(time.time() * 1000))
    params = {
        'BaseRequest': self.BaseRequest
    }
    h = self.headers
    h['ContentType'] = 'application/json; charset=UTF-8'
    response = self.session.post(url, data=json.dumps(params), headers=h, verify=False)
    data = response.content.decode('utf-8')
    print(data)
    dic = json.loads(data)
    self.ContactList = dic['ContactList']
    self.My = dic['User']
    SyncKeyList = []
    for item in dic['SyncKey']['List']:
        SyncKeyList.append('%s_%s' % (item['Key'], item['Val']))
    self.SyncKey = '|'.join(SyncKeyList)
    ErrMsg = dic['BaseResponse']['ErrMsg']
    Ret = dic['BaseResponse']['Ret']
    if Ret != 0:
        return False
    return True

请求初始化的链接,获取初始化响应数据。

def webwxgetcontact(self):
    url = self.base_uri + \
          '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (
              self.pass_ticket, self.skey, int(time.time()))
    h = self.headers
    h['ContentType'] = 'application/json; charset=UTF-8'
    response = self.session.get(url, headers=h, verify=False)
    data = response.content.decode('utf-8')
    # print(data)
    dic = json.loads(data)
    MemberList = dic['MemberList']
    # 倒序遍历,不然删除的时候出问题..
    SpecialUsers = ["newsapp", "fmessage", "filehelper", "weibo", "qqmail", "tmessage", "qmessage", "qqsync",
                    "floatbottle", "lbsapp", "shakeapp", "medianote", "qqfriend", "readerapp", "blogapp",
                    "facebookapp", "masssendapp",
                    "meishiapp", "feedsapp", "voip", "blogappweixin", "weixin", "brandsessionholder",
                    "weixinreminder", "wxid_novlwrv3lqwv11", "gh_22b87fa7cb3c", "officialaccounts",
                    "notification_messages", "wxitil", "userexperience_alarm"]
    for i in range(len(MemberList) - 1, -1, -1):
        Member = MemberList[i]
        if Member['VerifyFlag'] & 8 != 0:  # 公众号/服务号
            MemberList.remove(Member)
        elif Member['UserName'] in SpecialUsers:  # 特殊账号
            MemberList.remove(Member)
        elif Member['UserName'].find('@@') != -1:  # 群聊
            MemberList.remove(Member)
        elif Member['UserName'] == self.My['UserName']:  # 自己
            MemberList.remove(Member)
    return MemberList

请求contact的链接,获取联系人、公众号、群聊以及个人信息。响应代码为json格式,如下:

{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
}
,
"Count": 11,
"ContactList": [{
"Uin": 0,
"UserName": "filehelper",
"NickName": "文件传输助手",
"HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=621637626&username=filehelper&skey=@crypt_a82dd73a_7e8e1054c011e8d71d0b542f39c7db85",
"ContactFlag": 3,
"MemberCount": 0,
"MemberList": [],
"RemarkName": "",
"HideInputBarFlag": 0,
"Sex": 0,
"Signature": "",
"VerifyFlag": 0,
"OwnerUin": 0,
"PYInitial": "WJCSZS",
"PYQuanPin": "wenjianchuanshuzhushou",
"RemarkPYInitial": "",
"RemarkPYQuanPin": "",
"StarFriend": 0,
"AppAccountFlag": 0,
"Statues": 0,
"AttrStatus": 0,
"Province": "",
"City": "",
"Alias": "",
"SnsFlag": 0,
"UniFriend": 0,
"DisplayName": "",
"ChatRoomId": 0,
"KeyWord": "fil",
"EncryChatRoomId": "",
"IsOwner": 0
}
,{...}
...

根据响应中字段信息做信息操作,这里是获取好友列表,所以将其它字段如公众号、群聊、自己都去掉了,只保留好友信息。

7.主函数运行

def main(self):
    if not self.getUUID():
        print('获取uuid失败')
        return
    self.showQRImage()
    time.sleep(1)
    while self.checkLogin() != '200':
        pass
    os.remove(self.QRImgPath)
    if not self.login():
        print('登录失败')
        return
    # 登录完成, 下面查询好友
    if not self.webwxinit():
        print('初始化失败')
        return
    MemberList = self.webwxgetcontact()
    print('通讯录共%s位好友' % len(MemberList))
    for x in MemberList:
        sex = '未知' if x['Sex'] == 0 else '男' if x['Sex'] == 1 else '女'
        print('昵称:%s, 性别:%s, 备注:%s, 签名:%s' % (x['NickName'], sex, x['RemarkName'], x['Signature']))

模拟登录结果

好友列表如下:

clipboard.png

clipboard.png

当然,好友列表只是个例子,我们也可以对其它信息进行查看和管理或者数据分析。

总结

本篇与大家分享了网页版微信的模拟登录过程。尽管过程中请求多有点复杂,但是只要我们仔细分析还是可以一步一步实现的,希望对大家有帮助,代码已上传到github:https://github.com/xiaoyusmd/...

完毕。


关注微信公众号Python数据科学,获取 120G 人工智能 学习资料。
图片描述
图片描述

查看原文

louis110 赞了文章 · 6月8日

Python爬虫实战之(五)| 模拟登录wechat

作者:xiaoyu
微信公众号:Python数据科学
知乎:Python数据分析师


不知何时,微信已经成为我们不可缺少的一部分了,我们的社交圈、关注的新闻或是公众号、还有个人信息或是隐私都被绑定在了一起。既然它这么重要,如果我们可以利用爬虫模拟登录,是不是就意味着我们可以获取这些信息,甚至可以根据需要来对它们进行有效的查看和管理。是的,没错,这完全可以。本篇博主将会给大家分享一下如何模拟登录网页版的微信,并展示模拟登录后获取的好友列表信息

微信模拟登录的过程比较复杂,当然不管怎么样方法都是万变不离其宗,我们还是使用fiddler抓包工具来模拟登录的过程。
好了,下面让我们一步一步的详细讲解一下如何实现的这个复杂的过程。

用fiddler模拟登录的请求

首先,我们在浏览器上打开微信网页版(fiddler已经在这之前打开了),然后我们会看到一个二维码的界面。

clipboard.png

然后我们使用手机微信扫描并确认,这时候网页版的微信就登陆了。

好,我们去看看fiddler都给我们抓取了什么信息包。由于过程中发出的请求有点多,这里把抓包按操作进行分解并逐一分析。

1.打开微信网页

这一步骤的抓包是这样的,发现其中login.wx.qq.com的两个链接是我们需要的。

clipboard.png

于是点开详细分析一下。

第一个链接如下,是一个get请求,可以看到uri中携带了一些参数appid、redirect_uri、fun、lang、_

GET /jslogin?appid=wx782c26e4  c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1520350213674 HTTP/1.1

clipboard.png

经过多次抓取发现appid、redirect_uri、fun、lang参数都是固定的,而_是一串变化的数字,我们在之前模拟京东商城的文章提过,它其实是一个时间戳,如果不清楚可以回顾一下Python爬虫实战之(四)| 模拟登录京东商城

知道这些参数,模拟get发送出去就可以了。那么我们为什么要模拟这一步呢?

是因为访问这个链接会有如下的响应,而其中有我们后续需要的重要信息uuid(后面步骤会提到)。

window.QRLogin.code = 200; window.QRLogin.uuid = "Idf_QdW1OQ==";

2.模拟获取二维码

微信网页提供的登录方式是扫码,我们模拟也无法避开,因此也要进行扫码验证。回到浏览器,使用开发者工具可以轻松找到二维码的链接。

clipboard.png
https://login.weixin.qq.com/q...

我们发现最后的字符串是变化的。等等,它和uuid一模一样的。没错,它就是uuid,用来保证二维码的唯一性。

因此,我们将上面提取的uuid拼接到后面就可以得到二维码图片了,然后进行扫码确认操作。

3.识别登录状态

为了识别扫码是否成功,这个步骤我们需要用到上面提到的第二个链接。

GET /cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=Idf_QdW8OQ==&tip=1&r=68288473&_=1520050213675 HTTP/1.1

这个链接也是个get请求,同样携带了一些参数。

clipboard.png

实际上在抓包过程发现只要我们不扫描二维码,这个链接就会一直重复发送直到二维码被扫描或者超时。

那么我们如何判断二维码是否被扫描或者已经登陆了呢?

还是通过响应的数据来进行判断的。经分析发现如果二维码一直没被扫,那么响应是这样的:

window.code=408;

但是如果二维码被扫描了,响应是这样的:

window.code=201;window.userAvatar = .....
window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_uoBs@qrticket_0&uuid=gbJqPdkNSQ==&lang=zh_CN&scan=1520353803";

code=201说明二维码被扫描成功了。
code=200说明是登录成功了。

4.登录

扫描了二维码之后,fiddler上会多出几个新的请求。

clipboard.png

你可能发现了,上一步骤中code=200后面有个重定向的uri,这个uri就是此步骤中跳转的登录链接。

GET https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_udBs@qrticket_0&uuid=gbJqPdfNSQ==&lang=zh_CN&scan=1520353803&fun=new&version=v2 HTTP/1.1

通过上一步骤识别登录成功的响应我们可以得到响应里面的所有参数。没错,这些参数正好可以用在正式登录(即跳转链接)的请求中。于是我们利用这些参数再进行一次get请求。携带参数如下:

clipboard.png

当然,这个登录请求同样也会返回一些响应代码,响应代码如下:

<error>
     <ret>0</ret>
     <message>OK</message>
     <skey>xxx</skey>
     <wxsid>xxx</wxsid>
     <wxuin>xxx</wxuin>
     <pass_ticket>xxx</pass_ticket>
     <isgrayscale>1</isgrayscale>
</error>

又是一堆参数,简直没完没了啊。别着急,我们已经接近成功了。获取这个响应我们一样需要将其中的参数全部提取出来供下一请求使用。

5.初始化同步

好了,终于到了最后一步了,就是微信的初始化和同步的请求了,初始化信息链接如下:

POST https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=64629109&pass_ticket=4dU5IS9EqtXt5cIV2Gni1tKG7m2V56PXk5XI%252BdjdrIk%253D HTTP/1.1

contact联系链接如下:

GET https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket=4dU5IS9EqtXt5cIV2Gni1tKG7m2V56PXk5XI%252BdjdrIk%253D&r=1520353806102&seq=0&skey=@crypt_a82dd73a_3885c878ae2f4590f7b2b5ee949dd1bd HTTP/1.1

uri中参数pass_ticket,skey在上一步的响应中已获取,直接发送请求即可完成。从这两个链接的响应中,我们就可以得到一些真实有用的信息了。

还有一个同步的请求链接,所需参数可以从上面两个链接响应中提取。但是至此我们通过上面两个链接已经可以获取我们想要的信息,因此可以不必请求这个同步链接。

GEThttps://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=1520353806125&skey=%40crypt_a82dd73a_3885c878ae2f4590f7b2b5ee949dd1bd&sid=O2Se5s2LJzPebME2&uin=254891255&deviceid=e289448639092966&synckey=1_694936977%7C2_694936979%7C3_694936982%7C1000_1520324882&_=1520353793581 HTTP/1.1

基本的登录过程就是这样,有点复杂,博主总结了个流程图供参考。

clipboard.png

代码实现

请求模拟使用requests模块完成,解析使用re。这里需要注意一下,如果运行一直报ssl的错,可以在request请求里面加上了verify=False跳过证书认证来解决。

clipboard.png

1.初始化参数

def __init__(self):
    self.session = requests.session()
    self.headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 5.1; rv:33.0) Gecko/20100101 Firefox/33.0'}
    self.QRImgPath = os.path.split(os.path.realpath(__file__))[0] + os.sep + 'webWeixinQr.jpg'
    self.uuid = ''
    self.tip = 0
    self.base_uri = ''
    self.redirect_uri = ''
    self.skey = ''
    self.wxsid = ''
    self.wxuin = ''
    self.pass_ticket = ''
    self.deviceId = 'e000000000000000'
    self.BaseRequest = {}
    self.ContactList = []
    self.My = []
    self.SyncKey = ''

定义一个类,初始化实例的所有请求参数,定义二维码的路径。

2.请求uuid

def getUUID(self):
    url = 'https://login.weixin.qq.com/jslogin'
    params = {
        'appid': 'wx782c26e4c19acffb',
        'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage',
        'fun': 'new',
        'lang': 'zh_CN',
        '_': int(time.time() * 1000),  # 时间戳
    }
    response = self.session.get(url, params=params)
    target = response.content.decode('utf-8')
    pattern = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
    ob = re.search(pattern, target)  # 正则提取uuid
    code = ob.group(1)
    self.uuid = ob.group(2)
    if code == '200':  # 判断请求是否成功
        return True
    return False

使用正则对相应进行提取获取uuid,通过code判断请求是否成功,响应如下:

window.QRLogin.code = 200; window.QRLogin.uuid = "Idf_QdW1OQ==";

3.模拟获取二维码

def showQRImage(self):
    url = 'https://login.weixin.qq.com/qrcode/' + self.uuid
    response = self.session.get(url)
    self.tip = 1
    with open(self.QRImgPath, 'wb') as f:
        f.write(response.content)
        f.close()
    # 打开二维码
    if sys.platform.find('darwin') >= 0:
        subprocess.call(['open', self.QRImgPath])  # 苹果系统
    elif sys.platform.find('linux') >= 0:
        subprocess.call(['xdg-open', self.QRImgPath])  # linux系统
    else:
        os.startfile(self.QRImgPath)  # windows系统
    print('请使用微信扫描二维码登录')

使用uuid请求二维码图片,并根据操作系统自动打开。

4.识别登录状态

def checkLogin(self):
    url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % (
        self.tip, self.uuid, int(time.time() * 1000))
    response = self.session.get(url)
    target = response.content.decode('utf-8')
    pattern = r'window.code=(\d+);'
    ob = re.search(pattern, target)
    code = ob.group(1)
    if code == '201':  # 已扫描
        print('成功扫描,请在手机上点击确认登录')
        self.tip = 0
    elif code == '200':  # 已登录
        print('正在登录中...')
        regx = r'window.redirect_uri="(\S+?)";'
        ob = re.search(regx, target)
        self.redirect_uri = ob.group(1) + '&fun=new'
        self.base_uri = self.redirect_uri[:self.redirect_uri.rfind('/')]
    elif code == '408':  # 超时
        pass
    return code

响应如下:

window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_uoBs@qrticket_0&

根据响应中的code代码识别登录状态。
408:超时
201:已扫描
200:已登录

5.登录

def login(self):
    response = self.session.get(self.redirect_uri, verify=False)
    data = response.content.decode('utf-8')
    doc = xml.dom.minidom.parseString(data)
    root = doc.documentElement
    # 提取响应中的参数
    for node in root.childNodes:
        if node.nodeName == 'skey':
            self.skey = node.childNodes[0].data
        elif node.nodeName == 'wxsid':
            self.wxsid = node.childNodes[0].data
        elif node.nodeName == 'wxuin':
            self.wxuin = node.childNodes[0].data
        elif node.nodeName == 'pass_ticket':
            self.pass_ticket = node.childNodes[0].data
    if not all((self.skey, self.wxsid, self.wxuin, self.pass_ticket)):
        return False
    self.BaseRequest = {
        'Uin': int(self.wxuin),
        'Sid': self.wxsid,
        'Skey': self.skey,
        'DeviceID': self.deviceId,
    }
    return True

请求跳转的登录链接,提取响应代码参数,响应如下:

<error>
    <ret>0</ret>
    <message>OK</message>
    <skey>xxx</skey>
    <wxsid>xxx</wxsid>
    <wxuin>xxx</wxuin>
    <pass_ticket>xxx</pass_ticket>
    <isgrayscale>1</isgrayscale>
</error>

6.初始化获取信息

def webwxinit(self):
    url = self.base_uri + \
          '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % (
              self.pass_ticket, self.skey, int(time.time() * 1000))
    params = {
        'BaseRequest': self.BaseRequest
    }
    h = self.headers
    h['ContentType'] = 'application/json; charset=UTF-8'
    response = self.session.post(url, data=json.dumps(params), headers=h, verify=False)
    data = response.content.decode('utf-8')
    print(data)
    dic = json.loads(data)
    self.ContactList = dic['ContactList']
    self.My = dic['User']
    SyncKeyList = []
    for item in dic['SyncKey']['List']:
        SyncKeyList.append('%s_%s' % (item['Key'], item['Val']))
    self.SyncKey = '|'.join(SyncKeyList)
    ErrMsg = dic['BaseResponse']['ErrMsg']
    Ret = dic['BaseResponse']['Ret']
    if Ret != 0:
        return False
    return True

请求初始化的链接,获取初始化响应数据。

def webwxgetcontact(self):
    url = self.base_uri + \
          '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (
              self.pass_ticket, self.skey, int(time.time()))
    h = self.headers
    h['ContentType'] = 'application/json; charset=UTF-8'
    response = self.session.get(url, headers=h, verify=False)
    data = response.content.decode('utf-8')
    # print(data)
    dic = json.loads(data)
    MemberList = dic['MemberList']
    # 倒序遍历,不然删除的时候出问题..
    SpecialUsers = ["newsapp", "fmessage", "filehelper", "weibo", "qqmail", "tmessage", "qmessage", "qqsync",
                    "floatbottle", "lbsapp", "shakeapp", "medianote", "qqfriend", "readerapp", "blogapp",
                    "facebookapp", "masssendapp",
                    "meishiapp", "feedsapp", "voip", "blogappweixin", "weixin", "brandsessionholder",
                    "weixinreminder", "wxid_novlwrv3lqwv11", "gh_22b87fa7cb3c", "officialaccounts",
                    "notification_messages", "wxitil", "userexperience_alarm"]
    for i in range(len(MemberList) - 1, -1, -1):
        Member = MemberList[i]
        if Member['VerifyFlag'] & 8 != 0:  # 公众号/服务号
            MemberList.remove(Member)
        elif Member['UserName'] in SpecialUsers:  # 特殊账号
            MemberList.remove(Member)
        elif Member['UserName'].find('@@') != -1:  # 群聊
            MemberList.remove(Member)
        elif Member['UserName'] == self.My['UserName']:  # 自己
            MemberList.remove(Member)
    return MemberList

请求contact的链接,获取联系人、公众号、群聊以及个人信息。响应代码为json格式,如下:

{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
}
,
"Count": 11,
"ContactList": [{
"Uin": 0,
"UserName": "filehelper",
"NickName": "文件传输助手",
"HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=621637626&username=filehelper&skey=@crypt_a82dd73a_7e8e1054c011e8d71d0b542f39c7db85",
"ContactFlag": 3,
"MemberCount": 0,
"MemberList": [],
"RemarkName": "",
"HideInputBarFlag": 0,
"Sex": 0,
"Signature": "",
"VerifyFlag": 0,
"OwnerUin": 0,
"PYInitial": "WJCSZS",
"PYQuanPin": "wenjianchuanshuzhushou",
"RemarkPYInitial": "",
"RemarkPYQuanPin": "",
"StarFriend": 0,
"AppAccountFlag": 0,
"Statues": 0,
"AttrStatus": 0,
"Province": "",
"City": "",
"Alias": "",
"SnsFlag": 0,
"UniFriend": 0,
"DisplayName": "",
"ChatRoomId": 0,
"KeyWord": "fil",
"EncryChatRoomId": "",
"IsOwner": 0
}
,{...}
...

根据响应中字段信息做信息操作,这里是获取好友列表,所以将其它字段如公众号、群聊、自己都去掉了,只保留好友信息。

7.主函数运行

def main(self):
    if not self.getUUID():
        print('获取uuid失败')
        return
    self.showQRImage()
    time.sleep(1)
    while self.checkLogin() != '200':
        pass
    os.remove(self.QRImgPath)
    if not self.login():
        print('登录失败')
        return
    # 登录完成, 下面查询好友
    if not self.webwxinit():
        print('初始化失败')
        return
    MemberList = self.webwxgetcontact()
    print('通讯录共%s位好友' % len(MemberList))
    for x in MemberList:
        sex = '未知' if x['Sex'] == 0 else '男' if x['Sex'] == 1 else '女'
        print('昵称:%s, 性别:%s, 备注:%s, 签名:%s' % (x['NickName'], sex, x['RemarkName'], x['Signature']))

模拟登录结果

好友列表如下:

clipboard.png

clipboard.png

当然,好友列表只是个例子,我们也可以对其它信息进行查看和管理或者数据分析。

总结

本篇与大家分享了网页版微信的模拟登录过程。尽管过程中请求多有点复杂,但是只要我们仔细分析还是可以一步一步实现的,希望对大家有帮助,代码已上传到github:https://github.com/xiaoyusmd/...

完毕。


关注微信公众号Python数据科学,获取 120G 人工智能 学习资料。
图片描述
图片描述

查看原文

赞 32 收藏 79 评论 15

louis110 赞了文章 · 6月7日

微信应用号(小程序)资源汇总(1010更新)

wechat-weapp-resource

微信应用号(小程序)资源汇总。 每天不定期整理和收集微信小程序相关资源,方便查阅和学习,欢迎大家提交新的资源,完善和补充。

关注公众号,获取资源

QQ群:496528912

目录

微信小应用资源汇总整理

开源项目

微信官方资料(0924)

微信官方文档和开发工具已开发,不需要再用破解开发工具,直接使用官方开发工具就好。

工具文档

资料

教程

讨论

文章

看法观点

视频

Q&A


欢迎投稿:点击这里


查看原文

赞 38 收藏 131 评论 3

louis110 关注了专栏 · 6月7日

Stone的进击之路

每天都要好心情!

关注 15

louis110 赞了文章 · 6月7日

微信下调试H5页面

一、Android端微信

1、首先打开,http://debugx5.qq.com 或者扫描下面二维码
clipboard.png

2、打开微信 TBS 调试

clipboard.png

3、然后在谷歌浏览器地址栏输入chrome://inspect/#devices出现如下界面

clipboard.png

4、点击想要调试的页面下的inspect即可

二、iOS端微信没找到腾讯官方调试工具,发现了这个神器

https://github.com/wuchangmin...

查看原文

赞 7 收藏 2 评论 0

louis110 发布了文章 · 2016-03-11

GraphQL and Relay 浅析

Facebook 在去年夏天公布了 GraphQL,就像往前端深潭砸下了一颗巨石,人们都被水声吸引到了湖边,观望是否会出现什么,有些人期待,有些人猜疑。过了半年多,社区已经慢慢的摸清这个石头的材质,本文希望在你入门 GraphQL 和 Relay 的过程中能帮你清除一些障碍。

GraphQL

GraphQL 是在 Facebook 内部应用多年的一套数据查询语言和 runtime。
初次入门者建议先把官网的资料都读一遍,难度不大(specification 和 API 可以后面再看)。

GraphQL 包括什么

  1. 类型系统 - GraphQL 是强类型语言,强类型虽然写时会稍微累点,但就不用写一堆类型检测的代码了;

  2. 验证 - GraphQL 提供机制对你的语法和请求做一定层度的校验;

  3. introspection - 一个让你能通过几行代码就能了解整个资源提供方的细节的 API。

GraphQL 优势

官网已经列举了,我用更简练的语言描述下。

GraphQL 与 REST

同类型协议目前最出名的是 REST,特点是资源可定位,使用 HTTP verbs。REST 具体应该怎么写有很多争议,但简单的例子是没有争议的:

GET /users/1

REST 优点是简单明了,缺点也是太简单明了,导致语法可扩充性不强。
我们来看看 GraphQL 官网是怎么和 REST 对比的:

语法灵活

GraphQL 只需要一次请求就能够获得你所有想要的资源。这里举一个和 REST 对比的例子 让大家有直观的认识。

现在,我想获取id为1的用户的名字,年龄和他所有朋友的名字

GraphQL 实现的方案:

{
  user(id: 1) {
    name
    age
    friends {
      name
    }
  }
}

REST 实现的方案:

GET /users/1 and GET /users/1/friends  

GET /users/1?include=friends.name

发现区别了吗?用 REST 要不就发多次请求,要不就得用一个不方便扩展的语法。

没有冗余

日后扩充资源也没有冗余,你只会获得你想要的资源。还是用上面的例子,如果 user 多了个属性 gender 会怎么样?
在 REST 的方案中,如果客户端不变,取到的结果是会多了 gender 属性,而在 GraphQL 方案中,客户端是不会获取到 gender 属性的。

强类型

有 introspection 机制,代码即文档,方便快捷,而不需要去找这个 API 的说明文档在哪里,看个例子:
pic

自定义 schema

没必要像 REST 这样固定且通用的语法。

其他专有方案(Ad Hoc Endpoints)

和专有方案对比:

  1. 专有方案每个接口都自己定义获取数据,后端代码不能得到重用;

  2. 和 REST 对比的第二点一样;

  3. 每个接口的数据不能复用;

  4. 对比其他现有的专有方案,要么没有强类型,要么没有 GraphQL 这么昂贵,而且前面3点也还是没有解决。

与图数据库的关系

首先,介绍下什么是图数据库,可以参考neo4j的介绍,一图胜千言:
pic

pic
上边是关系数据库,下边是图数据库。

GraphQL 为什么有 Graph,是因为它的 query 是以图的形式来组织的:

user
┖-OWNS-> playlist
         ┖-CONTAINS-> track
                      ┖-LIKED_BY-> users

GraphQL 并不要求后台一定要是图数据库,关系数据库也可以,它只是一套查询数据的语言而已。

DataLoader

Dataloader 是一个小工具,帮你把你的请求转成批量请求的形式,和 GraphQL 搭配的也挺好,看个例子:

query FetchPlaylist {
  playlist(id: "e66637db-13f9-4056-abef-f731f8b1a3c7") {
    id
    name

    tracks {
      id
      title

      viewerHasLiked
    }
  }
}

这个 query 是要获取某个用户的歌单。
注意一个细节,这个 query 想获取每个 track 的一些属性。我们定义一下 Track 这个类型:

import {
  GraphQLString,
  GraphQLBoolean,
  GraphQLObjectType
} from 'graphql';

export default new GraphQLObjectType({
  name: 'Track',
  description: 'A Track',
  fields: () => ({
    id: {
      type: GraphQLString,
      resolve: it => it.uuid
    }

    title: { type: GraphQLString },

    viewerHasLiked: {
      type: GraphQLBoolean,
      resolve: (it, _, { rootValue: { ctx: { auth } } }) => (
        (auth.isAuthenticated) ? it.userHasLiked(auth.user) : null
      )
    }
  })
});

resolve 函数调用的是后端 API,注意这里的 it 就是 track 的对象。
我们获取 viewerHasLiked 这个属性需要调用 it.userHasLiked (auth.user)。那么,我的歌单里有 50 首歌的话,就要调用 50 次it.userHasLiked(auth.user),这样访问数据库的性能是无法接受的。合理的想法是变成批量的。那要怎么做呢?这就是 DataLoader 发挥作用的时候了:

import DataLoader from 'dataloader';
import BaseModel from './BaseModel';

const likeLoader = new DataLoader((requests) => {
  // requests is now a an array of [track, user] pairs.
  // Batch-load the results for those requests, reorder them to match
  // the order of requests and return.
})

export default class Track extends BaseModel {
  userHasLiked(user) {
    return likeLoader.load([this, user]);
  }
}

在一个 event loop 里每次调用 dataloader,dataloader 会记下你的请求参数,在下次 event loop 的时候把这么多次的请求参数变成一个数组提供你操作,你就可以拿这个数组对数据库执行批量的操作了。而且,它还对结果按你的请求参数进行了缓存,是居家必备的杀人利器。

安全性

或许有人有疑问,感觉 GraphQL 把我所拥有的资源全部都暴露了,别人不只一览全局,而且还能一次过全部拉下来,那还得了?
事实上,GraphQL 提供的资源不一定要和你数据库一样,因为它只是扮演中间层的角色,虽然也可能很像。所以,你要想好哪些资源可以被看。
至于获取,其实看到上面的例子里有这句 auth.isAuthenticated

可以看到你可以在里面插入权限限制的。至于获取资源太多拖垮服务器?

Jacob Gillespie 提到一些思路:

  1. 对语句做 AST 分析,太复杂的就拒绝了;

  2. 做超时限制,对容量也可以做限制;

  3. 客户端记得要做 cache(如 Relay)。

Relay

Relay 是连接 GraphQL 和 React 的一座桥梁。不过,除了让 React 认识 GraphQL 服务器之外,它还做了什么呢?

建议先把官网的资料都读一遍,Relay 相对来说比 GraphQL 复杂一些,而且文档并不详细(截至截稿时,Relay的版本是 v0.6.1),也缺失了关于 graphql-relay 库的详细介绍,扫一遍后,结合本文最后的学习资料的代码加深理解。

Relay 怎么用?

使用 Relay 是要侵入前后端的:

  • 在后端你得通过 graphql-relay-js 让 GraphQL schema 更适合 Relay;

  • 在前端再通过 react-relay 来配合 React。

Relay 包括什么?

Relay 把关于数据获取的事情都接管过来,比如说请求异常,loading,请求排队,cache,获取分页数据。我这里重点讲一下以下几个方面:

client-side cache

Relay 获取数据当然离不开 cache,可以看到 GraphQL 不再依赖 URL cache,而是按照 Graph 来 cache,最大的保证 cache 没有冗余,发最少的请求,我举一个例子:

比如下面这个请求:

query { stories { id, text } }

如果利用 URL 请求(比如说浏览器的 cache),那么这个请求下次确实命中 cache 了,那么假如我还有一个请求是:

query { story(id: "123") { id, text } }

看得出,下面这个请求获取的数据是上面请求的子集,这里有两个问题:

  • 如果第一第二两个请求获取的数据不一致怎么办?

  • 本来就是子集,为什么我还要发请求?

这两个想法催生出来了 GraphQL 的解决方案:按照 Graph 来 cache,也就是说子集不需要再发请求了,当然你也可以强制发请求来更新局部或者整个 cache。

具体做法是通过拍平数据结构(类似数据库的几个范式)来 cache 整个 Graph。

view 通过订阅他需要的每个 cache record 来更新,只要其中一个 record 更新了,也只有订阅了这个 record 的 view 才会得到更新。

最后,聊到修改,我们可以看到 mutation 有个反直觉的地方是请求的 query 里包括了需要获取的数据。为什么不直接返回你的修改影响的那些数据? 因为服务端实现这个太复杂了,有的时候一个简单的修改会影响到非常多的后台数据,而很多数据 view 是不需要知道它变化了。

所以,Relay 团队最后选择的方案是,让客户端告诉服务器端你认为哪些数据你想重新获取。具体到实现,Relay 采用的方案是获取 cache 和 fat query 有交集的部分,这样既更新了 cache,而且不在 cache 里的也不会获取。

Relay 的声明式数据获取

React 是按 Component 组织 view 的,最好的方式也是把 view 需要的数据写在 view。如果用常规的做法,view 负责自己的 Data-fetch,那么,由于 React 是一层一层的往里深入 Component 的,那么也就意味着每一层 Component 都自己发请求去了,是不可能做到用一个网络请求来获取所有数据的。

所以,Relay 通过抽象出一个 container 的概念,让每个模块提前声明自己需要的数据,Relay 会先遍历所有 container,组成 query tree,这样就达到了只使用一个网络请求的目的。

另外,通过声明式数据获取还可以更好的对组件约束,只能获取它声明的数据,并且 Relay 也可以做些验证。

graphql-relay-js

在看一些 React 和 Relay 协作的例子时,经常发现这个库的存在,这个库到底是干什么的?

通过查看源码后发现,里面其实是各种 helper 方法,负责生成一些 GraphQL 的类,为什么需要这样做?其实,这是因为 Relay 提供的一些功能(比如 ID handling,分页)需要 GraphQL 服务器提供特定的代码结构。如果你要开发一个 GraphQL 的前端,就算它基于其他框架,基于其他语言,实现一个像 graphql-relay-js 所实现的 Relay-compliant 的 server 是很有帮助的,比如graphql-go/relay

babel-relay-plugin

Relay 的 container 依赖的数据资源是通过声明的,但客户端是不知道后端的数据结构的。为了让客户端了解整个后台结构,就要引入这个 bable 插件,这个插件通过读取服务端的 schema,就可以让客户端正确理解它所需要的资源在服务端是长什么样的。

optimistic UI update

我们看下例子:

<Relay.RootContainer
  Component={ProfilePicture}
  route={profileRoute}
  renderLoading={function() {
    return <div>Loading...</div>;
  }}
  renderFailure={function(error, retry) {
    return (
      <div>
        <p>{error.message}</p>
        <p><button onClick={retry}>Retry?</button></p>
      </div>
    );
  }}
/>

可以看到在 Relay 里可以很简单的处理请求整个请求过程中的 UI 变化。

总结

相信阅读本文的读者都是对这两者有一定兴趣的人,但在我上手之后,我的心情是复杂的。GraphQL 和 Relay 带来了一些优势,最重要的是可以一次性获取资源,看上去是未来之路,但这优势其实用些不优雅的方法来解决也没什么问题,但为了这些优势需要编写大量与业务逻辑无关的代码,让我真心忧虑它的路能走多远,相信看过一个官方的 TODOList的例子 的入门者很容易就能感觉到。REST 如此简单,普及开来尚且用了几年,复杂好多倍的 GraphQL 的未来还任重而道远。

学习资料

相关的库

  • server:比如 express-graphql。

  • ORM:比如 graffiti。

  • facebook/dataloader。

  • adrenaline:React bindings for Redux with Relay。

  • react-router-relay:结合 react-router,介绍

  • graphql-relay-js

  • babel-relay-plugin

查看原文

赞 14 收藏 38 评论 4

认证与成就

  • 获得 14 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-03-11
个人主页被 255 人浏览