做为数据交换语言的json,怎么用可以最轻松?

json是一种轻量级的数据交换格式,它属于ECMAScript (欧洲计算机协会制定的js规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得json成为理想的数据交换语言。

这次,我们将眼光聚焦到json,针对json介绍一系列的解决方案。包括定位(jsonpath)、规范(json schema)、校验(hamcrest)。这是一套组合拳,你需要戴上拳套,跟着我好好练习。

jsonpath语法

我们用xpath定位xml,用正则定位文本,用xpath+selector定位html,这些都是一种规范。当然,用于数据交换语言的json也不示弱,它有自己的定位方式:jsonpath。可以说:jsonpath与xpath有着某种关联。让人不禁联想,他们肯定是一对很好的邻居。下面表格列出几个常用表达式:

%title插图%num

比如xpath中的语法是:

/store/book[0]/title

jsonpath也能表达相同的意思,但语法却不一样:

$.store.book[0].title
$['store']['book'][0]['title']

更多内容请访问:https://goessner.net/articles/JsonPath/

他们有很多相似的地方,但是他们关系并不和谐,并没有想象的那么好。你会看到,很多语法都改变了,接下来我们多练习一些jsonpath吧。下面是一组json结构:

{ "store": {
   "book": [
        { "category": "reference",
           "author": "Nigel Rees",
           "title": "Sayings of the Century",
           "price": 8.95
       },
        { "category": "fiction",
           "author": "Evelyn Waugh",
           "title": "Sword of Honour",
           "price": 12.99
        },
        { "category": "fiction",
           "author": "Herman Melville",
           "title": "Moby Dick",
           "isbn": "0-553-21311-3",
           "price": 8.99
        },
    { "category": "fiction",
           "author": "J. R. R. Tolkien",
           "title": "The Lord of the Rings",
           "isbn": "0-395-19395-8",
           "price": 22.99
        }
      ],
       "bicycle": {
         "color": "red",
         "price": 19.95
      }
    }
}

接下来我要对这些json进行操作,下表列出了xpath与jsonpath的对比:

%title插图%num

以上的操作可以涵盖我们大部分工作,我已经会一些jsonpath语法了,但是在哪里施展身手呢?别急,下面将介绍一些支持jsonpath语法的工具:jq,jsonpath,请收好。

jq出场

第一个出场的是jq:一个轻量级且灵活的JSON处理器(以命令行形式存在)。jq就像用sed处理JSON数据,你可以用它来切片和过滤,映射和转换数据,像sed,awk,grep一样方便。

既然可以这么方便的使用它,那安装会不会很麻烦?这点您可以放心,jq是用可移植的C编写的,它没有运行依赖。

最后贴上官网的自夸:jq can mangle the data format that you have into the one that you want with very little effort, and the program to do so is often shorter and simpler than you’d expect.

jq官网:https://stedolan.github.io/jq/

使用jq前,先介绍一个由雪球官网提供的接口你可以获得股票信息,可以用curl命令访问:

curl -k -H 'Cookie:xq_a_token=5806a70c6bc5d5fb2b00978aeb1895532fffe502;u=3446260779' -H 'User-Agent:Xueqiu Android 11.19' -H 'Accept-Language:en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4' -H 'Host:stock.xueqiu.com' --compressed 'https://101.201.175.228/v5/stock/portfolio/stock/list.json?_t=1UNKNOWNc60715cb4a61425b311034a49f4aa024.3446260779.1563002521424.1563005246620&_s=8c6b2d&category=1&pid=-1&size=10000&x=1.3&page=1'

执行curl命令,你会得到下面的结果 (悄悄告诉你:json数据太多了,这里只截取一小部分)

{
    "data": {
       "pid": -1,
       "category": 1,
       "stocks": [
        {
           "symbol": "00633",
           "name": "中国全通",
           "type": 30,
           "remark": "",
           "exchange": "HK",
           "created": 1563010594950
        },
        {
           "symbol": "SH600078",
           "name": "澄星股份",
           "type": 11,
           "remark": "",
           "exchange": "SH",
           "created": 1563010582605
        }
     
      ]
    },
     "error_code": 0,
     "error_description": ""
  }

上面是curl命令的执行结果,我们有了一份json数据,下面我想用jq操作它,比如取出中国全通

jq '.data.stocks[0].name'

由于命令太长,下面的执行结果用截图显示,使用jsonpath处理json数据的确方便。不知你发现没,jq的语法与jsonpath并不是一模一样,它们之前存在细小的差异,如果严格按照jsonpath来写,应该是$.data.stocks[0].name,也就是说jq取消了$,其他差异请参考官方文档。

%title插图%num

jq小巧易用,可以对json数据简单处理,但还是有解决该不了的问题,比如我想知道第一个股票是不是华宝中短债债券A9,这个操作不简单,涉及到取值对比,jq难以胜任,用Requests可以做吗?可以,但代码十分冗长:

url="https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?"
r=requests.get(url,
       params={ "category":"2"},
       headers={"User-Agent':'Xuegiu Android 11.19"},
       cookies={"u":"3446260779","xq_a_token":"5806a70c6bc5d5fb2b00978aeb1895532fffe502"}
          )
logging.info(json.dumps(r.json(),indent=2))
assert r.json()["data"]["catagory"] == 2
assert r.json()("data"J["stocks"][0]["name"] == "华宝中短债债券A9"
)

有没有更好的办法呢?python自带的json的操作太反人类了,我想用jsonpath!

jsonpath

当然有,python作为明星语音,对json数据的处理怎能少了它:jsonpath模块。使用前要用pip安装:

pip install jsonpath

我们把json数据存到1.json中,利用jsonpath整理1.json,比如得到0063这支股票的信息:

%title插图%num

发现了吗?jsonpath模块很听话,目前来看,它遵守了jsonpath语法:在命令前加入了$,但我还是建议你读一下jsonpath模块的文档,因为每个语言几乎都支持jsonpath,但是他们会做一些改动。我们将上面命令做些修改,并放入python中:

jsonpath.jsonpath(r.json(),"$.data.stocks[?(@.symbol == 'F006947')].name")

上面这条命令取出编号为F006947的股票名字,结果就不展示了。读到这里,你应该对jsonpath有了一点理解,我们可以在命令行中使用jq来处理json,也可以在python中使用jsonpath模块来处理json。

它们都遵循jsonpath语法,它们便利,清晰明了,但依旧有软肋。我展示的json数据很小,实际工作中会存在大量json数据,纵使有三头六臂也无法对每条数据进行断言,并且数据的内容通常是变化的。比如中国全通不一定在投票的第一位,此时我们急切渴望一种方法,它可以面对大量数据,无论它的值怎样变化,都可以进行断言。

json schema

有这样的方法吗?如果你读了本节标题,心中应该有了答案:json schema。它是json结构体的核心,一但你定义好schema,以后就可以利用schema去校验。我要告诉你一个好消息,你完成可以用下面的网站,自动生成schema。

schema生成器:https://www.jsonschema.net/json schema官网:http://json-schema.org/implementations.html

它的使用方法非常简单,将json放到左边,点击INFER SCHEMA按钮就可以生成schema。自动生成的schema

%title插图%num

有很多参数,其实主要使用下面几个:

  • “type”: “string”
  • “pattern”: “.*$”
  • “minimum”: 1.0,
  • “maximum”: 100.0

schema文件已经有了,我们怎么使用呢?首先,你需要安装jsonschema包:

pip install jsonschema

下面举个例子,我有一个简单的jsonschema,它只有两个重要字段name和price。schema规定name必须是string类型,price必须是number类型:

{
       "type": "object",
       "properties": {
           "price": {"type": "number"},
           "name": {"type":"string"},
       },
}

接下来,我要用jsonschema进行校验,使用validate方法,我输入一个name为   Eggs和price为34.99的数据进行校验:

def test_schema(self):
       schema = {
           "type": "object",
           "properties": {
               "price": {"type": "number"},
               "name": {"type":"string"},
              },
}
validate(instance={"name": "Eggs", "price": 34.99},schema=schema)

结果正常执行,现在我改一些内容,我们故意将数字写成string,就像下面这样:

def test_schema(self):
       schema = {
           "type": "object",
           "properties": {
               "price": {"type": "number"},
               "name": {"type":"string"},
          },
}
validate(instance={"name": "Eggs", "price": "34.99"},schema=schema)

可以看到,报错信息非常明显:

%title插图%num

显然,我们手动的完成了jsonschema校验,但这不是我们想要的,因为使用schema就是让它全自动,我们不想手动!要想这样做,首先把自动生成的schema存入list_schema.json中。

%title插图%num

我们看下python代码,有了刚才的例子,它们就很好理解了,我们存储requests的访问结果,并把它与jsonschema进行比较:

%title插图%num

毫无疑问,执行结果是正确的:

%title插图%num

从现在来看,这一套流程非常完美,jsonschema自动化操作,让我们解放双手,我们也不能丢弃jsonpath,两个工具配合使用才能达到工作的最大产出。

文章最后我还要向你介绍一个工具:hamcrest。这是一个辅助工具,你可以理解为增强版assert。通常情况下,python自带的断言可以解决80%的工作,它已经非常完美了,但我们是个追求完美的人,人生苦短,都用python了,何妨一试hamcrest。

hamcrest

hamcrest是一个多语言的匹配器,通常用于断言。其实,它是对assert的封装,以下是它的官网。这里吐槽一下,这ui设计,真是印证了那句名言“人生苦短,爱用不用”。

%title插图%num

依旧可以用一个简短命令进行安装:

pip install hamcrest

接下来我故意写错,将华宝中短愤债券A写成华宝中短愤债券B。利用hamcrest的assert_that方法进行断言,这是一个进行断言的标准语法:

assert_that(jsonpath.jsonpath(r.json(),"$.data.stocks[?(@.symbol == 'F006947')].name") [0],
equal_to("华宝中短愤债券B"), "比较上市代码与名字")
%title插图%num

这是一个非常基础的断言,有一个问题,基础断言的语法貌似很复杂,不信你看python的assert:

assert r.json()["data"]["stocks"][0]["name"] == "华宝中短债债券C"

这是一个类似的功能,assert清晰明了。在基础校验上,我建议你使用assert,那hamcrest强大在哪呢?举个例子,比如0.1*0.1在什么范围内?

%title插图%num

再者,我的字符有多个值,但只要匹配其中一个,就能通过:

assert_that(["a","b","c"],has_item("d"))
%title插图%num

我也可以对hasItem进行增强,使用hasItems,要求多个值都得匹配,才能通过。

assert_that(["a","b","c"],has_items("c","d"))
%title插图%num

当然,你也可以使用any_of。它相当于or,只要一个对,就是对的,all_of相当于and,必须全部对,才算对。

%title插图%num

hamcrest还有很多功能,具体请看下表:

%title插图%num