使用 Python+AT 指令接收并解码中文短信

我手上的设备型号是「EC600N」, 使用「联通4G」卡, 查看短信流程如下:

1
2
3
4
5
6
7
8
9
> AT+CMGF=1
OK # 这一句用于设置读取短信时为文本模式, 由于未知原因, 在我的设备上PDU模式总是空的
> AT+CMGL="ALL"
+CMGL: 0,"REC UNREAD","180********",,"22/10/03,11:35:58+32"
4E2D658777ED4FE16D4B8BD5002000680065006C006C006F00200077006F0072006C0064

OK # 这一句用于查阅所有短信, 无论已读未读
> AT+CMGD=1,4
OK # 这一句用于删除储存1里的所有短信, 阅后即焚

收到的短信格式是这样的:

1
2
3
4
5
> AT+CMGL="ALL"
+CMGL: 0,"REC UNREAD","180********",,"22/10/03,11:35:58+32"
4E2D658777ED4FE16D4B8BD5002000680065006C006C006F00200077006F0072006C0064

OK

这串字符是「utf-16-be」编码的十六进制表示, 在Python里应当使用如下解读方式:

1
2
3
>>> line = '4E2D658777ED4FE16D4B8BD5002000680065006C006C006F00200077006F0072006C0064'
>>> bytes.fromhex(line).decode('utf-16-be')
中文短信测试 hello world

综合来讲, 我的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def main():
ec600n = ES600N()
await ec600n.write('AT')
assert await ec600n.wait_ok()

await ec600n.write_wait_ok('AT+CMGF=1')
res = await ec600n.write_wait_ok('AT+CMGL="ALL"')
for line in res.splitlines(keepends=False):
if not re.match("^[0-9A-F]+$", line):
print(line)
else:
print(bytes.fromhex(line).decode('utf-16-be'))

inputs = input('删除所有短信吗? [yes/NO]\n')
if inputs.lower() == 'yes':
print(await ec600n.write_wait_ok('AT+CMGD=1,4'))

运行结果是这样的:

1
2
3
4
5
6
7
8
$ python 收短信.py
+CMGL: 0,"REC UNREAD","180********",,"22/10/03,11:35:58+32"
中文短信测试 hello world

OK
删除所有短信吗? [yes/NO]

$

Requests post 重复键丢失 bug 小记

图文无关, 单纯是我拍的照片

前情:

我有一段大一时候写的代码, 用于自动完成学校的经典阅读试卷, 最初版本参照 @左易方 学长的博客, 并摒弃了他字符串拼接式的提交让代码更清晰.

之后我做了更多, 我使用了 Flask 完成了一个原网页的代理, 以此公开我所修改一些内容, 大概流程就是对于每一个请求, 如果是不需要变动的, 那么使用 requests 原样转发, 如果是则按需变动.

在最终提交部分, 有一个这样的 post (的确是post, 而且是用 JavaScript 字符拼接得到的)

1
26866=94166&26866=94167&26866=94168&26866=94169&27352=95873&27352=95874&27352=95875&27352=95876

大致意思是

1
2
3
4
5
6
7
8
26866 对应了答案 94166
26866 对应了答案 94167
26866 对应了答案 94168
26866 对应了答案 94169
27352 对应了答案 95873
27352 对应了答案 95874
27352 对应了答案 95875
27352 对应了答案 95876

对于 Flask 得到的请求, 我采用如下方式转发

1
2
t = sess.post(url, request.form) // sess 是第一次访问页面时分配的
return t.content

到四月份一直相安无事. 直到今天坏了, 上面那一段提交后实际上得到的是

1
26866=94166&27352=95873

第一个重复项之后的都丢失了, 我第一反应就是处理的时候重复项因为未知原因被丢掉了, 大概看了看文档就知道如何做了

1
2
t = sess.post(url, request.form.lists()) // sess 是第一次访问页面时分配的
return t.content

问题到此解决, 但是——


但是我之前一直都好好的啊, 接下来是探秘.

requests 中请求的构造在models.pyprepare_body里, 构造非 stream 非 file body时, 使用的是self._encode_params(data), 核心代码如下

1
2
3
4
5
6
7
8
9
10
result = []
for k, vs in to_key_val_list(data):
if isinstance(vs, basestring) or not hasattr(vs, '__iter__'):
vs = [vs]
for v in vs:
if v is not None:
result.append(
(k.encode('utf-8') if isinstance(k, str) else k,
v.encode('utf-8') if isinstance(v, str) else v))
return urlencode(result, doseq=True)

result 在这里还是个list, 不用担心重复问题, 那么问题就一定是在for k, vs in to_key_val_list(data)了, to_key_val_listutils.py里的一个函数, 简单粗暴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def to_key_val_list(value):
"""Take an object and test to see if it can be represented as a
dictionary. If it can be, return a list of tuples, e.g.,

::

>>> to_key_val_list([('key', 'val')])
[('key', 'val')]
>>> to_key_val_list({'key': 'val'})
[('key', 'val')]
>>> to_key_val_list('string')
ValueError: cannot encode objects that are not 2-tuples.

:rtype: list
"""
if value is None:
return None

