NTRIP 配置及部分基础知识扫盲

题图为为我装设备盒和布电线网线的师傅

本文原为我写的实验室文档

0. 关于GNSS

0.0 何为GNSS

GNSS(global navigation satellite system) 全称全球导航卫星系统.

GPS(Global Positioning System) 全称全球定位系统, 是GNSS的一种实现.

目前世界的定位系统有:

  1. 美国 GPS
  2. 中国 BDS (北斗)
  3. 俄罗斯 GLONASS
  4. 欧盟 Galileo (伽利略, 尚在建造)

0.1 GNSS误差来源

GNSS不间断向大地发送信号, 一般仅需要一个接收器即可使用, 定位误差一般有:

  1. 轨道误差
  2. 时钟误差
  3. 电离层延迟
  4. 对流层延迟
  5. 多径误差
  6. 接收机噪音

误差主要来自电离层

综上, GNSS 误差一般在 10.2m 左右, 手机定位还参考了周围 WIFI/蓝牙等信号, 因此可达 1~4m.

1. 关于RTK

辅助定位方案有很多种, 比如 PPP, RTD, RTK 等等. 综合考虑各种情况, 我选择了 RTK.

RTK(Real Time Kinematic)全称实时动态载波相位差分技术, 可用于高精度, 实时的定位, 一般可达 1~20cm 的定位精度. 理论极限可达毫米级.

1.0 原理

RTK 通过已知确定位置的基站校准流动站位置.

详细参文档第五章.

1.1 组成

RTK需要:

  1. 基站
  2. 流动站
  3. 中间通讯手段

传统的中间通讯手段采用向周围发射关于基站信息的无线电波, 但一般只能覆盖几公里, 且有违法之虞, 又 RTK 基站的理论有效覆盖范围可达 30 公里, 因此有组织提出了 Ntrip 协议.

1.2 Ntrip

Ntrip(Networked Transport of RTCM via Internet Protocol) 全称基于互联网的 RTCM 网络传输协议, 运行流程如下:

有四个主要组成部分:

  1. Client
  2. Caster, 目前未见中文翻译, 拙译为汇总端.
  3. Server
  4. Source

服务端读取源端设备数据, 发送至汇总端, 客户端连接汇总端获取差分数据, 用于校正.

汇总端可将多个服务端数据汇总为一个虚拟基站, 也可以不汇总. 由于只有一个源和一个客户端, 以下讨论皆以单源为准.

1.3 Ntrip部署

Windows 的部署很简单, 使用 U-Center 一套解决.

以下介绍 Caster for Linux 部署, 以树莓派 3b+ 实操.

1.3.0 服务端

使用 RTKlib 的 str2str, 代码位于, 目前最新版本 2.4.3

1
2
3
4
git clone https://github.com/tomojitakasu/RTKLIB.git
cd ./rtklib_2.4.3/app/str2str/gcc
make
sudo cp str2str /usr/bin

str2str 有两个参数, -in 和 -out, 分别可选

