Ruby Ruby 和 Python 中对中文的处理

jiz4oh · May 11, 2020 · Last by jiz4oh replied at May 13, 2020 · 3982 hits

背景:小弟最近在对接腾讯云短信的接口,由于腾讯云短信没有提供 ruby 的 SDK,所以按照官方的签名方法自己进行了封装。但是腾讯一直报签名错误,故与腾讯贴出来的 python 代码示例进行了对比,发现问题出在 ruby 和 python 对中文的处理上

第一回合

ruby:

require 'json'
require 'digest'

params = { "key": '' }

u = JSON.dump(params).gsub(':', ': ').gsub(',', ', ')
res = Digest::SHA256.hexdigest(u)
puts res
# 30428a3206c817f42e49331138ba5d36e2a29124876f75fad011f4c1f5b94661

python:

import json, hashlib

params = {"key": ''}

u = json.dumps(params).encode('utf-8')
res = hashlib.sha256(u).hexdigest()
print(res)
# 30428a3206c817f42e49331138ba5d36e2a29124876f75fad011f4c1f5b94661

到这里为止,ruby 和 python 使用 SHA256 算法散列出来的结果是相同

第二回合

加入中文:

ruby:

require 'json'
require 'digest'

params = { "key": '爱你' }

u = JSON.dump(params).gsub(':', ': ').gsub(',', ', ')
res = Digest::SHA256.hexdigest(u)
puts res
# 3454c5b42519f5be5a953324779d48b7662d25d08e06fe020a5c2248a23921ae

python:

import json, hashlib

params = {"key": '爱你'}

u = json.dumps(params).encode('utf-8')
res = hashlib.sha256(u).hexdigest()
print(res)
# 981727d14419aedca58b2e719b4a9ff94cf20ef91e02c9cd22e7ee6f5744f630

疑惑

不知道 ruby 中对中文的处理和 python 中有何不同,造成了这个偏差?各路大佬可以讲讲吗?

ps:

  1. ruby 版本:2.5
  2. python 版本:3.7
  3. 可能有人会对 gsub(':', ': ').gsub(',', ', ') 这个操作感到奇怪,但是如果不做这个处理,结果会是这样
require 'json'
require 'digest'

params = { "key": '' }

u = JSON.dump(params)
res = Digest::SHA256.hexdigest(u)
puts res
# 0944d67c4d96fe949834700d0cb784b99ee5b0b6205b0667d842ece155405df2

先去掉 gsub(':', ': ').gsub(',', ', ') 看看 json string 有什么区别。

我没用过 python,但我执行你的 python 代码

import json, hashlib

params = {"key": '爱你'}

u = json.dumps(params).encode('utf-8')
# b'{"key": "\\u7231\\u4f60"}'

返回的结果里,是单引号 包着双引号。因此\u被转义成了\\u。于是顺着这个思路,我把部分中文 utf8都 gsub 成了\\uXXXX 的形式

require 'json'
require 'digest'

params = { "key": '爱你' }

u = JSON.dump(params).gsub(':', ': ').gsub(',', ', ').gsub(/[\u4e00-\u9fa5]/) do |s| "\\u#{s.unpack('U')[0].to_s(16)}" end
res = Digest::SHA256.hexdigest(u)
puts res
#  981727d14419aedca58b2e719b4a9ff94cf20ef91e02c9cd22e7ee6f5744f630
Reply to Rei

ruby 中的 json string 会是 {"key":""}{"key":"爱你"} python3 中 的 json string 会是 b'{"key": ""}'b'{"key": "\\u7231\\u4f60"}' python3 中字符串前面一个 b 代表的是非 unicode 编码 (因为此处被编码为 utf-8),\\u7231\\u4f60 是因为 python3 中默认编码为 unicode (中文先被转为了 unicode)

Reply to zhuoerri

这是一个解决方案,有更优雅的吗😄

hash = { key: '中文' }
JSON.generate hash
# => {"key":"中文"}
JSON.generate hash, ascii_only: true
# => {"key":"\u4e2d\u6587"}
JSON.generate hash, ascii_only: true, space: ' '
# => {"key": "\u4e2d\u6587"}