if isinstance(value, (str, bytes, bool, int)):
raise ValueError('cannot encode objects that are not 2-tuples')

if isinstance(value, Mapping):
value = value.items()

return list(value)

只关注最后几行, Mappingcollections里定义, isinstance(value, Mapping)在这里是真的, 那么问题就在value.items()或者list(value)中了.

此处我以前是直接传入的 Flask 中 request.form, 其类型是 werkzeug.datastructures.ImmutableMultiDict, 翻一翻代码可以发现, 它的items()方法继承自MultiDict, 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
def items(self, multi=False):
"""Return an iterator of ``(key, value)`` pairs.
:param multi: If set to `True` the iterator returned will have a pair
for each value of each key. Otherwise it will only
contain pairs for the first value of each key.
"""

for key, values in iteritems(dict, self):
if multi:
for value in values:
yield key, value
else:
yield key, values[0]

iteritems方法来自_compat.py

1
iteritems = lambda d, *args, **kwargs: iter(d.items(*args, **kwargs)) 

iteritems(dict, self)dict.items(self)(莫名像 Linux 的系统调用宏)

dict.items要求其参数具有leniter两个方法来生成新的 Dictionary view objects, (这是 Python 的鸭子类型的表现). 且MultiDictleniter实际上是继承自dict, 因此, len(MultiDict)时实际上是把MultiDict当成dict给出的值, 那么此处实际上是先把对象转成了dict, 然后才取其items的.

也就是说, 在一层层的转换过程中, 某一层把[('26866', '94166'), ('26866', '94167')]变成了{'26866': '94166'}, 然后又取items变成了[('26866', '94166')], 因此导致了重复键的其他值丢失.

另一方面, 这样一层层下来, 做了不少无用功, 而这些无用功很大程度上不是写代码的人造成的, 而是语言造成的, 也无怪乎 Python 的性能差了. 但是写起来舒服哇.


下周二就要考试了, 还花了半晚上 de 这个 bug, 啊, 滚去复习

Python 用 docx 处理 Word 中表格时出现的文本重复问题

这两天用docx提取Word中表格时, 发现对于稍稍复杂一点的表格就会出现很多重复项, 比如

一张很常见的Word中的表格

使用代码

1
2
3
4
5
6
7
8
9
10
11
12
table_temp = []
path = r"./demo.docx"

document = Document(path)
tables = document.tables

for row in tables[0].rows:
row_temp = []
for cell in row.cells:
row_temp.append(cell.text)
table_temp.append(row_temp)
table_temp

其得到结果部分为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
['从何时开始\n运行',
'\n\n2017.10',
'每年运行费用(万元)',
'每年运行费用(万元)',
'每年运行费用(万元)',
'\n\n2',
'场地\n面积',
'600㎡',
'600㎡',
'600㎡',
'600㎡',
'场地产权是否归团组织(如否,请注明使用年限)',
'场地产权是否归团组织(如否,请注明使用年限)',
'场地产权是否归团组织(如否,请注明使用年限)',
'是',
'是'],
['上级团组织或同级财政是否有配套经费',
'无',
'配套经费\n(万元)',
'配套经费\n(万元)',
'配套经费\n(万元)',
'',
'专职工作人员数',
'现有',
'现有',
'拟安排',
'拟安排',
'兼职工作人员数',
'兼职工作人员数',
'兼职工作人员数',
'现有',
'拟安排'],
['上级团组织或同级财政是否有配套经费',
'无',
'配套经费\n(万元)',
'配套经费\n(万元)',
'配套经费\n(万元)',
'',
'专职工作人员数',

最初我试图跳过已出现的文本, 但是有不少文本本来就是多次出现的, 遂作罢.

之后阅读了部分源代码发现, 那些程序性重复文本是对同一个对象的引用, 所以只要在读取一个值后对其text置空或者任意自定义的其他, 就可以把后面将要出现的程序性重复项也设置为自定义项.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
table_temp = []
path = r"demo.docx"
null_text = str(time.time())

document = Document(path)
tables = document.tables

for row in tables[0].rows:
row_temp = []
for cell in row.cells:
if cell.text != null_text:
row_temp.append(cell.text)
cell.text = null_text
table_temp.append(row_temp)
table_temp

可得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[['申报平台\n名称'],
['负责人\n是否专职', '负责人\n姓 名', '手机'],
['平台所在地址及邮编', '联系电话 '],
['从何时开始\n运行', '每年运行费用(万元)', '场地\n面积', '场地产权是否归团组织(如否,请注明使用年限)'],
['上级团组织或同级财政是否有配套经费',
'配套经费\n(万元)',
'专职工作人员数',
'现有',
'拟安排',
'兼职工作人员数',
'现有',
'拟安排'],
[],
['是否与系统团组织共建', '共建单位名称'],
['申请\n经费用途'],
['综合服务平台或建设团组织近三年获得的荣誉'],
['已\n开\n展\n的\n服\n务\n项\n目', '项目名称', '项目内容', '服务群体', '服务人数']]

至此, 只要稍稍处理下规则就能愉快地格式化数据了


吐槽:

docx对于表格的处理真的太麻缠了, 几乎要被逼换vba, QWQ


从我的知乎文章搬运而来