1
2
3
4
5
6
serial : serial://port[:brate[:bsize[:parity[:stopb[:fctr]]]]]
tcp server : tcpsvr://:port
tcp client : tcpcli://addr[:port]
ntrip client : ntrip://[user[:passwd]@]addr[:port][/mntpnt]
ntrip server : ntrips://[:passwd@]addr[:port][/mntpnt[:str]] (only out)
file : [file://]path[::T][::+start][::xseppd][::S=swap]

这里只用于从串口读, 并发给汇总端.

1.3.1 汇总端

1
2
3
4
5
git clone https://github.com/thpe/ntripcaster.git
cd ./ntripcaster/ntripcaster0.1.5
./configure –prefix=/opt
make
sudo make install

这时候你可以在 /opt/ 下 bin 中得到 ntripcaster, 并在 conf 下得到两个配置文件, 为了让汇总端读取, 需要重命名

1
2
3
cd /opt/conf
cp ntripcaster.conf.dist ntripcaster.conf
cp sourcetable.dat.dist sourcetable.dat

然后修改 ntripcaster.conf:

server_url后写成http://你的机器IP

encoder_password后改为密码, 尚未测试空密码如何

server_name后起一个随便的名字

port是服务端口, 这里我设置为 8000

这是我当前的配置

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
##################################
# NtripCaster configuration file #
################################################################################

############### Server Location and Resposible Person ##########################
# Server meta info with no fuctionality.

location BKG
rp_email [email protected]
server_url http://10.18.45.156

########################### Server Limits ######################################
# Maximum number of simultaneous connections.

max_clients 100
max_clients_per_source 100
max_sources 40

######################### Server passwords #####################################
# The "encoder_password" is used from the sources to log in.

encoder_password *********

#################### Server IP/port configuration ##############################
# The server_name specifies the hostname of the server and must not be set to
# an IP-adress. It is very important that server_name resolves to the IP-adress
# the server is running at.
# For every port, the server should listen to, a new port line can be added.

server_name testname
#port 80
port 8000

######################## Main Server Logfile ##################################
# logfile contains information about connections, warnings, errors etc.

logdir /opt/logs
logfile ntripcaster.log

############################ Access Control ###################################
# Here you specify which users have access to which mountpoints,
# one line per mount.
#
# Syntax: /<MOUNTPOINT>:<USER1>:<PASSWORD1>,<USER2>:<PASSWORD2>,...,<USERn>:<PASSWORDn>
#
# /<MOUNTPOINT>: name of the mountpoint. Must start with a slash.
# <USERi>: name of the user that has access to <MOUNTPOINT>.
# <PASSWORDi>: password of <USERi>.
#

# example:
#/mount0:user0:pass0,user1:pass1,user2:pass2
/BUCU0:user1:password1,user2:password2
/PADO0

10.18.45.156是我在内网的IP.

之后需要

修改sourcetable.dat, 添加一行:

1
STR;FFMJ2;Frankfurt;RTCM 3;1(1),3(19),16(59);0;GPS+BEIDOU;GREF;DEU;50.12;8.68;0;1;GPSNet V2.10;none;N;N;560;Demo

挂载点名 FFMJ2

[公式]

, 我的设备使用了 GPS 和 北斗双星定位, 因此写作 GPS+BEIDOU. 其余参数我也还不知道什么意思.

[1] http://yinflying.top/2017/01/274

1.3.2 客户端

目前先用 U-Center, 网上下一个就是了.

1.3.3 运行

首先运行汇总端

1
2
cd /opt/bin/
./ntripcaster

之后是服务端.

首先需要确定基站插在了哪里

1
2
pi@raspberrypi:~ $ ls -l /dev/ttyUSB*
crw-rw---- 1 root dialout 188, 0 7月 19 20:21 /dev/ttyUSB0

在 ttyUSB0 上(写这篇文章的时候我已经拔了), 因此下面写ttyUSB0, 9600 是基站波特率, ****是密码, 10.18.45.156 是设备IP, 8000 是端口, FFMJ2 是挂载点.

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
root@raspberrypi:/home/pi# str2str -in serial://ttyUSB0:9600:8:n:1:#stq -out ntrips://:****@10.18.45.156:8000/FFMJ2
stream server start
2019/07/19 13:42:03 [CW---] 0 B 0 bps (0) /dev/ttyUSB0 (1) 10.18.45.156
2019/07/19 13:42:08 [CC---] 652 B 1444 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:42:13 [CW---] 1324 B 741 bps (0) /dev/ttyUSB0 (1) disconnected
2019/07/19 13:42:18 [CW---] 1852 B 702 bps (0) /dev/ttyUSB0 (1) disconnected
2019/07/19 13:42:23 [CW---] 2928 B 1899 bps (0) /dev/ttyUSB0 (1) send error (111)
2019/07/19 13:42:28 [CW---] 3652 B 1198 bps (0) /dev/ttyUSB0 (1) send error (111)
2019/07/19 13:42:33 [CW---] 4852 B 1198 bps (0) /dev/ttyUSB0 (1) send error (111)
2019/07/19 13:42:38 [CW---] 6166 B 1900 bps (0) /dev/ttyUSB0 (1) send error (111)
2019/07/19 13:42:43 [CW---] 6786 B 987 bps (0) /dev/ttyUSB0 (1) 10.18.45.156
2019/07/19 13:42:48 [CW---] 7914 B 1651 bps (0) /dev/ttyUSB0 (1) 10.18.45.156
2019/07/19 13:42:53 [CW---] 8390 B 247 bps (0) /dev/ttyUSB0 (1) timeout
2019/07/19 13:42:58 [CW---] 8814 B 454 bps (0) /dev/ttyUSB0 (1) timeout
2019/07/19 13:43:03 [CC---] 9486 B 742 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:43:08 [CC---] 9900 B 494 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:43:13 [CC---] 10448 B 1445 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:43:18 [CC---] 11172 B 744 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:43:23 [CC---] 12020 B 952 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:43:28 [CW---] 12620 B 949 bps (0) /dev/ttyUSB0 (1) recv error (115)
2019/07/19 13:43:33 [CW---] 13416 B 2187 bps (0) /dev/ttyUSB0 (1) recv error (115)
2019/07/19 13:43:38 [CW---] 14378 B 701 bps (0) /dev/ttyUSB0 (1) 10.18.45.156
2019/07/19 13:43:43 [CW---] 14874 B 742 bps (0) /dev/ttyUSB0 (1) 10.18.45.156
2019/07/19 13:43:48 [CW---] 15598 B 1443 bps (0) /dev/ttyUSB0 (1) timeout
2019/07/19 13:43:53 [CW---] 16312 B 1196 bps (0) /dev/ttyUSB0 (1) timeout
2019/07/19 13:43:58 [CC---] 16736 B 989 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:03 [CC---] 17760 B 1729 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:08 [CC---] 18712 B 952 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:13 [CC---] 19550 B 2147 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:18 [CC---] 19964 B 702 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:23 [CC---] 21040 B 1443 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:28 [CC---] 22136 B 1689 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:33 [CC---] 22994 B 1237 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2
2019/07/19 13:44:38 [CC---] 23770 B 701 bps (0) /dev/ttyUSB0 (1) 10.18.45.156/FFMJ2

服务端成功连接后, 汇总端有输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@raspberrypi:/opt/bin# ./ntripcaster
NtripCaster Version 0.1.5 Initializing...
NtripCaster comes with NO WARRANTY, to the extent permitted by law.
You may redistribute copies of NtripCaster under the terms of the
GNU General Public License.
For more information about these matters, see the file named COPYING.
Starting thread engine...
[19/Jul/2019:21:45:54] NtripCaster Version 0.1.5 Starting..
[19/Jul/2019:21:45:54] Using stdout as NtripCaster logging window
[19/Jul/2019:21:45:54] Starting main connection handler...
[19/Jul/2019:21:46:14] WARNING: Resolving the server name [testname] does not work!
[19/Jul/2019:21:46:14] Listening on port 8000...
[19/Jul/2019:21:46:14] Using 'testname' as servername...
[19/Jul/2019:21:46:14] Server limits: 100 clients, 100 clients per source, 40 sources
[19/Jul/2019:21:46:14] Starting Calender Thread...
[19/Jul/2019:21:46:14] Bandwidth:0.000000KB/s Sources:0 Clients:0
[19/Jul/2019:21:46:14] Accepted encoder on mountpoint /FFMJ2 from 10.18.45.156. 2 sources connected
[19/Jul/2019:21:46:14] Accepted encoder on mountpoint /AB0 from 10.18.45.156. 2 sources connected
[19/Jul/2019:21:46:14] Kicking source 0 [10.18.45.156] [Source signed off (killed itself)] [encoder], connected for 0 seconds, 0 bytes transfered. 1 sources connected
[19/Jul/2019:21:46:14] Kicking all 0 clients for source 0
[19/Jul/2019:21:46:14] Kicking source 1 [10.18.45.156] [Source signed off (killed itself)] [encoder], connected for 0 seconds, 0 bytes transfered. 0 sources connected
[19/Jul/2019:21:46:14] Kicking all 0 clients for source 1

转移到 Windows 上, 打开 U-Center, Receiver->NTRIP Client...->填入各项信息, 用户名为空, 单击更新源表, 在下拉框里选择FFMJ2, 之后确定.

查看 Fix Mode, 很快, 就到了 3D/DGNSS/FIXED, 按 F12, 查看定位结果.

关于硬件配置购买及修改, 明日待续.


吐槽: 今天为了连接眼前三十厘米的树莓派, 我先笔记本从从手机热点将数据转送到北京的服务器, 然后服务器转发到校园网我的路由器, 路由器转发到运行frp的台式, 台式转发到同在校园网的树莓派,, 数据发过去一次要跑1600km, 回来又要跑1600km, fong了

RTK 设备选购及硬件配置

0. 选购

RTK芯片有很多家, 据我了解比较出名就是 u-blox, 大部分买到的封装过的模块, 内部估计都是这家的, 价格一般2k起步.

现在国内也有一些开始起步了, 价格低很多, 1.2k就可以拿到一件, 但是据我尝试, 效果还不是很好, 如果囊中羞涩, 也可以选择.

另一方面是天线, 天线我目前接触到了两种, 一种是400起步的蘑菇头测量天线, 另一种是小一点的普通的GPS天线, 几十块钱搞定.

如果要使用, 最基础需要一个移动站, 然后使用第三方 cors 服务, cors 服务一般有两种选择:

  • 本省的测绘地理信息局, 比如搜索江苏cors, 可以在官网看到电话号码025-83757130, 咨询过安装费1000, 一年3000, 负担不起.


  • 千寻知寸, 一年要3600, 负担不起, 但是可以按周购买.

所以我选择了自搭基站, 进口的2500一个, 一年千寻就够买一个半了. 以下以自搭基站为准.

购买的时候你需要考虑:

  • 天线 * 2
  • 处理模块 * 2
  • 串口-USB线 * 3, 由于现在的电脑一般都没有生串口了, 所以得买, 另外绿联那家的螺丝拧不下来, 很烦, 买的时候买可以直接手拧螺丝的那种线.
  • 一分二串口线 * 1, 最初测试的时候, 可以直接使用一分二串口线, 直接连上基站和移动站.

1. 配置

天太热了, 写文档的时候就在屋里了, 天线都是没信号的.

1.0 基站配置

连接后 F9 进入 Message View Windows, 然后 UBX->CFG->GNSS

我这个芯片似乎是四频的, 但是中国只有 GPS 和北斗信号不错, 因此勾上二者, 并勾上 Enable 和 Signals, 之后下方点击Send.

然后参考RTCM3这个页面:

如果是GPS+北斗, 就记下1005+1074+1077+1124+1127, 其余自行组合.

然后在UBX->CFG->MSG, 勾上对应的端口

我这里使用的UART1, 因此勾上后Send.

之后检查UBX->CFG->PRT, 是否 out 都打开.

之后是最影响精度的基站定位, 首先在UBX->CFG->TMODE3这里选择1 -Survey-in

定位一段时间后, 在 UBX->NAV->SVIN

记录Mean ECEF *, 然后回到UBX->CFG->TMODE3, 修改Mode到2 - Fixed Mode, 把刚记录的数据填入 Fixed Position.

1.1 流动站配置

连接后 F9 进入 Message View Windows, 然后 UBX->CFG->GNSS, 配置和基站相同.

检查 UBX->CFG->PRT, 看看 in 是否正常.

之后 UBX->CFG->NAV5, 如图配置

正确连接后等待一会, 可以看到 Fix Mode 进入 3D/DGNSS/FLOAT 或者直接进入 3D/DGNSS/FIXED

此时可达厘米级精度.

1.2 测试连接

测试连接方式如下

1
2
3
4
5
6
7
移动站
\
\
\_____基站
|
|
笔记本

移动站插一个一分二的线, 一方面使用USB-串口连接笔记本, 另一方面连基站, 如果可使用, 则可参考上篇 NTRIP 配置, 使用网络传递差分信息.


这方面资料真少, 无论中文英文, 而且甚至上一篇连一个词的中文翻译我都还没见到过.

想了半年的事情, 终于在暑假弄好了

Bug 10 重启和正常输入的抉择记录

2019/04/19, 我新加了一个 M.2 的固态, 现在机器上挂了三个硬盘. 两个 SATA, 一个 M.2.

2019/04/20 上午, 我迁移了原来固态上的系统到了新加固态上. 并将新硬盘作为优先启动盘.

2019/04/20 中午, 我收到了这样一个补丁

Intel Corporation - Display - 11/18/2018 12:00:00 AM 25.20.100.6373

一个去年十一月的补丁现在才推过来很奇怪了.

我选择了更新, 之后出现了我在 1709/1803 经常见到的关机后自动重启和睡眠后自动开机, 之前几次都是找找事件管理器, 看下哪个设备唤醒了计算机, 禁掉就行了. 然而这次 Bug 10 给我了一击.

关键事件的常规描述是:

用户模式进程尝试通过调用 SetSuspendState 或 SetSystemPowerState API 更改系统状态。

没头没尾的. 转过去看看事件的 XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
<Provider Name="Microsoft-Windows-Kernel-Power" Guid="{331c3b3a-2005-44c2-ac5e-77220c37d6b4}" />
<EventID>187</EventID>
<Version>0</Version>
<Level>4</Level>
<Task>243</Task>
<Opcode>0</Opcode>
<Keywords>0x8000400000000404</Keywords>
<TimeCreated SystemTime="2019-04-23T04:48:31.851398700Z" />
<EventRecordID>23238</EventRecordID>
<Correlation />
<Execution ProcessID="12104" ThreadID="12148" />
<Channel>System</Channel>
<Computer>myuan</Computer>
<Security UserID="S-1-5-21-3775543428-34658433-2914125342-1001" />
</System>
<EventData>
<Data Name="ApiCallerNameLength">58</Data>
<Data Name="ApiCallerName">\Device\HarddiskVolume1\Windows\System32\RuntimeBroker.exe</Data>
<Data Name="SystemAction">2</Data>
<Data Name="LightestSystemState">2</Data>
</EventData>
</Event>

<DataName=”ApiCallerName”>\Device\HarddiskVolume1\Windows\System32\RuntimeBroker.exe

ApiCallerName 的路径是 "\Device\HarddiskVolume1", 是新硬盘引起的吗? 暂时不可知. "\Device\HarddiskVolume1" 应该就是指的当前的 C 盘. 为什么不直接写 C, 而是用了硬盘描述呢?

引起事件的程序是 RuntimeBroker.exe , 查一下, 有说要关通知的, 有说改注册表的, 改通知无效, 改注册表他们说是改

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TimeBroker

但是很遗憾, 我这里没有这个路径, 倒是有这个

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TimeBrokerSvc

成吧, Start 从 3 改成 4 表示禁止, 重启, 正常重启了, 睡眠, 也正常睡眠了. 之后就开始叫人揪心了.

  1. 输入法框消失, 设置中选择输入法显示只能运行在桌面模式
  2. 可以通过 Win 键唤出开始菜单, 但是之后的任何以此为焦点的输入都无效, 比如 Tab/上下左右/字母/数字, 包括再按 Win 键关掉开始菜单
  3. 快捷键仍然存在(Win + R), 但是设置里的按键也全部失效.

暂时怀疑因为 RuntimeBroker 是用于 Windows10 权限管理的, 把这项服务 ban 掉后, 有一些应用无法获取到某些权限了, 因而表现为 UWP 应用无法输入. 但是这个进程为什么会禁止我关机和睡眠, 而且关键在用户态进行, 这就很奇怪了.

暂时没空折腾了, 只好先放弃睡眠功能, 每次要关机就选择重启之后进入 BIOS 电源按钮关机咯.

Windows 1809, 17763.437, 教育版


19/05/03补充

上面那行中文描述应该是中文互联网上首次出现了, 现在搜索错误描述只有这篇文章一个.

我已经尝试过移除RuntimeBroker.exe了, 移除成功后同样出现上述禁用服务的错误, 通过 Alt+F4 睡眠时, 这次的ApiCallerName就变成了\Device\HarddiskVolume1\Windows\System32\winlogon.exe, 这个可没法动, 而且之后我恢复了RuntimeBroker.exe, 现在睡眠的时候会有两个事件了, 一个RuntimeBroker.exe, 之后winlogon.exe.

我猜想是explorer.exe发出睡眠信号后其他程序唤醒了计算机, 如果有人有空的话, 可以试着写一个内核应用, ban 掉这两个进程在短时间内的唤醒信号, 或者如果二者会一直唤醒的话, 可以定期自行唤醒, 不过还是不如微软自带的睡眠啊, 想自行监控睡眠期间的按键鼠标动作着实有点难.

等五月底的 1903 吧


06/21:

六月底还没有给我推新版本, 终于用了易升手动更新到了 1903, 问题一如之前莫名其妙地解决


从我的知乎文章搬运

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, 啊, 滚去复习

双系统下 Linux 系统无法启动及其引导丢失之解决

背景介绍:

很久很久以前, 我在 NewSurfacePro(SP5) 里插了一张 128G 内存卡, 费力九牛二虎之力在上面装了 Deepin, 后来在某次不知道是 Windows 还是 Deepin 更新后, Deepin 启动时总要发生一个极具 Linux 特色的启动错误, witch 似乎在我树莓派上出现过, 折腾了很久也没好, 就放弃了, 又由于 Deepin for Surface 电源管理太弱了, 我还是改过内核的, Windows 下 8h 左右的电只能在 Deepin 下坚持 3h 不到, 之后 NSP 就被我当成能用远程的平板了, 只用于写写笔记和远程到大电脑上.

由于 Deepin 引导的存在, 每次开机都得下移两个选项选 Win, 特别是无键盘开机, 要眼疾手快点出来虚拟键盘, 端午节得了点空, 终于盯上了这个问题, 崩溃就此开始.

我最初尝试了在 BIOS 里换换引导顺序, 无效. 直接拔掉内存卡, 会掉入 grub, 怎么 set 都无法引导到 Windows, 但是网上却有很多文章说可以直接删除相关分区然后在 msconfig 修改, 我连 Windows 都进不去啊. 我的 ESP 分区目录树大概这个样子:

1
2
3
4
5
6
7
ESP
|-EFI
| -Boot
| -deepin
| -Microsoft
| | #下面还有, 略
| -ubuntu # 的确曾经装过Ubuntu

我尝试了可能的cmdpathprefix 的组合, 都不可以.

在 Win 下使用 EasyUEFI 查看也只有 Windows Boot Manager一个, 根本无所谓删谁的问题, 反正删了就没了. 于是在万分抱歉的心情下, 我在 BIOS 误删了 Windows Boot Manager, Internal Storage, 误删两个我也不知道我怎么做到的, 我发誓是误删的! 好了, 彻底崩了, 插上内存卡也没法用 Deepin 了.

又听闻一些 PE 盘下有一些工具, 可以一键修复 EFI, 于是我就刻录了相关的盘, 但是根本无法引导到 U盘上, 又看到微软官方表示开机要按照音量下键, 但是我试了, 仍然不行, 我以为是该 PE 不行, 又换了一个, 还是不行, 于是排除 PE 问题, 后来注意到微软提供了修复包, 经过观察发现格式和普通的 Windows 镜像差不多, 遂换上, 成功从 U盘引导.

之前已经做好了丢失所有文件准备, 如今重置就在我眼前, 我却下不了手了, 我记起来我装完系统有段时间经常需要用 bcdedit 改一下 .efi 文件, 于是开始了 bcdedit 修改配置的过程, 毫无作用, 该不行还不行.

后注意到有个 bcdboot, 各种文章里都用了/s /f参数的搭配, 但是我这里甚至会报错, 最后看着帮助发觉后面两个参数估计可以不需要, 去掉后直接bcdboot c:\windows完事, 重启完事.😥

这个折磨我超过半年的问题就此解决, 简单来说, 卸载 Windows 以外的双系统的确可以直接删分区, 然后引导 U盘直接bcdboot c:\windows, 但是 Surface 好像不能轻易引导非官方工具制作的 U盘, 我在这里卡了很久.


现在是 2019/06/09, 我三台处于 1809 的 Windows 电脑, 都没有收到 1903 更新推送, 即使天天点检查更新. 睡醒后还没有推的话, 我就要对 Surface 拿易升试试了.

我要 Windows Terminal, 我要 WSL2!

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


从我的知乎文章搬运而来