@Rei 的方法可以

irb(main):032:0> Digest::SHA256.hexdigest JSON.generate({ "key": '爱你' }, ascii_only: true, space: ' ')
"981727d14419aedca58b2e719b4a9ff94cf20ef91e02c9cd22e7ee6f5744f630"

逗号后面加空格没找到选项。

Reply to Rei

感谢~

Reply to Rei

# * *space*: a string that is put after, a : or , delimiter (default: ''),

generate 方法的 space 选项被说明为可以添加逗号后面的空格,但现在确实未生效,可能是一个 BUG?

Reply to Rei

ruby 确实没有提供选项可以把 [1,2,3] => [1, 2, 3],有个 array_nl 不过会在方括号内侧也加上空格:

irb(main):004:0> JSON.generate({ "key": [1, 2, 3] }, ascii_only: true, space: ' ')
"{\"key\": [1,2,3]}"
irb(main):005:0> JSON.generate({ "key": [1, 2, 3] }, ascii_only: true, space: ' ', array_nl: ' ')
"{\"key\": [ 1, 2, 3 ]}"

这样与 python 结果还是不一样。

gsub(',', ', ') 是个简单可行的办法,前提是不考虑对内容本身自带 , 的影响。

可以做个 gem 放到 github

Reply to jiz4oh

刚醒悟过来你们说的可能是 kv 与 kv 间隔的逗号而不是数组里的逗号。

space 选项不知道是文档没写好还是代码没写好,我看了下 ruby 逻辑,目前是只对 : 生效的。

https://github.com/ruby/ruby/blob/ruby_2_5/ext/json/generator/generator.c#L923

看 github 代码 state->space 只是 append 到 object_delim2 (:),没有 append 到 object_delim (,) 和 array_delim (,) 。

Reply to jiz4oh

官网文档是这样:

space: a string that is put after a : pair delimiter (default: '')

没有说 ,

https://ruby-doc.org/stdlib-2.7.0/libdoc/json/rdoc/JSON.html#method-i-generate

Reply to Rei

2.7.1 多了两个逗号

space: a string that is put after, a : or , delimiter (default: '')

https://ruby-doc.org/stdlib-2.7.1/libdoc/json/rdoc/JSON.html#method-i-generate

如果腾讯客户端用的是 Python,那要想办法把中文转码成 ascii

In [1]: import json                                

In [2]: params = {"key": '爱你'}     

In [3]: params                                     
Out[3]: {'key': '爱你'}

In [4]: json.dumps(params)                         
Out[4]: '{"key": "\\u7231\\u4f60"}'

In [6]: json.dumps(params, ensure_ascii=False)     
Out[6]: '{"key": "爱你"}'
Reply to zhengpd

感觉是 2.7.1 文档错了,master 又改了回去 https://github.com/ruby/ruby/commit/2e5ef30cb9f56e5a7a8139e0f1d75bbcf5ee8362

反正要写 gem 的话不能依赖这个参数。

Reply to Rei

是的,2.7.1 文档错了。

翻看了 2.3.1 里是 2.7.1 的版本,2.4.1 版没有错误了。我估计是 2.7.1 发布前有 commit 被回滚了导致重新引入了旧版错误。

https://ruby-doc.org/stdlib-2.3.1/libdoc/json/rdoc/JSON.html#method-i-generate

https://ruby-doc.org/stdlib-2.4.1/libdoc/json/rdoc/JSON.html#method-i-generate

Reply to nouse
  1. 不能使用 ensure_ascii 参数,会造成 sha256 散列与示例不同
  2. 倒不是腾讯客户端使用的 python😄 ,因为官方没有 ruby 的 sdk,我就对比 python 示例写了一个
Reply to Stone

还没写过 gem,后面尝试下

Reply to zhengpd

我目前使用的 2.5.5 版本又变成了 # * *space*: a string that is put after, a : or , delimiter (default: ''),可能也是回滚造成的

zhengpd in 请求帮助 Ruby [hash] 与 Python [dict] mention this topic. 15 Jun 14:57
You need to Sign in before reply, if you don't have an account, please Sign up first.