xsos：一个在 Linux 上阅读 SOSReport 的工具

Summary: xsos 是一个帮助用户轻松读取 Linux 系统上的 sosreport 的工具。另一方面，我们可以说它是 sosreport 考官。

# xsos：一个在 Linux 上阅读 SOSReport 的工具

我们都已经知道 [SOSReport](https://www.2daygeek.com/how-to-create-collect-sosreport-in-linux/)。它用来收集可用于诊断的系统信息。Redhat 的支持服务建议我们在提交案例时提供 SOSReport 来分析当前的系统状态。

它会收集全部类型的报告，以帮助用户找出问题的根本原因。我们可以轻松地提取和阅读 SOSReport，但它很难阅读。因为它的每个部分都是一个单独的文件。

那么，在 Linux 中使用语法高亮显示阅读所有这些内容的最佳方法是什么。是的，这可以通过 `xsos` 工具做到。

### sosreport

`sosreport` 命令是一个从运行中的系统（尤其是 RHEL 和 OEL 系统）收集大量配置细节、系统信息和诊断信息的工具。它可以帮助技术支持工程师在很多方面分析系统。

此报告包含有关系统的大量信息，例如引导信息、文件系统、内存、主机名、已安装的 RPM、系统 IP、网络详细信息、操作系统版本、已安装的内核、已加载的内核模块、打开的文件列表、PCI 设备列表、挂载点及其细节、运行中的进程信息、进程树输出、系统路由、位于 `/etc` 文件夹中的所有配置文件，以及位于 `/var` 文件夹中的所有日志文件。

这将需要一段时间来生成报告，这取决于你的系统安装和配置。

完成后，`sosreport` 将在 `/tmp` 目录下生成一个压缩的归档文件。

### xsos

[xsos](https://github.com/ryran/xsos) 是一个帮助用户轻松读取 Linux 系统上的 `sosreport` 的工具。另一方面，我们可以说它是 `sosreport` 考官。

它可以立即从 `sosreport` 或正在运行的系统中汇总系统信息。

`xsos` 将尝试简化、解析、计算并格式化来自数十个文件（和命令）的数据，以便为你提供有关系统的详细概述。

你可以通过运行以下命令立即汇总系统信息。

```
# curl -Lo ./xsos bit.ly/xsos-direct; chmod +x ./xsos; ./xsos -ya
```

### 如何在 Linux 上安装 xsos

我们可以使用以下两种方法轻松安装 `xsos`。

如果你正在寻找最新的前沿版本。使用以下步骤：

```
# curl -Lo /usr/local/bin/xsos bit.ly/xsos-direct
# chmod +x /usr/local/bin/xsos
```

下面是安装 `xsos` 的推荐方法。它将从 rpm 文件安装 `xsos`。

```
# yum install http://people.redhat.com/rsawhill/rpms/latest-rsawaroha-release.rpm
# yum install xsos
```

### 如何在 Linux 上使用 xsos

一旦通过上述方法之一安装了 xsos。只需运行 `xsos` 命令，不带任何选项，它们会显示有关系统的基本信息。

```
# xsos

OS
  Hostname: CentOS7.2daygeek.com
  Distro:   [redhat-release] CentOS Linux release 7.6.1810 (Core)
            [centos-release] CentOS Linux release 7.6.1810 (Core)
            [os-release] CentOS Linux 7 (Core) 7 (Core)
  RHN:      (missing)
  RHSM:     (missing)
  YUM:      2 enabled plugins: fastestmirror, langpacks
  Runlevel: N 5  (default graphical)
  SELinux:  enforcing  (default enforcing)
  Arch:     mach=x86_64  cpu=x86_64  platform=x86_64
  Kernel:
    Booted kernel:  3.10.0-957.el7.x86_64
    GRUB default:   3.10.0-957.el7.x86_64
    Build version:
      Linux version 3.10.0-957.el7.x86_64 ([email protected]) (gcc version 4.8.5 20150623 (Red
      Hat 4.8.5-36) (GCC) ) #1 SMP Thu Nov 8 23:39:32 UTC 2018
    Booted kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    GRUB default kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    Taint-check: 0  (kernel untainted)
    - - - - - - - - - - - - - - - - - - -
  Sys time:  Sun May 12 10:05:21 CDT 2019
  Boot time: Sun May 12 09:50:20 CDT 2019  (epoch: 1557672620)
  Time Zone: America/Chicago
  Uptime:    15 min,  1 user
  LoadAvg:   [1 CPU] 0.00 (0%), 0.04 (4%), 0.09 (9%)
  /proc/stat:
    procs_running: 2   procs_blocked: 0    processes [Since boot]: 6423
    cpu [Utilization since boot]:
      us 1%, ni 0%, sys 1%, idle 99%, iowait 0%, irq 0%, sftirq 0%, steal 0%
```

### 如何使用 xsos 命令在 Linux 中查看生成的 SOSReport 输出？

我们需要份 SOSReport 以使用 `xsos` 命令进一步阅读。

是的，我已经生成了一个 SOSReport，文件如下。

```
# ls -lls -lh /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa.tar.xz
9.8M -rw-------. 1 root root 9.8M May 12 10:13 /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa.tar.xz
```

运行如下命令解开它。

```
# tar xf sosreport-CentOS7-01-1005-2019-05-12-pomeqsa.tar.xz
```

要查看全部信息，带上 `-a` 或 `--all` 开关运行 `xsos`：

```
# xsos --all /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
```

要查看 BIOS 信息，带上 `-b` 或 `--bios` 开关运行 `xsos`。

```
# xsos --bios /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
DMIDECODE
  BIOS:
    Vend: innotek GmbH
    Vers: VirtualBox
    Date: 12/01/2006
    BIOS Rev:
    FW Rev:
  System:
    Mfr:  innotek GmbH
    Prod: VirtualBox
    Vers: 1.2
    Ser:  0
    UUID: 002f47b8-2af2-48f5-be1d-67b67e03514c
  CPU:
    0 of 0 CPU sockets populated, 0 cores/0 threads per CPU
    0 total cores, 0 total threads
    Mfr:
    Fam:
    Freq:
    Vers:
  Memory:
    Total: 0 MiB (0 GiB)
    DIMMs: 0 of 0 populated
    MaxCapacity: 0 MiB (0 GiB / 0.00 TiB)
```

要查看系统基本信息，如主机名、发行版、SELinux、内核信息、正常运行时间等，请使用 `-o` 或 `--os` 开关运行 `xsos`。

```
# xsos --os /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
OS
  Hostname: CentOS7.2daygeek.com
  Distro:   [redhat-release] CentOS Linux release 7.6.1810 (Core)
            [centos-release] CentOS Linux release 7.6.1810 (Core)
            [os-release] CentOS Linux 7 (Core) 7 (Core)
  RHN:      (missing)
  RHSM:     (missing)
  YUM:      2 enabled plugins: fastestmirror, langpacks
  SELinux:  enforcing  (default enforcing)
  Arch:     mach=x86_64  cpu=x86_64  platform=x86_64
  Kernel:
    Booted kernel:  3.10.0-957.el7.x86_64
    GRUB default:   3.10.0-957.el7.x86_64
    Build version:
      Linux version 3.10.0-957.el7.x86_64 ([email protected]) (gcc version 4.8.5 20150623 (Red
      Hat 4.8.5-36) (GCC) ) #1 SMP Thu Nov 8 23:39:32 UTC 2018
    Booted kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    GRUB default kernel cmdline:
      root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet
      LANG=en_US.UTF-8
    Taint-check: 536870912  (see https://access.redhat.com/solutions/40594)
      29  TECH_PREVIEW: Technology Preview code is loaded
    - - - - - - - - - - - - - - - - - - -
  Sys time:  Sun May 12 10:12:22 CDT 2019
  Boot time: Sun May 12 09:50:20 CDT 2019  (epoch: 1557672620)
  Time Zone: America/Chicago
  Uptime:    22 min,  1 user
  LoadAvg:   [1 CPU] 1.19 (119%), 0.27 (27%), 0.14 (14%)
  /proc/stat:
    procs_running: 8   procs_blocked: 2    processes [Since boot]: 9005
    cpu [Utilization since boot]:
      us 1%, ni 0%, sys 1%, idle 99%, iowait 0%, irq 0%, sftirq 0%, steal 0%
```

要查看 kdump 配置，请使用 `-k` 或 `--kdump` 开关运行 `xsos`。

```
# xsos --kdump /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
KDUMP CONFIG
  kexec-tools rpm version:
    kexec-tools-2.0.15-21.el7.x86_64
  Service enablement:
    UNIT           STATE
    kdump.service  enabled
  kdump initrd/initramfs:
    13585734 Feb 19 05:51 initramfs-3.10.0-957.el7.x86_64kdump.img
  Memory reservation config:
    /proc/cmdline { crashkernel=auto }
    GRUB default  { crashkernel=auto }
  Actual memory reservation per /proc/iomem:
      2a000000-340fffff : Crash kernel
  kdump.conf:
    path /var/crash
    core_collector makedumpfile -l --message-level 1 -d 31
  kdump.conf "path" available space:
    System MemTotal (uncompressed core size) { 1.80 GiB }
    Available free space on target path's fs { 22.68 GiB }  (fs=/)
  Panic sysctls:
    kernel.sysrq [bitmask] =  "16"  (see proc man page)
    kernel.panic [secs] =  0  (no autoreboot on panic)
    kernel.hung_task_panic =  0
    kernel.panic_on_oops =  1
    kernel.panic_on_io_nmi =  0
    kernel.panic_on_unrecovered_nmi =  0
    kernel.panic_on_stackoverflow =  0
    kernel.softlockup_panic =  0
    kernel.unknown_nmi_panic =  0
    kernel.nmi_watchdog =  1
    vm.panic_on_oom [0-2] =  0  (no panic)
```

要查看有关 CPU 的信息，请使用 `-c` 或 `--cpu` 开关运行 `xsos`。

```
# xsos --cpu /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
CPU
  1 logical processors
  1 Intel Core i7-6700HQ CPU @ 2.60GHz (flags: aes,constant_tsc,ht,lm,nx,pae,rdrand)
```

要查看内存利用情况，请使用 `-m` 或 `--mem` 开关运行 `xsos`。

```
# xsos --mem /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
MEMORY
  Stats graphed as percent of MemTotal:
    MemUsed    ▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊.....................  58.8%
    Buffers    ..................................................   0.6%
    Cached     ▊▊▊▊▊▊▊▊▊▊▊▊▊▊▊...................................  29.9%
    HugePages  ..................................................   0.0%
    Dirty      ..................................................   0.7%
  RAM:
    1.8 GiB total ram
    1.1 GiB (59%) used
    0.5 GiB (28%) used excluding Buffers/Cached
    0.01 GiB (1%) dirty
  HugePages:
    No ram pre-allocated to HugePages
  LowMem/Slab/PageTables/Shmem:
    0.09 GiB (5%) of total ram used for Slab
    0.02 GiB (1%) of total ram used for PageTables
    0.01 GiB (1%) of total ram used for Shmem
  Swap:
    0 GiB (0%) used of 2 GiB total
```

要查看添加的磁盘信息，请使用 `-d` 和 `-disks` 开关运行 `xsos`。

```
# xsos --disks /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
STORAGE
  Whole Disks from /proc/partitions:
    2 disks, totaling 40 GiB (0.04 TiB)
    - - - - - - - - - - - - - - - - - - - - -
    Disk    Size in GiB
    ----    -----------
    sda     30
    sdb     10
```

要查看网络接口配置，请使用 `-e` 或 `--ethtool` 开关运行 `xsos`。

```
# xsos --ethtool /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
ETHTOOL
  Interface Status:
    enp0s10     0000:00:0a.0  link=up 1000Mb/s full (autoneg=Y)  rx ring 256/4096  drv e1000 v7.3.21-k8-NAPI / fw UNKNOWN
    enp0s9      0000:00:09.0  link=up 1000Mb/s full (autoneg=Y)  rx ring 256/4096  drv e1000 v7.3.21-k8-NAPI / fw UNKNOWN
    virbr0      N/A           link=DOWN                          rx ring UNKNOWN   drv bridge v2.3 / fw N/A
    virbr0-nic  tap           link=DOWN                          rx ring UNKNOWN   drv tun v1.6 / fw UNKNOWN
```

要查看有关 IP 地址的信息，请使用 `-i` 或 `--ip` 开关运行 `xsos`。

```
# xsos --ip /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
IP4
  Interface   Master IF  MAC Address        MTU     State  IPv4 Address
  =========   =========  =================  ======  =====  ==================
  lo          -          -                  65536   up     127.0.0.1/8
  enp0s9      -          08:00:27:0b:bc:e9  1500    up     192.168.1.8/24
  enp0s10     -          08:00:27:b2:08:91  1500    up     192.168.1.9/24
  virbr0      -          52:54:00:ae:01:94  1500    up     192.168.122.1/24
  virbr0-nic  virbr0     52:54:00:ae:01:94  1500    DOWN   -

IP6
  Interface   Master IF  MAC Address        MTU     State  IPv6 Address                                 Scope
  =========   =========  =================  ======  =====  ===========================================  =====
  lo          -          -                  65536   up     ::1/128                                      host
  enp0s9      -          08:00:27:0b:bc:e9  1500    up     fe80::945b:8333:f4bc:9723/64                 link
  enp0s10     -          08:00:27:b2:08:91  1500    up     fe80::7ed4:1fab:23c3:3790/64                 link
  virbr0      -          52:54:00:ae:01:94  1500    up     -                                            -
  virbr0-nic  virbr0     52:54:00:ae:01:94  1500    DOWN   -                                            -
```

要通过 `ps` 查看正在运行的进程，请使用 `-p` 或 `--ps` 开关运行 `xsos`。

```
# xsos --ps /var/tmp/sosreport-CentOS7-01-1005-2019-05-12-pomeqsa
PS CHECK
  Total number of threads/processes:
    501 / 171
  Top users of CPU & MEM:
    USER     %CPU   %MEM   RSS
    root     20.6%  14.1%  0.30 GiB
    gdm      0.3%   16.8%  0.33 GiB
    postfix  0.0%   0.6%   0.01 GiB
    polkitd  0.0%   0.6%   0.01 GiB
    daygeek  0.0%   0.2%   0.00 GiB
    colord   0.0%   0.4%   0.01 GiB
  Uninteruptible sleep threads/processes (0/0):
    [None]
  Defunct zombie threads/processes (0/0):
    [None]
  Top CPU-using processes:
    USER      PID   %CPU  %MEM  VSZ-MiB  RSS-MiB  TTY    STAT  START  TIME  COMMAND
    root      6542  15.6  4.2   875      78       pts/0  Sl+   10:11  0:07  /usr/bin/python /sbin/sosreport
    root      7582  3.0   0.1   10       2        pts/0  S     10:12  0:00  /bin/bash /usr/sbin/dracut --print-cmdline
    root      7969  0.7   0.1   95       4        ?      Ss    10:12  0:00  /usr/sbin/certmonger -S -p
    root      7889  0.4   0.2   24       4        ?      Ss    10:12  0:00  /usr/lib/systemd/systemd-hostnamed
    gdm       3866  0.3   7.1   2856     131      ?      Sl    09:50  0:04  /usr/bin/gnome-shell
    root      8553  0.2   0.1   47       3        ?      S     10:12  0:00  /usr/lib/systemd/systemd-udevd
    root      6971  0.2   0.4   342      9        ?      Sl    10:12  0:00  /usr/sbin/abrt-dbus -t133
    root      3200  0.2   0.9   982      18       ?      Ssl   09:50  0:02  /usr/sbin/libvirtd
    root      2855  0.1   0.1   88       3        ?      Ss    09:50  0:01  /sbin/rngd -f
    rtkit     2826  0.0   0.0   194      2        ?      SNsl  09:50  0:00  /usr/libexec/rtkit-daemon
  Top MEM-using processes:
    USER      PID   %CPU  %MEM  VSZ-MiB  RSS-MiB  TTY    STAT  START  TIME  COMMAND
    gdm       3866  0.3   7.1   2856     131      ?      Sl    09:50  0:04  /usr/bin/gnome-shell
    root      6542  15.6  4.2   875      78       pts/0  Sl+   10:11  0:07  /usr/bin/python /sbin/sosreport
    root      3264  0.0   1.2   271      23       tty1   Ssl+  09:50  0:00  /usr/bin/X :0 -background
    root      3200  0.2   0.9   982      18       ?      Ssl   09:50  0:02  /usr/sbin/libvirtd
    root      3189  0.0   0.9   560      17       ?      Ssl   09:50  0:00  /usr/bin/python2 -Es /usr/sbin/tuned
    gdm       4072  0.0   0.9   988      17       ?      Sl    09:50  0:00  /usr/libexec/gsd-media-keys
    gdm       4076  0.0   0.8   625      16       ?      Sl    09:50  0:00  /usr/libexec/gsd-power
    gdm       4056  0.0   0.8   697      16       ?      Sl    09:50  0:00  /usr/libexec/gsd-color
    root      2853  0.0   0.7   622      14       ?      Ssl   09:50  0:00  /usr/sbin/NetworkManager --no-daemon
    gdm       4110  0.0   0.7   544      14       ?      Sl    09:50  0:00  /usr/libexec/gsd-wacom
  Top thread-spawning processes:
    #   USER      PID   %CPU  %MEM  VSZ-MiB  RSS-MiB  TTY    STAT  START  TIME  COMMAND
    17  root      3200  0.2   0.9   982      18       ?      -     09:50  0:02  /usr/sbin/libvirtd
    12  root      6542  16.1  4.5   876      83       pts/0  -     10:11  0:07  /usr/bin/python /sbin/sosreport
    10  gdm       3866  0.3   7.1   2856     131      ?      -     09:50  0:04  /usr/bin/gnome-shell
    7   polkitd   2864  0.0   0.6   602      13       ?      -     09:50  0:01  /usr/lib/polkit-1/polkitd --no-debug
    6   root      2865  0.0   0.0   203      1        ?      -     09:50  0:00  /usr/sbin/gssproxy -D
    5   root      3189  0.0   0.9   560      17       ?      -     09:50  0:00  /usr/bin/python2 -Es /usr/sbin/tuned
    5   root      2823  0.0   0.3   443      6        ?      -     09:50  0:00  /usr/libexec/udisks2/udisksd
    5   gdm       4102  0.0   0.2   461      5        ?      -     09:50  0:00  /usr/libexec/gsd-smartcard
    4   root      3215  0.0   0.2   470      4        ?      -     09:50  0:00  /usr/sbin/gdm
    4   gdm       4106  0.0   0.2   444      5        ?      -     09:50  0:00  /usr/libexec/gsd-sound
```

---

via: <https://www.2daygeek.com/xsos-a-tool-to-read-sosreport-in-linux/>

作者：[Magesh Maruthamuthu](https://www.2daygeek.com/author/magesh/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[wxy](https://github.com/wxy) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用深度学习检测疟疾

Summary: 人工智能结合开源硬件工具能够提升严重传染病疟疾的诊断。

人工智能结合开源硬件工具能够提升严重传染病疟疾的诊断。

# 使用深度学习检测疟疾

>
> 人工智能结合开源硬件工具能够提升严重传染病疟疾的诊断。
>
>
>

人工智能（AI）和开源工具、技术和框架是促进社会进步的强有力的结合。“健康就是财富”可能有点陈词滥调，但它却是非常准确的！在本篇文章，我们将测试 AI 是如何与低成本、有效、精确的开源深度学习方法结合起来一起用来检测致死的传染病疟疾。

我既不是一个医生，也不是一个医疗保健研究者，我也绝不像他们那样合格，我只是对将 AI 应用到医疗保健研究感兴趣。在这片文章中我的想法是展示 AI 和开源解决方案如何帮助疟疾检测和减少人工劳动的方法。

*Python 和 TensorFlow: 一个构建开源深度学习方法的很棒的结合*

感谢 Python 的强大和像 TensorFlow 这样的深度学习框架，我们能够构建健壮的、大规模的、有效的深度学习方法。因为这些工具是自由和开源的，我们能够构建非常经济且易于被任何人采纳和使用的解决方案。让我们开始吧！

### 项目动机

疟疾是由*疟原虫*造成的致死的、有传染性的、蚊子传播的疾病，主要通过受感染的雌性按蚊叮咬传播。共有五种寄生虫能够引起疟疾，但是大多数病例是这两种类型造成的：恶性疟原虫和间日疟原虫。

这个地图显示了疟疾在全球传播分布形势，尤其在热带地区，但疾病的性质和致命性是该项目的主要动机。

如果一只受感染雌性蚊子叮咬了你，蚊子携带的寄生虫进入你的血液，并且开始破坏携带氧气的红细胞（RBC）。通常，疟疾的最初症状类似于流感病毒，在蚊子叮咬后，他们通常在几天或几周内发作。然而，这些致死的寄生虫可以在你的身体里生存长达一年并且不会造成任何症状，延迟治疗可能造成并发症甚至死亡。因此，早期的检查能够挽救生命。

世界健康组织（WHO）的[疟疾实情](https://www.who.int/features/factfiles/malaria/en/)表明，世界近乎一半的人口面临疟疾的风险，有超过 2 亿的疟疾病例，每年由于疟疾造成的死亡将近 40 万。这是使疟疾检测和诊断快速、简单和有效的一个动机。

### 检测疟疾的方法

有几种方法能够用来检测和诊断疟疾。该文中的项目就是基于 Rajaraman, et al. 的论文：“[预先训练的卷积神经网络作为特征提取器，用于改善薄血涂片图像中的疟疾寄生虫检测](https://peerj.com/articles/4568/)”介绍的一些方法，包含聚合酶链反应（PCR）和快速诊断测试（RDT）。这两种测试通常用于无法提供高质量显微镜服务的地方。

标准的疟疾诊断通常是基于血液涂片工作流程的，根据 Carlos Ariza 的文章“[Malaria Hero：一个更快诊断疟原虫的网络应用](https://blog.insightdatascience.com/https-blog-insightdatascience-com-malaria-hero-a47d3d5fc4bb)”，我从中了解到 Adrian Rosebrock 的“[使用 Keras 的深度学习和医学图像分析](https://www.pyimagesearch.com/2018/12/03/deep-learning-and-medical-image-analysis-with-keras/)”。我感激这些优秀的资源的作者，让我在疟原虫预防、诊断和治疗方面有了更多的想法。

*一个疟原虫检测的血涂片工作流程*

根据 WHO 方案，诊断通常包括对放大 100 倍的血涂片的集中检测。受过训练的人们手工计算在 5000 个细胞中有多少红细胞中包含疟原虫。正如上述解释中引用的 Rajaraman， et al. 的论文：

>
> 厚血涂片有助于检测寄生虫的存在，而薄血涂片有助于识别引起感染的寄生虫种类（疾病控制和预防中心, 2012）。诊断准确性在很大程度上取决于诊断人的专业知识，并且可能受到观察者间差异和疾病流行/资源受限区域大规模诊断所造成的不利影响（Mitiku, Mengistu 和 Gelaw, 2003）。可替代的技术是使用聚合酶链反应（PCR）和快速诊断测试（RDT）；然而，PCR 分析受限于它的性能（Hommelsheim, et al., 2014），RDT 在疾病流行的地区成本效益低（Hawkes, Katsuva 和 Masumbuko, 2009）。
>
>
>

因此，疟疾检测可能受益于使用机器学习的自动化。

### 疟疾检测的深度学习

人工诊断血涂片是一个繁重的手工过程，需要专业知识来分类和计数被寄生虫感染的和未感染的细胞。这个过程可能不能很好的规模化，尤其在那些专业人士不足的地区。在利用最先进的图像处理和分析技术提取人工选取特征和构建基于机器学习的分类模型方面取得了一些进展。然而，这些模型不能大规模推广，因为没有更多的数据用来训练，并且人工选取特征需要花费很长时间。

深度学习模型，或者更具体地讲，卷积神经网络（CNN），已经被证明在各种计算机视觉任务中非常有效。（如果你想更多的了解关于 CNN 的背景知识，我推荐你阅读[视觉识别的 CS2331n 卷积神经网络](http://cs231n.github.io/convolutional-networks/)。）简单地讲，CNN 模型的关键层包含卷积和池化层，正如下图所示。

*一个典型的 CNN 架构*

卷积层从数据中学习空间层级模式，它是平移不变的，因此它们能够学习图像的不同方面。例如，第一个卷积层将学习小的和局部图案，例如边缘和角落，第二个卷积层将基于第一层的特征学习更大的图案，等等。这允许 CNN 自动化提取特征并且学习对于新数据点通用的有效的特征。池化层有助于下采样和减少尺寸。

因此，CNN 有助于自动化和规模化的特征工程。同样，在模型末尾加上密集层允许我们执行像图像分类这样的任务。使用像 CNN 这样的深度学习模型自动的疟疾检测可能非常有效、便宜和具有规模性，尤其是迁移学习和预训练模型效果非常好，甚至在少量数据的约束下。

Rajaraman, et al. 的论文在一个数据集上利用六个预训练模型在检测疟疾对比无感染样本获取到令人吃惊的 95.9% 的准确率。我们的重点是从头开始尝试一些简单的 CNN 模型和用一个预训练的训练模型使用迁移学习来查看我们能够从相同的数据集中得到什么。我们将使用开源工具和框架，包括 Python 和 TensorFlow，来构建我们的模型。

### 数据集

我们分析的数据来自 Lister Hill 国家生物医学交流中心（LHNCBC）的研究人员，该中心是国家医学图书馆（NLM）的一部分，他们细心收集和标记了公开可用的健康和受感染的血涂片图像的[数据集](https://ceb.nlm.nih.gov/repositories/malaria-datasets/)。这些研究者已经开发了一个运行在 Android 智能手机的[疟疾检测手机应用](https://www.ncbi.nlm.nih.gov/pubmed/29360430)，连接到一个传统的光学显微镜。它们使用吉姆萨染液将 150 个受恶性疟原虫感染的和 50 个健康病人的薄血涂片染色，这些薄血涂片是在孟加拉的吉大港医学院附属医院收集和照相的。使用智能手机的内置相机获取每个显微镜视窗内的图像。这些图片由在泰国曼谷的马希多-牛津热带医学研究所的一个专家使用幻灯片阅读器标记的。

让我们简要地查看一下数据集的结构。首先，我将安装一些基础的依赖（基于使用的操作系统）。

我使用的是云上的带有一个 GPU 的基于 Debian 的操作系统，这样我能更快的运行我的模型。为了查看目录结构，我们必须使用 `sudo apt install tree` 安装 `tree` 及其依赖（如果我们没有安装的话）。

我们有两个文件夹包含血细胞的图像，包括受感染的和健康的。我们通过输入可以获取关于图像总数更多的细节：

```
import os
import glob

base_dir = os.path.join('./cell_images')
infected_dir = os.path.join(base_dir,'Parasitized')
healthy_dir = os.path.join(base_dir,'Uninfected')

infected_files = glob.glob(infected_dir+'/*.png')
healthy_files = glob.glob(healthy_dir+'/*.png')
len(infected_files), len(healthy_files)

# Output
(13779, 13779)
```

看起来我们有一个平衡的数据集，包含 13,779 张疟疾的和 13,779 张非疟疾的（健康的）血细胞图像。让我们根据这些构建数据帧，我们将用这些数据帧来构建我们的数据集。

```
import numpy as np
import pandas as pd

np.random.seed(42)

files_df = pd.DataFrame({
    'filename': infected_files + healthy_files,
    'label': ['malaria'] * len(infected_files) + ['healthy'] * len(healthy_files)
}).sample(frac=1, random_state=42).reset_index(drop=True)

files_df.head()
```

### 构建和了解图像数据集

为了构建深度学习模型，我们需要训练数据，但是我们还需要使用不可见的数据测试模型的性能。相应的，我们将使用 60:10:30 的比例来划分用于训练、验证和测试的数据集。我们将在训练期间应用训练和验证数据集，并用测试数据集来检查模型的性能。

```
from sklearn.model_selection import train_test_split
from collections import Counter

train_files, test_files, train_labels, test_labels = train_test_split(files_df['filename'].values,
                                                                      files_df['label'].values,
                                                                      test_size=0.3, random_state=42)
train_files, val_files, train_labels, val_labels = train_test_split(train_files,
                                                                    train_labels,
                                                                    test_size=0.1, random_state=42)

print(train_files.shape, val_files.shape, test_files.shape)
print('Train:', Counter(train_labels), '\nVal:', Counter(val_labels), '\nTest:', Counter(test_labels))

# Output
(17361,) (1929,) (8268,)
Train: Counter({'healthy': 8734, 'malaria': 8627})
Val: Counter({'healthy': 970, 'malaria': 959})
Test: Counter({'malaria': 4193, 'healthy': 4075})
```

这些图片尺寸并不相同，因为血涂片和细胞图像是基于人、测试方法、图片方向不同而不同的。让我们总结我们的训练数据集的统计信息来决定最佳的图像尺寸（牢记，我们根本不会碰测试数据集）。

```
import cv2
from concurrent import futures
import threading

def get_img_shape_parallel(idx, img, total_imgs):
    if idx % 5000 == 0 or idx == (total_imgs - 1):
        print('{}: working on img num: {}'.format(threading.current_thread().name,
                                                  idx))
    return cv2.imread(img).shape

ex = futures.ThreadPoolExecutor(max_workers=None)
data_inp = [(idx, img, len(train_files)) for idx, img in enumerate(train_files)]
print('Starting Img shape computation:')
train_img_dims_map = ex.map(get_img_shape_parallel,
                            [record[0] for record in data_inp],
                            [record[1] for record in data_inp],
                            [record[2] for record in data_inp])
train_img_dims = list(train_img_dims_map)
print('Min Dimensions:', np.min(train_img_dims, axis=0))
print('Avg Dimensions:', np.mean(train_img_dims, axis=0))
print('Median Dimensions:', np.median(train_img_dims, axis=0))
print('Max Dimensions:', np.max(train_img_dims, axis=0))

# Output
Starting Img shape computation:
ThreadPoolExecutor-0_0: working on img num: 0
ThreadPoolExecutor-0_17: working on img num: 5000
ThreadPoolExecutor-0_15: working on img num: 10000
ThreadPoolExecutor-0_1: working on img num: 15000
ThreadPoolExecutor-0_7: working on img num: 17360
Min Dimensions: [46 46  3]
Avg Dimensions: [132.77311215 132.45757733   3.]
Median Dimensions: [130. 130.   3.]
Max Dimensions: [385 394   3]
```

我们应用并行处理来加速图像读取，并且基于汇总统计结果，我们将每幅图片的尺寸重新调整到 125x125 像素。让我们载入我们所有的图像并重新调整它们为这些固定尺寸。

```
IMG_DIMS = (125, 125)

def get_img_data_parallel(idx, img, total_imgs):
    if idx % 5000 == 0 or idx == (total_imgs - 1):
        print('{}: working on img num: {}'.format(threading.current_thread().name,
                                                  idx))
    img = cv2.imread(img)
    img = cv2.resize(img, dsize=IMG_DIMS,
                     interpolation=cv2.INTER_CUBIC)
    img = np.array(img, dtype=np.float32)
    return img

ex = futures.ThreadPoolExecutor(max_workers=None)
train_data_inp = [(idx, img, len(train_files)) for idx, img in enumerate(train_files)]
val_data_inp = [(idx, img, len(val_files)) for idx, img in enumerate(val_files)]
test_data_inp = [(idx, img, len(test_files)) for idx, img in enumerate(test_files)]

print('Loading Train Images:')
train_data_map = ex.map(get_img_data_parallel,
                        [record[0] for record in train_data_inp],
                        [record[1] for record in train_data_inp],
                        [record[2] for record in train_data_inp])
train_data = np.array(list(train_data_map))

print('\nLoading Validation Images:')
val_data_map = ex.map(get_img_data_parallel,
                        [record[0] for record in val_data_inp],
                        [record[1] for record in val_data_inp],
                        [record[2] for record in val_data_inp])
val_data = np.array(list(val_data_map))

print('\nLoading Test Images:')
test_data_map = ex.map(get_img_data_parallel,
                        [record[0] for record in test_data_inp],
                        [record[1] for record in test_data_inp],
                        [record[2] for record in test_data_inp])
test_data = np.array(list(test_data_map))

train_data.shape, val_data.shape, test_data.shape

# Output
Loading Train Images:
ThreadPoolExecutor-1_0: working on img num: 0
ThreadPoolExecutor-1_12: working on img num: 5000
ThreadPoolExecutor-1_6: working on img num: 10000
ThreadPoolExecutor-1_10: working on img num: 15000
ThreadPoolExecutor-1_3: working on img num: 17360

Loading Validation Images:
ThreadPoolExecutor-1_13: working on img num: 0
ThreadPoolExecutor-1_18: working on img num: 1928

Loading Test Images:
ThreadPoolExecutor-1_5: working on img num: 0
ThreadPoolExecutor-1_19: working on img num: 5000
ThreadPoolExecutor-1_8: working on img num: 8267
((17361, 125, 125, 3), (1929, 125, 125, 3), (8268, 125, 125, 3))
```

我们再次应用并行处理来加速有关图像载入和重新调整大小的计算。最终，我们获得了所需尺寸的图片张量，正如前面的输出所示。我们现在查看一些血细胞图像样本，以对我们的数据有个印象。

```
import matplotlib.pyplot as plt
%matplotlib inline

plt.figure(1 , figsize = (8 , 8))
n = 0
for i in range(16):
    n += 1
    r = np.random.randint(0 , train_data.shape[0] , 1)
    plt.subplot(4 , 4 , n)
    plt.subplots_adjust(hspace = 0.5 , wspace = 0.5)
    plt.imshow(train_data[r[0]]/255.)
    plt.title('{}'.format(train_labels[r[0]]))
    plt.xticks([]) , plt.yticks([])
```

基于这些样本图像，我们看到一些疟疾和健康细胞图像的细微不同。我们将使我们的深度学习模型试图在模型训练中学习这些模式。

开始我们的模型训练前，我们必须建立一些基础的配置设置。

```
BATCH_SIZE = 64
NUM_CLASSES = 2
EPOCHS = 25
INPUT_SHAPE = (125, 125, 3)

train_imgs_scaled = train_data / 255.
val_imgs_scaled = val_data / 255.

# encode text category labels
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
le.fit(train_labels)
train_labels_enc = le.transform(train_labels)
val_labels_enc = le.transform(val_labels)

print(train_labels[:6], train_labels_enc[:6])

# Output
['malaria' 'malaria' 'malaria' 'healthy' 'healthy' 'malaria'] [1 1 1 0 0 1]
```

我们修复我们的图像尺寸、批量大小，和纪元，并编码我们的分类的类标签。TensorFlow 2.0 于 2019 年三月发布，这个练习是尝试它的完美理由。

```
import tensorflow as tf

# Load the TensorBoard notebook extension (optional)
%load_ext tensorboard.notebook

tf.random.set_seed(42)
tf.__version__

# Output
'2.0.0-alpha0'
```

### 深度学习训练

在模型训练阶段，我们将构建三个深度训练模型，使用我们的训练集训练，使用验证数据比较它们的性能。然后，我们保存这些模型并在之后的模型评估阶段使用它们。

#### 模型 1：从头开始的 CNN

我们的第一个疟疾检测模型将从头开始构建和训练一个基础的 CNN。首先，让我们定义我们的模型架构，

```
inp = tf.keras.layers.Input(shape=INPUT_SHAPE)

conv1 = tf.keras.layers.Conv2D(32, kernel_size=(3, 3),
                               activation='relu', padding='same')(inp)
pool1 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv1)
conv2 = tf.keras.layers.Conv2D(64, kernel_size=(3, 3),
                               activation='relu', padding='same')(pool1)
pool2 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv2)
conv3 = tf.keras.layers.Conv2D(128, kernel_size=(3, 3),
                               activation='relu', padding='same')(pool2)
pool3 = tf.keras.layers.MaxPooling2D(pool_size=(2, 2))(conv3)

flat = tf.keras.layers.Flatten()(pool3)

hidden1 = tf.keras.layers.Dense(512, activation='relu')(flat)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=inp, outputs=out)
model.compile(optimizer='adam',
                loss='binary_crossentropy',
                metrics=['accuracy'])
model.summary()

# Output
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_1 (InputLayer)         [(None, 125, 125, 3)]     0
_________________________________________________________________
conv2d (Conv2D)              (None, 125, 125, 32)      896
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 62, 62, 32)        0
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 62, 62, 64)        18496
_________________________________________________________________
...
...
_________________________________________________________________
dense_1 (Dense)              (None, 512)               262656
_________________________________________________________________
dropout_1 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 513
=================================================================
Total params: 15,102,529
Trainable params: 15,102,529
Non-trainable params: 0
_________________________________________________________________
```

基于这些代码的架构，我们的 CNN 模型有三个卷积和一个池化层，其后是两个致密层，以及用于正则化的失活。让我们训练我们的模型。

```
import datetime

logdir = os.path.join('/home/dipanzan_sarkar/projects/tensorboard_logs',
                      datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                              patience=2, min_lr=0.000001)
callbacks = [reduce_lr, tensorboard_callback]

history = model.fit(x=train_imgs_scaled, y=train_labels_enc,
                    batch_size=BATCH_SIZE,
                    epochs=EPOCHS,
                    validation_data=(val_imgs_scaled, val_labels_enc),
                    callbacks=callbacks,
                    verbose=1)

# Output
Train on 17361 samples, validate on 1929 samples
Epoch 1/25
17361/17361 [====] - 32s 2ms/sample - loss: 0.4373 - accuracy: 0.7814 - val_loss: 0.1834 - val_accuracy: 0.9393
Epoch 2/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.1725 - accuracy: 0.9434 - val_loss: 0.1567 - val_accuracy: 0.9513
...
...
Epoch 24/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.0036 - accuracy: 0.9993 - val_loss: 0.3693 - val_accuracy: 0.9565
Epoch 25/25
17361/17361 [====] - 30s 2ms/sample - loss: 0.0034 - accuracy: 0.9994 - val_loss: 0.3699 - val_accuracy: 0.9559
```

我们获得了 95.6% 的验证精确率，这很好，尽管我们的模型看起来有些过拟合（通过查看我们的训练精确度，是 99.9%）。通过绘制训练和验证的精度和损失曲线，我们可以清楚地看到这一点。

```
f, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
t = f.suptitle('Basic CNN Performance', fontsize=12)
f.subplots_adjust(top=0.85, wspace=0.3)

max_epoch = len(history.history['accuracy'])+1
epoch_list = list(range(1,max_epoch))
ax1.plot(epoch_list, history.history['accuracy'], label='Train Accuracy')
ax1.plot(epoch_list, history.history['val_accuracy'], label='Validation Accuracy')
ax1.set_xticks(np.arange(1, max_epoch, 5))
ax1.set_ylabel('Accuracy Value')
ax1.set_xlabel('Epoch')
ax1.set_title('Accuracy')
l1 = ax1.legend(loc="best")

ax2.plot(epoch_list, history.history['loss'], label='Train Loss')
ax2.plot(epoch_list, history.history['val_loss'], label='Validation Loss')
ax2.set_xticks(np.arange(1, max_epoch, 5))
ax2.set_ylabel('Loss Value')
ax2.set_xlabel('Epoch')
ax2.set_title('Loss')
l2 = ax2.legend(loc="best")
```

*基础 CNN 学习曲线*

我们可以看在在第五个纪元，情况并没有改善很多。让我们保存这个模型用于将来的评估。

```
model.save('basic_cnn.h5')
```

#### 深度迁移学习

就像人类有与生俱来在不同任务间传输知识的能力一样，迁移学习允许我们利用从以前任务学到的知识用到新的相关的任务，即使在机器学习或深度学习的情况下也是如此。如果想深入探究迁移学习，你应该看我的文章“[一个易于理解与现实应用一起学习深度学习中的迁移学习的指导实践](https://towardsdatascience.com/a-comprehensive-hands-on-guide-to-transfer-learning-with-real-world-applications-in-deep-learning-212bf3b2f27a)”和我的书《[Python 迁移学习实践](https://github.com/dipanjanS/hands-on-transfer-learning-with-python)》。

在这篇实践中我们想要探索的想法是：

>
> 在我们的问题背景下，我们能够利用一个预训练深度学习模型（在大数据集上训练的，像 ImageNet）通过应用和迁移知识来解决疟疾检测的问题吗？
>
>
>

我们将应用两个最流行的深度迁移学习策略。

* 预训练模型作为特征提取器
* 微调的预训练模型

我们将使用预训练的 VGG-19 深度训练模型（由剑桥大学的视觉几何组（VGG）开发）进行我们的实验。像 VGG-19 这样的预训练模型是在一个大的数据集（[Imagenet](http://image-net.org/index)）上使用了很多不同的图像分类训练的。因此，这个模型应该已经学习到了健壮的特征层级结构，相对于你的 CNN 模型学到的特征，是空间不变的、转动不变的、平移不变的。因此，这个模型，已经从百万幅图片中学习到了一个好的特征显示，对于像疟疾检测这样的计算机视觉问题，可以作为一个好的合适新图像的特征提取器。在我们的问题中发挥迁移学习的能力之前，让我们先讨论 VGG-19 模型。

##### 理解 VGG-19 模型

VGG-19 模型是一个构建在 ImageNet 数据库之上的 19 层（卷积和全连接的）的深度学习网络，ImageNet 数据库为了图像识别和分类的目的而开发。该模型是由 Karen Simonyan 和 Andrew Zisserman 构建的，在他们的论文“[大规模图像识别的非常深的卷积网络](https://arxiv.org/pdf/1409.1556.pdf)”中进行了描述。VGG-19 的架构模型是：

你可以看到我们总共有 16 个使用 3x3 卷积过滤器的卷积层，与最大的池化层来下采样，和由 4096 个单元组成的两个全连接的隐藏层，每个隐藏层之后跟随一个由 1000 个单元组成的致密层，每个单元代表 ImageNet 数据库中的一个分类。我们不需要最后三层，因为我们将使用我们自己的全连接致密层来预测疟疾。我们更关心前五个块，因此我们可以利用 VGG 模型作为一个有效的特征提取器。

我们将使用模型之一作为一个简单的特征提取器，通过冻结五个卷积块的方式来确保它们的位权在每个纪元后不会更新。对于最后一个模型，我们会对 VGG 模型进行微调，我们会解冻最后两个块（第 4 和第 5）因此当我们训练我们的模型时，它们的位权在每个时期（每批数据）被更新。

#### 模型 2：预训练的模型作为一个特征提取器

为了构建这个模型，我们将利用 TensorFlow 载入 VGG-19 模型并冻结卷积块，因此我们能够将它们用作特征提取器。我们在末尾插入我们自己的致密层来执行分类任务。

```
vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet',
                                        input_shape=INPUT_SHAPE)
vgg.trainable = False
# Freeze the layers
for layer in vgg.layers:
    layer.trainable = False

base_vgg = vgg
base_out = base_vgg.output
pool_out = tf.keras.layers.Flatten()(base_out)
hidden1 = tf.keras.layers.Dense(512, activation='relu')(pool_out)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=base_vgg.input, outputs=out)
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=1e-4),
                loss='binary_crossentropy',
                metrics=['accuracy'])
model.summary()

# Output
Model: "model_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
input_2 (InputLayer)         [(None, 125, 125, 3)]     0
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 125, 125, 64)      1792
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 125, 125, 64)      36928
_________________________________________________________________
...
...
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 3, 3, 512)         0
_________________________________________________________________
flatten_1 (Flatten)          (None, 4608)              0
_________________________________________________________________
dense_3 (Dense)              (None, 512)               2359808
_________________________________________________________________
dropout_2 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_4 (Dense)              (None, 512)               262656
_________________________________________________________________
dropout_3 (Dropout)          (None, 512)               0
_________________________________________________________________
dense_5 (Dense)              (None, 1)                 513
=================================================================
Total params: 22,647,361
Trainable params: 2,622,977
Non-trainable params: 20,024,384
_________________________________________________________________
```

从整个输出可以明显看出，在我们的模型中我们有了很多层，我们将只利用 VGG-19 模型的冻结层作为特征提取器。你可以使用下列代码来验证我们的模型有多少层是实际可训练的，以及我们的网络中总共存在多少层。

```
print("Total Layers:", len(model.layers))
print("Total trainable layers:",
      sum([1 for l in model.layers if l.trainable]))

# Output
Total Layers: 28
Total trainable layers: 6
```

我们将使用和我们之前的模型相似的配置和回调来训练我们的模型。参考[我的 GitHub 仓库](https://nbviewer.jupyter.org/github/dipanjanS/data_science_for_all/tree/master/os_malaria_detection/)以获取训练模型的完整代码。我们观察下列图表，以显示模型精确度和损失曲线。

*冻结的预训练的 CNN 的学习曲线*

这表明我们的模型没有像我们的基础 CNN 模型那样过拟合，但是性能有点不如我们的基础的 CNN 模型。让我们保存这个模型，以备将来的评估。

```
model.save('vgg_frozen.h5')
```

#### 模型 3：使用图像增强来微调预训练的模型

在我们的最后一个模型中，我们将在预定义好的 VGG-19 模型的最后两个块中微调层的位权。我们同样引入了图像增强的概念。图像增强背后的想法和其名字一样。我们从训练数据集中载入现有图像，并且应用转换操作，例如旋转、裁剪、转换、放大缩小等等，来产生新的、改变过的版本。由于这些随机转换，我们每次获取到的图像不一样。我们将应用 tf.keras 中的一个名为 ImageDataGenerator 的优秀工具来帮助构建图像增强器。

```
train_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255,
                                                                zoom_range=0.05,
                                                                rotation_range=25,
                                                                width_shift_range=0.05,
                                                                height_shift_range=0.05,
                                                                shear_range=0.05, horizontal_flip=True,
                                                                fill_mode='nearest')

val_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)

# build image augmentation generators
train_generator = train_datagen.flow(train_data, train_labels_enc, batch_size=BATCH_SIZE, shuffle=True)
val_generator = val_datagen.flow(val_data, val_labels_enc, batch_size=BATCH_SIZE, shuffle=False)
```

我们不会对我们的验证数据集应用任何转换（除非是调整大小，因为这是必须的），因为我们将使用它评估每个纪元的模型性能。对于在传输学习环境中的图像增强的详细解释，请随时查看我上面引用的[文章](https://towardsdatascience.com/a-comprehensive-hands-on-guide-to-transfer-learning-with-real-world-applications-in-deep-learning-212bf3b2f27a)。让我们从一批图像增强转换中查看一些样本结果。

```
img_id = 0
sample_generator = train_datagen.flow(train_data[img_id:img_id+1], train_labels[img_id:img_id+1],
                                      batch_size=1)
sample = [next(sample_generator) for i in range(0,5)]
fig, ax = plt.subplots(1,5, figsize=(16, 6))
print('Labels:', [item[1][0] for item in sample])
l = [ax[i].imshow(sample[i][0][0]) for i in range(0,5)]
```

你可以清晰的看到与之前的输出的我们图像的轻微变化。我们现在构建我们的学习模型，确保 VGG-19 模型的最后两块是可以训练的。

```
vgg = tf.keras.applications.vgg19.VGG19(include_top=False, weights='imagenet',
                                        input_shape=INPUT_SHAPE)
# Freeze the layers
vgg.trainable = True

set_trainable = False
for layer in vgg.layers:
    if layer.name in ['block5_conv1', 'block4_conv1']:
        set_trainable = True
    if set_trainable:
        layer.trainable = True
    else:
        layer.trainable = False

base_vgg = vgg
base_out = base_vgg.output
pool_out = tf.keras.layers.Flatten()(base_out)
hidden1 = tf.keras.layers.Dense(512, activation='relu')(pool_out)
drop1 = tf.keras.layers.Dropout(rate=0.3)(hidden1)
hidden2 = tf.keras.layers.Dense(512, activation='relu')(drop1)
drop2 = tf.keras.layers.Dropout(rate=0.3)(hidden2)

out = tf.keras.layers.Dense(1, activation='sigmoid')(drop2)

model = tf.keras.Model(inputs=base_vgg.input, outputs=out)
model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=1e-5),
                loss='binary_crossentropy',
                metrics=['accuracy'])

print("Total Layers:", len(model.layers))
print("Total trainable layers:", sum([1 for l in model.layers if l.trainable]))

# Output
Total Layers: 28
Total trainable layers: 16
```

在我们的模型中我们降低了学习率，因为我们不想在微调的时候对预训练的层做大的位权更新。模型的训练过程可能有轻微的不同，因为我们使用了数据生成器，因此我们将应用 `fit_generator(...)` 函数。

```
tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5,
                              patience=2, min_lr=0.000001)

callbacks = [reduce_lr, tensorboard_callback]
train_steps_per_epoch = train_generator.n // train_generator.batch_size
val_steps_per_epoch = val_generator.n // val_generator.batch_size
history = model.fit_generator(train_generator, steps_per_epoch=train_steps_per_epoch, epochs=EPOCHS,
                              validation_data=val_generator, validation_steps=val_steps_per_epoch,
                              verbose=1)

# Output
Epoch 1/25
271/271 [====] - 133s 489ms/step - loss: 0.2267 - accuracy: 0.9117 - val_loss: 0.1414 - val_accuracy: 0.9531
Epoch 2/25
271/271 [====] - 129s 475ms/step - loss: 0.1399 - accuracy: 0.9552 - val_loss: 0.1292 - val_accuracy: 0.9589
...
...
Epoch 24/25
271/271 [====] - 128s 473ms/step - loss: 0.0815 - accuracy: 0.9727 - val_loss: 0.1466 - val_accuracy: 0.9682
Epoch 25/25
271/271 [====] - 128s 473ms/step - loss: 0.0792 - accuracy: 0.9729 - val_loss: 0.1127 - val_accuracy: 0.9641
```

这看起来是我们的最好的模型。它给了我们近乎 96.5% 的验证精确率，基于训练精度，它看起来不像我们的第一个模型那样过拟合。这可以通过下列的学习曲线验证。

*微调过的预训练 CNN 的学习曲线*

让我们保存这个模型，因此我们能够在测试集上使用。

```
model.save('vgg_finetuned.h5')
```

这就完成了我们的模型训练阶段。现在我们准备好了在测试集上测试我们模型的性能。

### 深度学习模型性能评估

我们将通过在我们的测试集上做预测来评估我们在训练阶段构建的三个模型，因为仅仅验证是不够的！我们同样构建了一个检测工具模块叫做 `model_evaluation_utils`，我们可以使用相关分类指标用来评估使用我们深度学习模型的性能。第一步是扩展我们的数据集。

```
test_imgs_scaled = test_data / 255.
test_imgs_scaled.shape, test_labels.shape

# Output
((8268, 125, 125, 3), (8268,))
```

下一步包括载入我们保存的深度学习模型，在测试集上预测。

```
# Load Saved Deep Learning Models
basic_cnn = tf.keras.models.load_model('./basic_cnn.h5')
vgg_frz = tf.keras.models.load_model('./vgg_frozen.h5')
vgg_ft = tf.keras.models.load_model('./vgg_finetuned.h5')

# Make Predictions on Test Data
basic_cnn_preds = basic_cnn.predict(test_imgs_scaled, batch_size=512)
vgg_frz_preds = vgg_frz.predict(test_imgs_scaled, batch_size=512)
vgg_ft_preds = vgg_ft.predict(test_imgs_scaled, batch_size=512)

basic_cnn_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0
                                                  for pred in basic_cnn_preds.ravel()])
vgg_frz_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0
                                                  for pred in vgg_frz_preds.ravel()])
vgg_ft_pred_labels = le.inverse_transform([1 if pred > 0.5 else 0
                                                  for pred in vgg_ft_preds.ravel()])
```

下一步是应用我们的 `model_evaluation_utils` 模块根据相应分类指标来检查每个模块的性能。

```
import model_evaluation_utils as meu
import pandas as pd

basic_cnn_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=basic_cnn_pred_labels)
vgg_frz_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=vgg_frz_pred_labels)
vgg_ft_metrics = meu.get_metrics(true_labels=test_labels, predicted_labels=vgg_ft_pred_labels)

pd.DataFrame([basic_cnn_metrics, vgg_frz_metrics, vgg_ft_metrics],
             index=['Basic CNN', 'VGG-19 Frozen', 'VGG-19 Fine-tuned'])
```

看起来我们的第三个模型在我们的测试集上执行的最好，给出了一个模型精确性为 96% 的 F1 得分，这非常好，与我们之前提到的研究论文和文章中的更复杂的模型相当。

### 总结

疟疾检测不是一个简单的过程，全球的合格人员的不足在病例诊断和治疗当中是一个严重的问题。我们研究了一个关于疟疾的有趣的真实世界的医学影像案例。利用 AI 的、易于构建的、开源的技术在检测疟疾方面可以为我们提供最先进的精确性，因此使 AI 具有社会效益。

我鼓励你查看这篇文章中提到的文章和研究论文，没有它们，我就不能形成概念并写出来。如果你对运行和采纳这些技术感兴趣，本篇文章所有的代码都可以在[我的 GitHub 仓库](https://nbviewer.jupyter.org/github/dipanjanS/data_science_for_all/tree/master/os_malaria_detection/)获得。记得从[官方网站](https://ceb.nlm.nih.gov/repositories/malaria-datasets/)下载数据。

让我们希望在健康医疗方面更多的采纳开源的 AI 能力，使它在世界范围内变得更便宜、更易用。

---

via: <https://opensource.com/article/19/4/detecting-malaria-deep-learning>

作者：[Dipanjan (DJ) Sarkar](https://opensource.com/users/djsarkar) 选题：[lujun9972](https://github.com/lujun9972) 译者：[warmfrog](https://github.com/warmfrog) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


Duc：一个能够可视化洞察硬盘使用情况的工具包

Cool!

Cool!

Cool!

感觉 du 命令就够用了

# Duc：一个能够可视化洞察硬盘使用情况的工具包

Duc 是一个在类 Unix 操作系统上可以用来索引、洞察及可视化硬盘使用情况的工具包。别把它当成一个仅能用漂亮图表展现硬盘使用情况的 CLI 工具。它对巨大的文件系统也支持的很好。Duc 已在由超过五亿个文件和几 PB 的存储组成的系统上测试过，没有任何问题。

Duc 是一个快速而且灵活的工具。它将你的硬盘使用情况存在一个优化过的数据库里，这样你就可以在索引完成后迅速找到你的数据。此外，它自带不同的用户交互界面与后端以访问数据库并绘制图表。

以下列出的是目前支持的用户界面（UI）：

1. 命令行界面（`duc ls`）
2. Ncurses 控制台界面（`duc ui`）
3. X11 GUI（`duc gui`）
4. OpenGL GUI（`duc gui`）

支持的后端数据库：

* Tokyocabinet
* Leveldb
* Sqlite3

Duc 默认使用 Tokyocabinet 作为后端数据库。

### 安装 Duc

Duc 可以从 Debian 以及其衍生品例如 Ubuntu 的默认仓库中获取。因此在基于 DEB 的系统上安装 Duc 是小菜一碟。

```
$ sudo apt-get install duc
```

在其它 Linux 发行版上你需要像以下所展示的那样手动从源代码编译安装 Duc。

可以从 Github 上的[发行](https://github.com/zevv/duc/releases)页面下载最新的 Duc 源代码的 .tgz 文件。在写这篇教程的时候，最新的版本是1.4.4。

```
$ wget https://github.com/zevv/duc/releases/download/1.4.4/duc-1.4.4.tar.gz
```

然后一个接一个地运行以下命令来安装 DUC。

```
$ tar -xzf duc-1.4.4.tar.gz
$ cd duc-1.4.4
$ ./configure
$ make
$ sudo make install
```

### 使用 Duc

`duc` 的典型用法是：

```
$ duc <subcommand> <options>
```

你可以通过运行以下命令来浏览总的选项列表以及子命令：

```
$ duc help
```

你也可以像下面这样了解一个特定子命令的用法。

```
$ duc help <subcommand>
```

要查看所有命令与其选项的列表，仅需运行：

```
$ duc help --all
```

让我们看看一些 `duc` 工具的特定用法。

### 创建索引（数据库）

首先，你需要创建一个你文件系统的索引文件（数据库）。使用 `duc index` 命令以创建索引文件。

比如说，要创建你的 `/home` 目录的索引，仅需运行：

```
$ duc index /home
```

上述命令将会创建你的 `/home` 目录的索引，并将其保存在 `$HOME/.duc.db` 文件中。如果你以后需要往 `/home` 目录添加新的文件或目录，只要在之后重新运行一下上面的命令来重建索引。

### 查询索引

Duc 有不同的子命令来查询并探索索引。

要查看可访问的索引列表，运行：

```
$ duc info
```

示例输出：

```
Date Time Files Dirs Size Path
2019-04-09 15:45:55 3.5K 305 654.6M /home
```

如你在上述输出所见，我已经索引好了 `/home` 目录。

要列出当前工作目录中所有的文件和目录，你可以这样做：

```
$ duc ls
```

要列出指定的目录，例如 `/home/sk/Downloads` 中的文件/目录，仅需像下面这样将路径作为参数传过去。

```
$ duc ls /home/sk/Downloads
```

类似的，运行 `duc ui` 命令来打开基于 ncurses 的控制台用户界面以探索文件系统使用情况，运行`duc gui` 以打开图形界面（X11）来探索文件系统。

要了解更多子命令的用法，仅需参考帮助部分。

```
$ duc help ls
```

上述命令将会展现 `ls` 子命令的帮助部分。

### 可视化硬盘使用状况

在之前的部分我们以及看到如何用 duc 子命令列出文件和目录。在此之外，你甚至可以用一张漂亮的图表展示文件大小。

要展示所提供目录的图表，像以下这样使用 `ls` 子命令。

```
$ duc ls -Fg /home/sk
```

示例输出：

如你在上述输出所见，`ls` 子命令查询 duc 数据库并列出了所提供目录包含的文件与目录的大小，在这里就是 `/home/sk/`。

这里 `-F` 选项是往条目中用来添加文件类型指示符（`/`），`-g` 选项是用来绘制每个条目相对大小的图表。

请注意如果未提供任何路径，就会使用当前工作目录。

你可以使用 `-R` 选项来用[树状结构](https://www.ostechnix.com/view-directory-tree-structure-linux/)浏览硬盘使用情况。

```
$ duc ls -R /home/sk
```

要查询 duc 数据库并打开基于 ncurses 的控制台以探索所提供的目录，像以下这样使用 `ui` 子命令。

```
$ duc ui /home/sk
```

类似的，我们使用 `gui *` 子命令来查询 duc 数据库以及打开一个图形界面（X11）来了解指定路径的硬盘使用情况。

```
$ duc gui /home/sk
```

像我之前所提到的，我们可以像下面这样了解更多关于特定子命令的用法。

```
$ duc help <子命令名字>
```

我仅仅覆盖了基本用法的部分，参考 man 页面了解关于 `duc` 工具的更多细节。

```
$ man duc
```

相关阅读：

* [Filelight – 在你的 Linux 系统上可视化硬盘使用情况](https://www.ostechnix.com/filelight-visualize-disk-usage-on-your-linux-system/)
* [一些好的 du 命令的替代品](https://www.ostechnix.com/some-good-alternatives-to-du-command/)
* [如何在 Linux 中用 Ncdu 检查硬盘使用情况](https://www.ostechnix.com/check-disk-space-usage-linux-using-ncdu/)
* [Agedu——发现 Linux 中被浪费的硬盘空间](https://www.ostechnix.com/agedu-find-out-wasted-disk-space-in-linux/)
* [如何在 Linux 中找到目录大小](https://www.ostechnix.com/find-size-directory-linux/)
* [为初学者打造的带有示例的 df 命令教程](https://www.ostechnix.com/the-df-command-tutorial-with-examples-for-beginners/)

### 总结

Duc 是一款简单却有用的硬盘用量查看器。如果你想要快速简便地知道哪个文件/目录占用你的硬盘空间，Duc 可能是一个好的选择。你还等什么呢？获取这个工具，扫描你的文件系统，摆脱无用的文件/目录。

现在就到此为止了。希望这篇文章有用处。更多好东西马上就到。保持关注！

欢呼吧！

资源：

* [Duc 网站](https://duc.zevv.nl/)

---

via: <https://www.ostechnix.com/duc-a-collection-of-tools-to-inspect-and-visualize-disk-usage/>

作者：[sk](https://www.ostechnix.com/author/sk/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[tomjlw](https://github.com/tomjlw) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


PiShrink：使树莓派镜像更小

Summary: PiShrink 将自动缩小镜像，然后在启动时将其调整为 SD 卡的最大大小。

赞呀，这个帖子帮我解决了问题！

PiShrink 将自动缩小镜像，然后在启动时将其调整为 SD 卡的最大大小。

# PiShrink：使树莓派镜像更小

<img src="content_image/album_201905_24_124416eti958m5tt55vtbv.png" alt="Make Raspberry Pi Images Smaller With PiShrink In Linux" title="Make Raspberry Pi Images Smaller With PiShrink In Linux">

树莓派不需要过多介绍。它是一款小巧、价格实惠，只有信用卡大小的电脑，它可以连接到显示器或电视。我们可以连接一个标准的键盘和鼠标，并将其用作一台成熟的台式计算机来完成日常任务，如互联网浏览、播放视频/玩游戏、文字处理和电子表格制作等。它主要是为学校的计算机科学教学而开发的。如今，树莓派被广泛用于大学、中小型组织和研究所来教授编码。

如果你有一台树莓派，你可能需要了解一个名为 PiShrink 的 bash 脚本，该脚本可使树莓派镜像更小。PiShrink 将自动缩小镜像，然后在启动时将其调整为 SD 卡的最大大小。这能更快地将镜像复制到 SD 卡中，同时缩小的镜像将更好地压缩。这对于将大容量镜像放入 SD 卡非常有用。在这个简短的指南中，我们将学习如何在类 Unix 系统中将树莓派镜像缩小到更小。

### 安装 PiShrink

要在 Linux 机器上安装 PiShrink，请先使用以下命令下载最新版本：

```
$ wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
```

接下来，将下载的 PiShrink 变成二进制可执行文件：

```
$ chmod +x pishrink.sh
```

最后，移动到目录：

```
$ sudo mv pishrink.sh /usr/local/bin/
```

### 使树莓派镜像更小

你可能已经知道，Raspbian 是所有树莓派型号的官方操作系统。树莓派基金会为 PC 和 Mac 开发了树莓派桌面版本。你可以创建一个 live CD，并在虚拟机中运行它，甚至也可以将其安装在桌面上。树莓派也有少量非官方操作系统镜像。为了测试，我从[官方下载页面](https://www.raspberrypi.org/downloads/)下载了官方的 Raspbian 系统。

解压下载的系统镜像：

```
$ unzip 2019-04-08-raspbian-stretch-lite.zip
```

上面的命令将提取当前目录中 `2019-04-08-raspbian-stretch-lite.zip` 文件的内容。

让我们看下提取文件的实际大小：

```
$ du -h 2019-04-08-raspbian-stretch-lite.img
1.7G 2019-04-08-raspbian-stretch-lite.img
```

如你所见，提取的树莓派系统镜像大小为 1.7G。

现在，使用 PiShrink 缩小此文件的大小，如下所示：

```
$ sudo pishrink.sh 2019-04-08-raspbian-stretch-lite.img
```

示例输出：

```
Creating new /etc/rc.local
rootfs: 39795/107072 files (0.1% non-contiguous), 239386/428032 blocks
resize2fs 1.45.0 (6-Mar-2019)
resize2fs 1.45.0 (6-Mar-2019)
Resizing the filesystem on /dev/loop1 to 280763 (4k) blocks.
Begin pass 3 (max = 14)
Scanning inode table XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 4 (max = 3728)
Updating inode references XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The filesystem on /dev/loop1 is now 280763 (4k) blocks long.

Shrunk 2019-04-08-raspbian-stretch-lite.img from 1.7G to 1.2G
```

正如你在上面的输出中看到的，树莓派镜像的大小已减少到 1.2G。

你还可以使用 `-s` 标志跳过该过程的自动扩展部分。

```
$ sudo pishrink.sh -s 2019-04-08-raspbian-stretch-lite.img newpi.img
```

这将创建一个源镜像文件（即 `2019-04-08-raspbian-stretch-lite.img`）的副本到一个新镜像文件（`newpi.img`）并进行处理。有关更多详细信息，请查看最后给出的官方 GitHub 页面。

就是这些了。希望本文有用。还有更多好东西，敬请期待！

资源：

* [PiShrink 的 GitHub 仓库](https://github.com/Drewsif/PiShrink)
* [树莓派网站](https://www.raspberrypi.org/)

---

via: <https://www.ostechnix.com/pishrink-make-raspberry-pi-images-smaller/>

作者：[sk](https://www.ostechnix.com/author/sk/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用 Ispell 在命令行中检查拼写

Summary: Ispell 可以帮助你在纯文本中消除超过 50 种语言的拼写错误。

Ispell 可以帮助你在纯文本中消除超过 50 种语言的拼写错误。

# 使用 Ispell 在命令行中检查拼写

>
> Ispell 可以帮助你在纯文本中消除超过 50 种语言的拼写错误。
>
>
>

好的拼写是一种技巧。它是一项需要时间学习和掌握的技能。也就是说，有些人从来没有完全掌握这种技能，我知道有两三个出色的作家就无法完全掌握拼写。

即使你拼写得很好，偶尔也会输入错字。特别是在最后期限前如果你快速敲击键盘，那就更是如此。无论你的拼写的是什么，通过拼写检查器检查你所写的内容总是一个好主意。

我用[纯文本](https://plaintextproject.online)完成了我的大部分写作，并经常使用名为 [Aspell](https://opensource.com/article/18/2/how-check-spelling-linux-command-line-aspell) 的命令行拼写检查器来完成这项工作。Aspell 不是唯一的工具。你可能还想要看下不错的 [Ispell](https://www.cs.hmc.edu/%7Egeoff/ispell.html)。

### 入门

自 1971 年以来，Ispell 就以各种形式出现过。不要被它的年龄欺骗。Ispell 仍然是一个可以在 21 世纪高效使用的应用。

在开始之前，请打开终端窗口并输入 `which ispell` 来检查计算机上是否安装了 Ispell。如果未安装，请打开发行版的软件包管理器并从那里安装 Ispell。

不要忘记为你使用的语言安装词典。我唯一使用的语言是英语，所以我只需下载美国和英国英语字典。你可以不局限于我的（也是唯一的）母语。Ispell 有[超过 50 种语言的词典](https://www.cs.hmc.edu/%7Egeoff/ispell-dictionaries.html)。

### 使用 Ispell

如果你还没有猜到，Ispell 只能用在文本文件。这包括用 HTML、LaTeX 和 [nroff 或 troff](https://opensource.com/article/18/2/how-format-academic-papers-linux-groff-me) 标记的文档。之后会有更多相关内容。

要开始使用，请打开终端窗口并进入包含要运行拼写检查的文件的目录。输入 `ispell` 后跟文件名，然后按回车键。

Ispell 高亮了它无法识别的第一个词。如果单词拼写错误，Ispell 通常会提供一个或多个备选方案。按下 `R`，然后按下正确选择旁边的数字。在上面的截图中，我按了 `R` 和 `0` 来修复错误。

另一方面，如果单词拼写正确，请按下 `A` 然后移动到下一个拼写错误的单词。

继续这样做直到到达文件的末尾。Ispell 会保存你的更改，创建你刚检查的文件的备份（扩展名为 `.bak`），然后关闭。

### 其他几个选项

此示例说明了 Ispell 的基本用法。这个程序有[很多选项](https://www.cs.hmc.edu/%7Egeoff/ispell-man.html)，有些你*可能*会用到，而另一些你*可能永远*不会使用。让我们快速看下我经常使用的一些。

之前我提到过 Ispell 可以用于某些标记语言。你需要告诉它文件的格式。启动 Ispell 时，为 TeX 或 LaTeX 文件添加 `-t`，为 HTML 文件添加 `-H`，对于 groff 或 troff 文件添加 `-n`。例如，如果输入 `ispell -t myReport.tex`，Ispell 将忽略所有标记。

如果你不想在检查文件后创建备份文件，请将 `-x` 添加到命令行。例如，`ispell -x myFile.txt`。

如果 Ispell 遇到拼写正确但不在其字典中的单词，比如名字，会发生什么？你可以按 `I` 将该单词添加到个人单词列表中。这会将单词保存到 `/home` 目录下的 `.ispell_default` 的文件中。

这些是我在使用 Ispell 时最有用的选项，但请查看 [Ispell 的手册页](https://www.cs.hmc.edu/%7Egeoff/ispell-man.html)以了解其所有选项。

Ispell 比 Aspell 或其他命令行拼写检查器更好或者更快么？我会说它不比其他的差或者慢。Ispell 不是适合所有人。它也许也不适合你。但有更多选择也不错，不是么？

---

via: <https://opensource.com/article/19/5/spelling-command-line-ispell>

作者：[Scott Nesbitt](https://opensource.com/users/scottnesbitt) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


“文章助手”的助手

Summary: 半个月前，我们推出了一个“文章助手”的小程序，用于解决在微信公众号文章中无法放置可点击的链接的问题。

# “文章助手”的助手

半个月前，我们推出了一个“文章助手”的小程序，用于解决在微信公众号文章中**无法放置可点击的链接**的问题。

可能对于我们这种技术向的文章来说，很多时候都需要插入链接，而一个认真的读者也经常希望可以点击链接看看。在我们从 2013 年开始运营公众号以来，我们对此问题有过几种解决方案：

* 直接将链接以文字的方式显示在**正文**中，读者需要手工选择并复制链接，然后另外在浏览器中打开
* 类似于文章脚注一样，将链接放置在**文末**，读者需要翻到文末，根据所要找的链接标号来复制并在浏览器中打开
* 将链接整理后放在另外一个 web 页面中，然后通过“**阅读原文**”的方式引导读者去点击，在其中完成链接功能

但是这几种方法都不太尽如人意，读者经常还是会下意识去点击文章内该出现链接的地方。所以，我们最近又有了新的解决方案。我们利用微信小程序的能力，在微信公众号文章内，采用小程序链接来替代外部链接；点击小程序链接后会打开该小程序，自动复制外部链接到剪贴板；打开浏览器（自动）贴入剪贴板中的链接来访问。

应该说，效果还是达到了我们的预期，虽然有些功能限于小程序本身支持无法做到，比如无法得到来源公众号的信息、无法主动唤起浏览器等。

不过，在使用过程中，公众号编辑们发现对链接一个个进行替换非常繁琐，所以，我决定给这个“文章助手”提供一个“助手”。我做了一个静态页面，在此页面内，只要将你编辑的公众号文章内容贴入其中的输入框，**一键点击**即可将全部链接转换为“文章助手”链接。

此外，经常还有认识或不认识的朋友，对我们的公众号排版表示好奇，比如这种<ruby> 注释性的标签 <rp>  （ </rp> <rt>  Ruby tag </rt> <rp>  ） </rp></ruby>是怎么回事？也有人希望采用 PingFang 字体，这个是微信编辑器默认不提供的。这次我们就一便提供好了。

“文章助手”的助手地址如下： <https://linux.cn/static/tools/a.html>

* 这个页面是纯静态页面，你可以连着其中使用的 jQuery 保存下来自行使用。
* 该小程序永久免费，并永久不添加第三方广告。
* 除不可抗力（如被微信官方封杀，但目前我们判断并未触犯微信使用规则），该小程序会一直运营下去。
* 贴入输入框的文章内容可能暂时不能显示来自微信已发送文章内的图片，但是并不影响转换和转换后再贴入微信编辑器内使用。
* 在输入框中选定格式“中文（English）”这样的内容时，点击下方的“转换 RUBY”的按钮，会将该字符串转换为<ruby> 中文 <rp>  （ </rp> <rt>  English </rt> <rp>  ） </rp></ruby>样式。
* 如果要全文使用“PingFang”字体，转换前勾选即可（最显眼的区别是，逗号和句号是垂直居中而不是底线对齐的）。

好了，老王的这些家底都给你们了，祝你们的公众号文章看起来越来越专业、越来越漂亮。


# 在 Ubuntu 中使用 Slimbook Battery Optimizer 切换电源模式

>
> Slimbook Battery Optimizer 是一个美观实用的指示器小程序，它可以让你在安装了 Linux 的笔记本上快速切换电源模式来延长续航时间。
>
>
>

[Slimbook](https://slimbook.es/en/) 是一个销售 [预装 Linux 的笔记本电脑](https://itsfoss.com/get-linux-laptops/) 的西班牙电脑制造商，他们发布了一款好用的小程序，用来在基于 Ubuntu 的 Linux 发行版下调整电池性能。

因为 Slimbook 销售他们自己的 Linux 系统，所以他们制作了一些在 Linux 上用于调整他们自己硬件性能的小工具。Battery Optimizer 就是这样一个工具。

要使用这个实用小程序，你不必购买 Slimbook 的产品，因为 Slimbook 已经将它在 [他们的官方 PPA 源](https://launchpad.net/%7Eslimbook/+archive/ubuntu/slimbook) 发行了。

### Slimbook Battery Optimizer 简介

这个程序叫 Slimbook Battery。它是一个常驻顶栏的指示器小程序，使得你可以快速切换电源模式。

你可能在 Windows 中见过类似的程序。Slimbook Battery 和它们一样，提供了类似的电源计划：

* 节能：最大程度延长电池续航时间
* 平衡：性能与节能间的最佳平衡
* 高性能：最大程度提高性能

你可以在高级模式中配置这些模式：

如果你觉得你把设置调乱了，你可以用“恢复默认设置”的按钮还原它。

你也可以修改像程序自启或默认电源模式这样的通用设置。

Slimbook 有专门为多种电源管理参数提供的页面。如果你希望自己配置，请参照 [此页](https://slimbook.es/en/tutoriales/aplicaciones-slimbook/398-slimbook-battery-3-application-for-optimize-battery-of-your-laptop)。

不过，我认为 Slimbook 的界面需要一些改进。例如，某些页面上的“问题标记”的图标应该改为可点击的，以此提供更多信息。然而，在我写这篇文章时，那个标记仍然无法点击。

总的来说，Slimbook Battery 是一个小巧精美的软件，你可以用它来快速切换电源模式。如果你决定在 Ubuntu 及其衍生发行版上（比如 Linux Mint 或 elementary OS 等），你可以使用官方 [PPA 源](https://itsfoss.com/ppa-guide/)。

#### 在基于 Ubuntu 的发行版上安装 Slimbook Battery

打开终端，一步一步地使用以下命令：

```
sudo add-apt-repository ppa:slimbook/slimbook
sudo apt update
sudo apt install slimbookbattery
```

安装好之后，在菜单中搜索 Slimbook Battery：

在你点击它之后，你会发现它出现在了顶栏。你可以在这里选择你希望使用的电源模式。

#### 卸载 Slimbook Battery

如果你不再使用它，你可以通过以下命令来卸载它：

```
sudo apt remove slimbookbattery
sudo add-apt-repository -r ppa:slimbook/slimbook
```

在我看来，这样的应用程序为某些特定的目的服务，这是值得鼓励的。这个工具给了你一条调整电源模式的捷径，和调整性能的更多选项。

你用过 Slimbook Battery 吗？你觉得它如何？

---

via: <https://itsfoss.com/slimbook-battry-optimizer-ubuntu/>

作者：[Abhishek Prakash](https://itsfoss.com/author/abhishek/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[zhs852](https://github.com/zhs852) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


PHP PDO 简单教程

Summary: PHP 5.5 版本之前，我们有用于访问 MySQL 数据库的 mysql_ 命令，但由于安全性不足，它们最终被弃用。mysql_ 的弃用带来了访问数据库的两种主要方法：mysqli 和 PDO 库。

# PHP PDO 简单教程

<img src="content_image/album_201905_26_104631lriqriqrrusyyy22.jpg" alt="" title="">

大约 80% 的 Web 应用程序由 PHP 提供支持。类似地，SQL 也是如此。PHP 5.5 版本之前，我们有用于访问 MySQL 数据库的 mysql\_ 命令，但由于安全性不足，它们最终被弃用。

弃用这件事是发生在 2013 年的 PHP 5.5 上，我写这篇文章的时间是 2018 年，PHP 版本为 7.2。mysql\_ 的弃用带来了访问数据库的两种主要方法：mysqli 和 PDO 库。

虽然 mysqli 库是官方指定的，但由于 mysqli 只能支持 mysql 数据库，而 PDO 可以支持 12 种不同类型的数据库驱动程序，因此 PDO 获得了更多的赞誉。此外，PDO 还有其它一些特性，使其成为大多数开发人员的更好选择。你可以在下表中看到一些特性比较：

|  | PDO | MySQLi |
| --- | --- | --- |
| 数据库支持 | 12 种驱动 | 只有 MySQL |
| 范例 | OOP | 过程 + OOP |
| 预处理语句（客户端侧） | Yes | No |
| 1命名参数 | Yes | No |

现在我想对于大多数开发人员来说，PDO 是首选的原因已经很清楚了。所以让我们深入研究它，并希望在本文中尽量涵盖关于 PDO 你需要的了解的。

### 连接

第一步是连接到数据库，由于 PDO 是完全面向对象的，所以我们将使用 PDO 类的实例。

我们要做的第一件事是定义主机、数据库名称、用户名、密码和数据库字符集。

```
$host = 'localhost';
$db   = 'theitstuff';
$user = 'root';
$pass = 'root';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$conn = new PDO($dsn, $user, $pass);
```

之后，正如你在上面的代码中看到的，我们创建了 DSN 变量，DSN 变量只是一个保存数据库信息的变量。对于一些在外部服务器上运行 MySQL 的人，你还可以通过提供一个 `port=$port_number` 来调整端口号。

最后，你可以创建一个 PDO 类的实例，我使用了 `$conn` 变量，并提供了 `$dsn`、`$user`、`$pass` 参数。如果你遵循这些步骤，你现在应该有一个名为 `$conn` 的对象，它是 PDO 连接类的一个实例。现在是时候进入数据库并运行一些查询。

### 一个简单的 SQL 查询

现在让我们运行一个简单的 SQL 查询。

```
$tis = $conn->query('SELECT name, age FROM students');
while ($row = $tis->fetch())
{
  echo $row['name']."\t";
  echo $row['age'];
  echo "<br>";
}
```

这是使用 PDO 运行查询的最简单形式。我们首先创建了一个名为 `tis`（TheITStuff 的缩写 ）的变量，然后你可以看到我们使用了创建的 `$conn` 对象中的查询函数。

然后我们运行一个 `while` 循环并创建了一个 `$row` 变量来从 `$tis` 对象中获取内容，最后通过调用列名来显示每一行。

很简单，不是吗？现在让我们来看看预处理语句。

### 预处理语句

预处理语句是人们开始使用 PDO 的主要原因之一，因为它提供了可以阻止 SQL 注入的语句。

有两种基本方法可供使用，你可以使用位置参数或命名参数。

#### 位置参数

让我们看一个使用位置参数的查询示例。

```
$tis = $conn->prepare("INSERT INTO STUDENTS(name, age) values(?, ?)");
$tis->bindValue(1,'mike');
$tis->bindValue(2,22);
$tis->execute();
```

在上面的例子中，我们放置了两个问号，然后使用 `bindValue()` 函数将值映射到查询中。这些值绑定到语句问号中的位置。

我还可以使用变量而不是直接提供值，通过使用 `bindParam()` 函数相同例子如下：

```
$name='Rishabh'; $age=20;
$tis = $conn->prepare("INSERT INTO STUDENTS(name, age) values(?, ?)");
$tis->bindParam(1,$name);
$tis->bindParam(2,$age);
$tis->execute();
```

### 命名参数

命名参数也是预处理语句，它将值/变量映射到查询中的命名位置。由于没有位置绑定，因此在多次使用相同变量的查询中非常有效。

```
$name='Rishabh'; $age=20;
$tis = $conn->prepare("INSERT INTO STUDENTS(name, age) values(:name, :age)");
$tis->bindParam(':name', $name);
$tis->bindParam(':age', $age);
$tis->execute();
```

你可以注意到，唯一的变化是我使用 `:name` 和 `:age` 作为占位符，然后将变量映射到它们。冒号在参数之前使用，让 PDO 知道该位置是一个变量，这非常重要。

你也可以类似地使用 `bindValue()` 来使用命名参数直接映射值。

### 获取数据

PDO 在获取数据时非常丰富，它实际上提供了许多格式来从数据库中获取数据。

你可以使用 `PDO::FETCH_ASSOC` 来获取关联数组，`PDO::FETCH_NUM` 来获取数字数组，使用 `PDO::FETCH_OBJ` 来获取对象数组。

```
$tis = $conn->prepare("SELECT * FROM STUDENTS");
$tis->execute();
$result = $tis->fetchAll(PDO::FETCH_ASSOC);
```

你可以看到我使用了 `fetchAll`，因为我想要所有匹配的记录。如果只需要一行，你可以简单地使用 `fetch`。

现在我们已经获取了数据，现在是时候循环它了，这非常简单。

```
foreach ($result as $lnu){
  echo $lnu['name'];
  echo $lnu['age']."<br>";
}
```

你可以看到，因为我请求了关联数组，所以我正在按名称访问各个成员。

虽然在定义希望如何传输递数据方面没有要求，但在定义 `$conn` 变量本身时，实际上可以将其设置为默认值。

你需要做的就是创建一个 `$options` 数组，你可以在其中放入所有默认配置，只需在 `$conn` 变量中传递数组即可。

```
$options = [
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$conn = new PDO($dsn, $user, $pass, $options);
```

这是一个非常简短和快速的 PDO 介绍，我们很快就会制作一个高级教程。如果你在理解本教程的任何部分时遇到任何困难，请在评论部分告诉我，我会在那你为你解答。

---

via: <http://www.theitstuff.com/easiest-pdo-tutorial-basics>

作者：[Rishabh Kandari](http://www.theitstuff.com/author/reevkandari) 选题：[lujun9972](https://github.com/lujun9972) 译者：[MjSeven](https://github.com/MjSeven) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


API 演进的正确方式

Summary: 负责任的库作者与其用户的十个约定。

您在百度可以搜到的 果敢老街位于云南省 临沧市 镇康县 南伞镇 出个国门 10公里就到了

我们公司的实体 就在 缅甸果敢老街 双凤塔前面 80米左右 随时你可以视频验证呢

公司是经过这边政府合法营业，腾龙公司实力强大资金雄厚  官网的顾客朋友，都可以放心的娱乐，不会像那些假平台黑你的钱，其一：一个投资上亿的公司每天拥有上万个网投客人，这样的公司是不会骗你那点小钱的，因为这么大的一个公司怎么可能会因为你的那点钱自毁名声呢。其二：缅甸的公司都是经过政府的审核以后才允许开的，政府把公司视为最大的经济核心，就算有心玩假，

翻译的真好，新译者学习了。

负责任的库作者与其用户的十个约定。

# API 演进的正确方式

>
> 负责任的库作者与其用户的十个约定。
>
>
>

想象一下你是一个造物主，为一个生物设计一个身体。出于仁慈，你希望它能随着时间进化：首先，因为它必须对环境的变化作出反应；其次，因为你的智慧在增长，你对这个小东西想到了更好的设计，它不应该永远保持一个样子。

然而，这个生物可能有赖于其目前解剖学的特征。你不能无所顾忌地添加翅膀或改变它的身材比例。它需要一个有序的过程来适应新的身体。作为一个负责任的设计者，你如何才能温柔地引导这种生物走向更大的进步呢？

对于负责任的库维护者也是如此。我们向依赖我们代码的人保证我们的承诺：我们会发布 bug 修复和有用的新特性。如果对库的未来有利，我们有时会删除某些特性。我们会不断创新，但我们不会破坏使用我们库的人的代码。我们怎样才能一次实现所有这些目标呢？

### 添加有用的特性

你的库不应该永远保持不变：你应该添加一些特性，使你的库更适合用户。例如，如果你有一个爬行动物类，并且如果有个可以飞行的翅膀是有用的，那就去添加吧。

```
class Reptile:
    @property
    def teeth(self):
        return 'sharp fangs'

    # 如果 wings 是有用的，那就添加它!
    @property
    def wings(self):
        return 'majestic wings'
```

但要注意，特性是有风险的。考虑 Python 标准库中以下功能，看看它出了什么问题。

```
bool(datetime.time(9, 30)) == True
bool(datetime.time(0, 0)) == False
```

这很奇怪：将任何时间对象转换为布尔值都会得到 True，但午夜时间除外。（更糟糕的是，时区感知时间的规则更加奇怪。）

我已经写了十多年的 Python 了，但直到上周才发现这条规则。这种奇怪的行为会在用户代码中引起什么样的 bug？

比如说一个日历应用程序，它带有一个创建事件的函数。如果一个事件有一个结束时间，那么函数也应该要求它有一个开始时间。

```
def create_event(day,
                 start_time=None,
                 end_time=None):
    if end_time and not start_time:
        raise ValueError("Can't pass end_time without start_time")

    # 女巫集会从午夜一直开到凌晨 4 点
create_event(datetime.date.today(),
             datetime.time(0, 0),
             datetime.time(4, 0))
```

不幸的是，对于女巫来说，从午夜开始的事件无法通过校验。当然，一个了解午夜怪癖的细心程序员可以正确地编写这个函数。

```
def create_event(day,
                 start_time=None,
                 end_time=None):
    if end_time is not None and start_time is None:
        raise ValueError("Can't pass end_time without start_time")
```

但这种微妙之处令人担忧。如果一个库作者想要创建一个伤害用户的 API，那么像午夜的布尔转换这样的“特性”很有效。

但是，负责任的创建者的目标是使你的库易于正确使用。

这个功能是由 Tim Peters 在 2002 年首次编写 datetime 模块时造成的。即时是像 Tim 这样的奠基 Python 的高手也会犯错误。[这个怪异之处后来被消除了](https://bugs.python.org/issue13936)，现在所有时间的布尔值都是 True。

```
# Python 3.5 以后

bool(datetime.time(9, 30)) == True
bool(datetime.time(0, 0)) == True
```

不知道午夜怪癖的古怪之处的程序员现在可以从这种晦涩的 bug 中解脱出来，但是一想到任何依赖于古怪的旧行为的代码现在没有注意变化，我就会感到紧张。如果从来没有实现这个糟糕的特性，情况会更好。这就引出了库维护者的第一个承诺：

#### 第一个约定：避免糟糕的特性

最痛苦的变化是你必须删除一个特性。一般来说，避免糟糕特性的一种方法是少添加特性！没有充分的理由，不要使用公共方法、类、功能或属性。因此：

#### 第二个约定：最小化特性

特性就像孩子：在充满激情的瞬间孕育，但是它们必须要支持多年（LCTT 译注：我怀疑作者在开车，可是我没有证据）。不要因为你能做傻事就去做傻事。不要画蛇添足！

但是，当然，在很多情况下，用户需要你的库中尚未提供的东西，你如何选择合适的功能给他们？以下另一个警示故事。

### 一个来自 asyncio 的警示故事

你可能知道，当你调用一个协程函数，它会返回一个协程对象：

```
async def my_coroutine():
    pass

print(my_coroutine())
```

```
<coroutine object my_coroutine at 0x10bfcbac8>
```

你的代码必须 “<ruby> 等待 <rt>  await </rt></ruby>” 这个对象以此来运行协程。人们很容易忘记这一点，所以 asyncio 的开发人员想要一个“调试模式”来捕捉这个错误。当协程在没有等待的情况下被销毁时，调试模式将打印一个警告，并在其创建的行上进行回溯。

当 Yury Selivanov 实现调试模式时，他添加了一个“协程装饰器”的基础特性。装饰器是一个函数，它接收一个协程并返回任何内容。Yury 使用它在每个协程上接入警告逻辑，但是其他人可以使用它将协程转换为字符串 “hi!”。

```
import sys

def my_wrapper(coro):
    return 'hi!'

sys.set_coroutine_wrapper(my_wrapper)

async def my_coroutine():
    pass

print(my_coroutine())
```

```
hi!
```

这是一个地狱般的定制。它改变了 “<ruby> 异步 <rt>  async </rt></ruby>“ 的含义。调用一次 `set_coroutine_wrapper` 将在全局永久改变所有的协程函数。正如 [Nathaniel Smith 所说](https://bugs.python.org/issue32591)：“一个有问题的 API” 很容易被误用，必须被删除。如果 asyncio 开发人员能够更好地按照其目标来设计该特性，他们就可以避免删除该特性的痛苦。负责任的创建者必须牢记这一点：

#### 第三个约定：保持特性单一

幸运的是，Yury 有良好的判断力，他将该特性标记为临时，所以 asyncio 用户知道不能依赖它。Nathaniel 可以用更单一的功能替换 `set_coroutine_wrapper`，该特性只定制回溯深度。

```
import sys

sys.set_coroutine_origin_tracking_depth(2)

async def my_coroutine():
    pass

print(my_coroutine())

```

```
<coroutine object my_coroutine at 0x10bfcbac8>

RuntimeWarning:'my_coroutine' was never awaited

Coroutine created at (most recent call last)
    File "script.py", line 8, in <module>
        print(my_coroutine())
```

这样好多了。没有可以更改协程的类型的其他全局设置，因此 asyncio 用户无需编写防御代码。造物主应该像 Yury 一样有远见。

#### 第四个约定：标记实验特征“临时”

如果你只是预感你的生物需要犄角和四叉舌，那就引入这些特性，但将它们标记为“临时”。

你可能会发现犄角是无关紧要的，但是四叉舌是有用的。在库的下一个版本中，你可以删除前者并标记后者为正式的。

### 删除特性

无论我们如何明智地指导我们的生物进化，总会有一天想要删除一个正式特征。例如，你可能已经创建了一只蜥蜴，现在你选择删除它的腿。也许你想把这个笨拙的家伙变成一条时尚而现代的蟒蛇。

删除特性主要有两个原因。首先，通过用户反馈或者你自己不断增长的智慧，你可能会发现某个特性是个坏主意。午夜怪癖的古怪行为就是这种情况。或者，最初该特性可能已经很好地适应了你的库环境，但现在生态环境发生了变化，也许另一个神发明了哺乳动物，你的生物想要挤进哺乳动物的小洞穴里，吃掉里面美味的哺乳动物，所以它不得不失去双腿。

同样，Python 标准库会根据语言本身的变化删除特性。考虑 asyncio 的 Lock 功能，在把 `await` 作为一个关键字添加进来之前，它一直在等待：

```
lock = asyncio.Lock()

async def critical_section():
    await lock
    try:
        print('holding lock')
    finally:
        lock.release()
```

但是现在，我们可以做“异步锁”：

```
lock = asyncio.Lock()

async def critical_section():
    async with lock:
        print('holding lock')
```

新方法好多了！很短，并且在一个大函数中使用其他 try-except 块时不容易出错。因为“尽量找一种，最好是唯一一种明显的解决方案”，[旧语法在 Python 3.7 中被弃用](https://bugs.python.org/issue32253)，并且很快就会被禁止。

不可避免的是，生态变化会对你的代码产生影响，因此要学会温柔地删除特性。在此之前，请考虑删除它的成本或好处。负责任的维护者不会愿意让用户更改大量代码或逻辑。（还记得 Python 3 在重新添加会 `u` 字符串前缀之前删除它是多么痛苦吗？）如果代码删除是机械性的动作，就像一个简单的搜索和替换，或者如果该特性是危险的，那么它可能值得删除。

#### 是否删除特性

| 反对 | 支持 |
| --- | --- |
| 代码必须改变 | 改变是机械性的 |
| 逻辑必须改变 | 特性是危险的 |

就我们饥饿的蜥蜴而言，我们决定删除它的腿，这样它就可以滑进老鼠洞里吃掉它。我们该怎么做呢？我们可以删除 `walk` 方法，像下面一样修改代码：

```
class Reptile:
    def walk(self):
        print('step step step')
```

变成这样：

```
class Reptile:
    def slither(self):
        print('slide slide slide')
```

这不是一个好主意，这个生物习惯于走路！或者，就库而言，你的用户拥有依赖于现有方法的代码。当他们升级到最新库版本时，他们的代码将会崩溃。

```
# 用户的代码，哦，不!
Reptile.walk()
```

因此，负责任的创建者承诺：

#### 第五条预定：温柔地删除

温柔地删除一个特性需要几个步骤。从用腿走路的蜥蜴开始，首先添加新方法 `slither`。接下来，弃用旧方法。

```
import warnings

class Reptile:
    def walk(self):
        warnings.warn(
                "walk is deprecated, use slither",
                 DeprecationWarning, stacklevel=2)
        print('step step step')

    def slither(self):
        print('slide slide slide')
```

Python 的 warnings 模块非常强大。默认情况下，它会将警告输出到 stderr，每个代码位置只显示一次，但你可以禁用警告或将其转换为异常，以及其它选项。

一旦将这个警告添加到库中，PyCharm 和其他 IDE 就会使用删除线呈现这个被弃用的方法。用户马上就知道该删除这个方法。

>
> Reptile().~~walk()~~
>
>
>

当他们使用升级后的库运行代码时会发生什么？

```
$ python3 script.py

DeprecationWarning: walk is deprecated, use slither
    script.py:14: Reptile().walk()

step step step
```

默认情况下，他们会在 stderr 上看到警告，但脚本会成功并打印 “step step step”。警告的回溯显示必须修复用户代码的哪一行。（这就是 `stacklevel` 参数的作用：它显示了用户需要更改的调用，而不是库中生成警告的行。）请注意，错误消息有指导意义，它描述了库用户迁移到新版本必须做的事情。

你的用户可能会希望测试他们的代码，并证明他们没有调用弃用的库方法。仅警告不会使单元测试失败，但异常会失败。Python 有一个命令行选项，可以将弃用警告转换为异常。

```
> python3 -Werror::DeprecationWarning script.py

Traceback (most recent call last):
    File "script.py", line 14, in <module>
        Reptile().walk()
    File "script.py", line 8, in walk
        DeprecationWarning, stacklevel=2)
DeprecationWarning: walk is deprecated, use slither
```

现在，“step step step” 没有输出出来，因为脚本以一个错误终止。

因此，一旦你发布了库的一个版本，该版本会警告已启用的 `walk` 方法，你就可以在下一个版本中安全地删除它。对吧？

考虑一下你的库用户在他们项目的 `requirements` 中可能有什么。

```
# 用户的 requirements.txt 显示 reptile 包的依赖关系
reptile
```

下次他们部署代码时，他们将安装最新版本的库。如果他们尚未处理所有的弃用，那么他们的代码将会崩溃，因为代码仍然依赖 `walk`。你需要温柔一点，你必须向用户做出三个承诺：维护更改日志，选择版本化方案和编写升级指南。

#### 第六个约定：维护变更日志

你的库必须有更改日志，其主要目的是宣布用户所依赖的功能何时被弃用或删除。

>
> **版本 1.1 中的更改**
>
>
> **新特性**
>
>
> * 新功能 Reptile.slither()
>
>
> **弃用**
>
>
> * Reptile.walk() 已弃用，将在 2.0 版本中删除，请使用 slither()
>
>
>

负责任的创建者会使用版本号来表示库发生了怎样的变化，以便用户能够对升级做出明智的决定。“版本化方案”是一种用于交流变化速度的语言。

#### 第七个约定：选择一个版本化方案

有两种广泛使用的方案，[语义版本控制](https://semver.org)和基于时间的版本控制。我推荐任何库都进行语义版本控制。Python 的风格在 [PEP 440](https://www.python.org/dev/peps/pep-0440/) 中定义，像 `pip` 这样的工具可以理解语义版本号。

如果你为库选择语义版本控制，你可以使用版本号温柔地删除腿，例如：

>
> 1.0: 第一个“稳定”版，带有 `walk()` 1.1: 添加 `slither()`，废弃 `walk()` 2.0: 删除 `walk()`
>
>
>

你的用户依赖于你的库的版本应该有一个范围，例如：

```
# 用户的 requirements.txt
reptile>=1,<2
```

这允许他们在主要版本中自动升级，接收错误修正并可能引发一些弃用警告，但不会升级到**下**个主要版本并冒着更改破坏其代码的风险。

如果你遵循基于时间的版本控制，则你的版本可能会编号：

>
> 2017.06.0: 2017 年 6 月的版本 2018.11.0: 添加 `slither()`，废弃 `walk()` 2019.04.0: 删除 `walk()`
>
>
>

用户可以这样依赖于你的库：

```
# 用户的 requirements.txt，基于时间控制的版本
reptile==2018.11.*
```

这非常棒，但你的用户如何知道你的版本方案，以及如何测试代码来进行弃用呢？你必须告诉他们如何升级。

#### 第八个约定：写一个升级指南

下面是一个负责任的库创建者如何指导用户：

>
> **升级到 2.0**
>
>
> **从弃用的 API 迁移**
>
>
> 请参阅更改日志以了解已弃用的特性。
>
>
> **启用弃用警告**
>
>
> 升级到 1.1 并使用以下代码测试代码：
>
>
> `python -Werror::DeprecationWarning`
>
>
>  现在可以安全地升级了。
>
>
>

你必须通过向用户显示命令行选项来教会用户如何处理弃用警告。并非所有 Python 程序员都知道这一点 —— 我自己就每次都得查找这个语法。注意，你必须*发布*一个版本，它输出来自每个弃用的 API 的警告，以便用户可以在再次升级之前使用该版本进行测试。在本例中，1.1 版本是小版本。它允许你的用户逐步重写代码，分别修复每个弃用警告，直到他们完全迁移到最新的 API。他们可以彼此独立地测试代码和库的更改，并隔离 bug 的原因。

如果你选择语义版本控制，则此过渡期将持续到下一个主要版本，从 1.x 到 2.0，或从 2.x 到 3.0 以此类推。删除生物腿部的温柔方法是至少给它一个版本来调整其生活方式。不要一次性把腿删掉！

版本号、弃用警告、更改日志和升级指南可以协同工作，在不违背与用户约定的情况下温柔地改进你的库。[Twisted 项目的兼容性政策](https://twistedmatrix.com/documents/current/core/development/policy/compatibility-policy.html) 解释的很漂亮：

>
> “先行者总是自由的”
>
>
> 运行的应用程序在没有任何警告的情况下都可以升级为 Twisted 的一个次要版本。
>
>
> 换句话说，任何运行其测试而不触发 Twisted 警告的应用程序应该能够将其 Twisted 版本升级至少一次，除了可能产生新警告之外没有任何不良影响。
>
>
>

现在，我们的造物主已经获得了智慧和力量，可以通过添加方法来添加特性，并温柔地删除它们。我们还可以通过添加参数来添加特性，但这带来了新的难度。你准备好了吗？

### 添加参数

想象一下，你只是给了你的蛇形生物一对翅膀。现在你必须允许它选择是滑行还是飞行。目前它的 `move` 功能只接受一个参数。

```
# 你的库代码
def move(direction):
    print(f'slither {direction}')

# 用户的应用
move('north')
```

你想要添加一个 `mode` 参数，但如果用户升级库，这会破坏他们的代码，因为他们只传递了一个参数。

```
# 你的库代码
def move(direction, mode):
    assert mode in ('slither', 'fly')
    print(f'{mode} {direction}')

# 一个用户的代码，出现错误!
move('north')
```

一个真正聪明的创建者者会承诺不会以这种方式破坏用户的代码。

#### 第九条约定：兼容地添加参数

要保持这个约定，请使用保留原始行为的默认值添加每个新参数。

```
# 你的库代码
def move(direction, mode='slither'):
    assert mode in ('slither', 'fly')
    print(f'{mode} {direction}')

# 用户的应用
move('north')
```

随着时间推移，参数是函数演化的自然历史。它们首先列出最老的参数，每个都有默认值。库用户可以传递关键字参数以选择特定的新行为，并接受所有其他行为的默认值。

```
# 你的库代码
def move(direction,
         mode='slither',
         turbo=False,
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用户应用
move('north', extra_sinuous=True)
```

但是有一个危险，用户可能会编写如下代码：

```
# 用户应用，简写
move('north', 'slither', False, True)
```

如果在你在库的下一个主要版本中去掉其中一个参数，例如 `turbo`，会发生什么？

```
# 你的库代码，下一个主要版本中 "turbo" 被删除
def move(direction,
         mode='slither',
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用户应用，简写
move('north', 'slither', False, True)
```

用户的代码仍然能编译，这是一件坏事。代码停止了曲折的移动并开始招呼 Lyft，这不是它的本意。我相信你可以预测我接下来要说的内容：删除参数需要几个步骤。当然，首先弃用 `trubo` 参数。我喜欢这种技术，它可以检测任何用户的代码是否依赖于这个参数。

```
# 你的库代码
_turbo_default = object()

def move(direction,
         mode='slither',
         turbo=_turbo_default,
         extra_sinuous=False,
         hail_lyft=False):
    if turbo is not _turbo_default:
        warnings.warn(
                "'turbo' is deprecated",
                DeprecationWarning,
                stacklevel=2)
    else:
        # The old default.
        turbo = False
```

但是你的用户可能不会注意到警告。警告声音不是很大：它们可以在日志文件中被抑制或丢失。用户可能会漫不经心地升级到库的下一个主要版本——那个删除 `turbo` 的版本。他们的代码运行时将没有错误、默默做错误的事情！正如 Python 之禅所说：“错误绝不应该被默默 pass”。实际上，爬行动物的听力很差，所有当它们犯错误时，你必须非常大声地纠正它们。

保护用户的最佳方法是使用 Python 3 的星型语法，它要求调用者传递关键字参数。

```
# 你的库代码
# 所有 “*” 后的参数必须以关键字方式传输。
def move(direction,
         *,
         mode='slither',
         turbo=False,
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用户代码，简写
# 错误！不能使用位置参数，关键字参数是必须的
move('north', 'slither', False, True)
```

有了这个星，以下是唯一允许的语法：

```
# 用户代码
move('north', extra_sinuous=True)
```

现在，当你删除 `turbo` 时，你可以确定任何依赖于它的用户代码都会明显地提示失败。如果你的库也支持 Python2，这没有什么大不了。你可以模拟星型语法（[归功于 Brett Slatkin](http://www.informit.com/articles/article.aspx?p=2314818)）：

```
# 你的库代码，兼容 Python 2
def move(direction, **kwargs):
    mode = kwargs.pop('mode', 'slither')
    turbo = kwargs.pop('turbo', False)
    sinuous = kwargs.pop('extra_sinuous', False)
    lyft = kwargs.pop('hail_lyft', False)

    if kwargs:
        raise TypeError('Unexpected kwargs: %r'
                        % kwargs)

# ...
```

要求关键字参数是一个明智的选择，但它需要远见。如果允许按位置传递参数，则不能仅在以后的版本中将其转换为仅关键字。所以，现在加上星号。你可以在 asyncio API 中观察到，它在构造函数、方法和函数中普遍使用星号。尽管到目前为止，`Lock` 只接受一个可选参数，但 asyncio 开发人员立即添加了星号。这是幸运的。

```
# In asyncio.
class Lock:
    def __init__(self, *, loop=None):
        # ...
```

现在，我们已经获得了改变方法和参数的智慧，同时保持与用户的约定。现在是时候尝试最具挑战性的进化了：在不改变方法或参数的情况下改变行为。

### 改变行为

假设你创造的生物是一条响尾蛇，你想教它一种新行为。

横向移动！这个生物的身体看起来是一样的，但它的行为会发生变化。我们如何为这一进化步骤做好准备？

*Image by HCA [[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0)], [via Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Neonate_sidewinder_sidewinding_with_tracks_unlabeled.jpg), 由 Opensource.com 修改*

当行为在没有新函数或新参数的情况下发生更改时，负责任的创建者可以从 Python 标准库中学习。很久以前，os 模块引入了 `stat` 函数来获取文件统计信息，比如创建时间。起初，这个时间总是整数。

```
>>> os.stat('file.txt').st_ctime
1540817862
```

有一天，核心开发人员决定在 `os.stat` 中使用浮点数来提供亚秒级精度。但他们担心现有的用户代码还没有做好准备更改。于是他们在 Python 2.3 中创建了一个设置 `stat_float_times`，默认情况下是 `False` 。用户可以将其设置为 True 来选择浮点时间戳。

```
>>> # Python 2.3.
>>> os.stat_float_times(True)
>>> os.stat('file.txt').st_ctime
1540817862.598021
```

从 Python 2.5 开始，浮点时间成为默认值，因此 2.5 及之后版本编写的任何新代码都可以忽略该设置并期望得到浮点数。当然，你可以将其设置为 `False` 以保持旧行为，或将其设置为 `True` 以确保所有 Python 版本都得到浮点数，并为删除 `stat_float_times` 的那一天准备代码。

多年过去了，在 Python 3.1 中，该设置已被弃用，以便为人们为遥远的未来做好准备，最后，经过数十年的旅程，[这个设置被删除](https://bugs.python.org/issue31827)。浮点时间现在是唯一的选择。这是一个漫长的过程，但负责任的神灵是有耐心的，因为我们知道这个渐进的过程很有可能于意外的行为变化拯救用户。

#### 第十个约定：逐渐改变行为

以下是步骤：

* 添加一个标志来选择新行为，默认为 `False`，如果为 `False` 则发出警告
* 将默认值更改为 `True`，表示完全弃用标记
* 删除该标志

如果你遵循语义版本控制，版本可能如下：

| 库版本 | 库 API | 用户代码 |
| --- | --- | --- |
| 1.0 | 没有标志 | 预期的旧行为 |
| 1.1 | 添加标志，默认为 `False`，如果是 `False`，则警告 | 设置标志为 `True`，处理新行为 |
| 2.0 | 改变默认为 `True`，完全弃用标志 | 处理新行为 |
| 3.0 | 移除标志 | 处理新行为 |

你需要**两**个主要版本来完成该操作。如果你直接从“添加标志，默认为 `False`，如果是 `False` 则发出警告”变到“删除标志”，而没有中间版本，那么用户的代码将无法升级。为 1.1 正确编写的用户代码必须能够升级到下一个版本，除了新警告之外，没有任何不良影响，但如果在下一个版本中删除了该标志，那么该代码将崩溃。一个负责任的神明从不违反扭曲的政策：“先行者总是自由的”。

### 负责任的创建者

我们的 10 个约定大致可以分为三类：

**谨慎发展**

1. 避免不良功能
2. 最小化特性
3. 保持功能单一
4. 标记实验特征“临时”
5. 温柔删除功能

**严格记录历史**

1. 维护更改日志
2. 选择版本方案
3. 编写升级指南

**缓慢而明显地改变**

1. 兼容添加参数
2. 逐渐改变行为

如果你对你所创造的物种保持这些约定，你将成为一个负责任的造物主。你的生物的身体可以随着时间的推移而进化，一直在改善和适应环境的变化，而不是在生物没有准备好就突然改变。如果你维护一个库，请向用户保留这些承诺，这样你就可以在不破坏依赖该库的代码的情况下对库进行更新。

---

这篇文章最初是在 [A. Jesse Jiryu Davis 的博客上'](https://emptysqua.re/blog/api-evolution-the-right-way/)出现的，经允许转载。

插图参考：

* [《世界进步》, Delphian Society, 1913](https://www.gutenberg.org/files/42224/42224-h/42224-h.htm)
* [《走进蛇的历史》, Charles Owen, 1742](https://publicdomainreview.org/product-att/artist/charles-owen/)
* [关于哥斯达黎加的 batrachia 和爬行动物，关于尼加拉瓜和秘鲁的爬行动物和鱼类学的记录, Edward Drinker Cope, 1875](https://archive.org/details/onbatrachiarepti00cope/page/n3)
* [《自然史》, Richard Lydekker et. al., 1897](https://www.flickr.com/photos/internetarchivebookimages/20556001490)
* [Mes Prisons, Silvio Pellico, 1843](https://www.oldbookillustrations.com/illustrations/stationery/)
* [Tierfotoagentur / m.blue-shadow](https://www.alamy.com/mediacomp/ImageDetails.aspx?ref=D7Y61W)
* [洛杉矶公共图书馆, 1930](https://www.vintag.es/2013/06/riding-alligator-c-1930s.html)

---

via: <https://opensource.com/article/19/5/api-evolution-right-way>

作者：[A. Jesse](https://opensource.com/users/emptysquare) 选题：[lujun9972](https://github.com/lujun9972) 译者：[MjSeven](https://github.com/MjSeven) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


在 Pygame 游戏中放置平台

# 在 Pygame 游戏中放置平台

>
> 在这个从零构建一个 Python 游戏系列的第六部分中，为你的角色创建一些平台来旅行。
>
>
>

这是仍在进行中的关于使用 Pygame 模块来在 Python 3 中创建电脑游戏的系列文章的第六部分。先前的文章是：

* [通过构建一个简单的掷骰子游戏去学习怎么用 Python 编程](/article-9071-1.html)
* [使用 Python 和 Pygame 模块构建一个游戏框架](/article-10850-1.html)
* [如何在你的 Python 游戏中添加一个玩家](/article-10858-1.html)
* [用 Pygame 使你的游戏角色移动起来](/article-10874-1.html)
* [如何向你的 Python 游戏中添加一个敌人](/article-10883-1.html)

一个平台类游戏需要平台。

在 [Pygame](https://www.pygame.org/news) 中，平台本身也是个妖精，正像你那个可玩的妖精。这一点是重要的，因为有个是对象的平台，可以使你的玩家妖精更容易与之互动。

创建平台有两个主要步骤。首先，你必须给该对象编写代码，然后，你必须映射出你希望该对象出现的位置。

### 编码平台对象

要构建一个平台对象，你要创建一个名为 `Platform` 的类。它是一个妖精，正像你的 `Player` [妖精](https://opensource.com/article/17/12/game-python-add-a-player) 一样，带有很多相同的属性。

你的 `Platform` 类需要知道很多平台类型的信息，它应该出现在游戏世界的哪里、它应该包含的什么图片等等。这其中很多信息可能还尚不存在，这要看你为你的游戏计划了多少，但是没有关系。正如直到[移动你的游戏角色](https://opensource.com/article/17/12/game-python-moving-player)那篇文章结束时，你都没有告诉你的玩家妖精移动速度有多快，你不必事先告诉 `Platform` 每一件事。

在这系列中你所写的脚本的开头附近，创建一个新的类。在这个代码示例中前三行是用于说明上下文，因此在注释的下面添加代码：

```
import pygame
import sys
import os
## 新代码如下：

class Platform(pygame.sprite.Sprite):
# x location, y location, img width, img height, img file
def __init__(self,xloc,yloc,imgw,imgh,img):
    pygame.sprite.Sprite.__init__(self)
    self.image = pygame.image.load(os.path.join('images',img)).convert()
    self.image.convert_alpha()
    self.image.set_colorkey(ALPHA)
    self.rect = self.image.get_rect()
    self.rect.y = yloc
    self.rect.x = xloc
```

当被调用时，这个类在某个 X 和 Y 位置上创建一个屏上对象，具有某种宽度和高度，并使用某种图像作为纹理。这与如何在屏上绘制出玩家或敌人非常类似。

### 平台的类型

下一步是绘制出你的平台需要出现的地方。

#### 瓷砖方式

实现平台类游戏世界有几种不同的方法。在最初的横向滚轴游戏中，例如，马里奥超级兄弟和刺猬索尼克，这个技巧是使用“瓷砖”方式，也就是说有几个代表地面和各种平台的块，并且这些块被重复使用来制作一个关卡。你只能有 8 或 12 种不同的块，你可以将它们排列在屏幕上来创建地面、浮动的平台，以及你游戏中需要的一切其它的事物。有人发现这是制作游戏最容易的方法了，因为你只需要制作（或下载）一小组关卡素材就能创建很多不同的关卡。然而，这里的代码需要一点数学知识。

*[SuperTux](https://www.supertux.org/) ，一个基于瓷砖的电脑游戏。*

#### 手工绘制方式

另一种方法是将每个素材作为一个整体图像。如果你喜欢为游戏世界创建素材，那你会在用图形应用程序构建游戏世界的每个部分上花费很多时间。这种方法不需要太多的数学知识，因为所有的平台都是整体的、完整的对象，你只需要告诉 [Python](https://www.python.org/) 将它们放在屏幕上的什么位置。

每种方法都有优势和劣势，并且根据于你选择使用的方式，代码稍有不同。我将覆盖这两方面，所以你可以在你的工程中使用一种或另一种，甚至两者的混合。

### 关卡绘制

总的来说，绘制你的游戏世界是关卡设计和游戏编程中的一个重要的部分。这需要数学知识，但是没有什么太难的，而且 Python 擅长数学，它会有所帮助。

你也许发现先在纸张上设计是有用的。拿一张表格纸，并绘制一个方框来代表你的游戏窗体。在方框中绘制平台，并标记其每一个平台的 X 和 Y 坐标，以及它的宽度和高度。在方框中的实际位置没有必要是精确的，你只要保持数字合理即可。譬如，假设你的屏幕是 720 像素宽，那么你不能在一个屏幕上放 8 块 100 像素的平台。

当然，不是你游戏中的所有平台都必须容纳在一个屏幕大小的方框里，因为你的游戏将随着你的玩家行走而滚动。所以，可以继续绘制你的游戏世界到第一屏幕的右侧，直到关卡结束。

如果你更喜欢精确一点，你可以使用方格纸。当设计一个瓷砖类的游戏时，这是特别有用的，因为每个方格可以代表一个瓷砖。

*一个关卡地图示例。*

#### 坐标系

你可能已经在学校中学习过[笛卡尔坐标系](https://en.wikipedia.org/wiki/Cartesian_coordinate_system)。你学习的东西也适用于 Pygame，除了在 Pygame 中你的游戏世界的坐标系的原点 `0,0` 是放置在你的屏幕的左上角而不是在中间，是你在地理课上用过的坐标是在中间的。

*在 Pygame 中的坐标示例。*

X 轴起始于最左边的 0，向右无限增加。Y 轴起始于屏幕顶部的 0，向下延伸。

#### 图片大小

如果你不知道你的玩家、敌人、平台是多大的，绘制出一个游戏世界是毫无意义的。你可以在图形程序中找到你的平台或瓷砖的尺寸。例如在 [Krita](https://krita.org/en/) 中，单击“图像”菜单，并选择“属性”。你可以在“属性”窗口的最顶部处找到它的尺寸。

另外，你也可以创建一个简单的 Python 脚本来告诉你的一个图像的尺寸。打开一个新的文本文件，并输入这些代码到其中：

```
#!/usr/bin/env python3

from PIL import Image
import os.path
import sys

if len(sys.argv) > 1:
    print(sys.argv[1])
else:
    sys.exit('Syntax: identify.py [filename]')

pic = sys.argv[1]
dim = Image.open(pic)
X   = dim.size[0]
Y   = dim.size[1]

print(X,Y)
```

保存该文本文件为 `identify.py`。

要使用这个脚本，你必须安装一些额外的 Python 模块，它们包含了这个脚本中新使用的关键字：

```
$ pip3 install Pillow --user
```

一旦安装好，在你游戏工程目录中运行这个脚本：

```
$ python3 ./identify.py images/ground.png
(1080, 97)
```

在这个示例中，地面平台的图形的大小是 1080 像素宽和 97 像素高。

### 平台块

如果你选择单独地绘制每个素材，你必须创建想要插入到你的游戏世界中的几个平台和其它元素，每个素材都放在它自己的文件中。换句话说，你应该让每个素材都有一个文件，像这样：

*每个对象一个图形文件。*

你可以按照你希望的次数重复使用每个平台，只要确保每个文件仅包含一个平台。你不能使用一个文件包含全部素材，像这样：

*你的关卡不能是一个图形文件。*

当你完成时，你可能希望你的游戏看起来像这样，但是如果你在一个大文件中创建你的关卡，你就没有方法从背景中区分出一个平台，因此，要么把对象绘制在它们自己的文件中，要么从一个更大的文件中裁剪出它们，并保存为单独的副本。

**注意：** 如同你的其它素材，你可以使用 [GIMP](https://www.gimp.org/)、Krita、[MyPaint](http://mypaint.org/about/)，或 [Inkscape](https://inkscape.org/en/) 来创建你的游戏素材。

平台出现在每个关卡开始的屏幕上，因此你必须在你的 `Level` 类中添加一个 `platform` 函数。在这里特例是地面平台，它重要到应该拥有它自己的一个组。通过把地面看作一组特殊类型的平台，你可以选择它是否滚动，或它上面是否可以站立，而其它平台可以漂浮在它上面。这取决于你。

添加这两个函数到你的 `Level` 类：

```
def ground(lvl,x,y,w,h):
    ground_list = pygame.sprite.Group()
    if lvl == 1:
        ground = Platform(x,y,w,h,'block-ground.png')
        ground_list.add(ground)

    if lvl == 2:
        print("Level " + str(lvl) )

    return ground_list

def platform( lvl ):
    plat_list = pygame.sprite.Group()
    if lvl == 1:
        plat = Platform(200, worldy-97-128, 285,67,'block-big.png')
        plat_list.add(plat)
        plat = Platform(500, worldy-97-320, 197,54,'block-small.png')
        plat_list.add(plat)
    if lvl == 2:
        print("Level " + str(lvl) )

    return plat_list
```

`ground` 函数需要一个 X 和 Y 位置，以便 Pygame 知道在哪里放置地面平台。它也需要知道平台的宽度和高度，这样 Pygame 知道地面延伸到每个方向有多远。该函数使用你的 `Platform` 类来生成一个屏上对象，然后将这个对象添加到 `ground_list` 组。

`platform` 函数本质上是相同的，除了其有更多的平台。在这个示例中，仅有两个平台，但是你可以想有多少就有多少。在进入一个平台后，在列出另一个前你必须添加它到 `plat_list` 中。如果你不添加平台到组中，那么它将不出现在你的游戏中。

>
> **提示：** 很难想象你的游戏世界的 0 是在顶部，因为在真实世界中发生的情况是相反的；当估计你有多高时，你不会从上往下测量你自己，而是从脚到头顶来测量。
>
>
> 如果对你来说从“地面”上来构建你的游戏世界更容易，将 Y 轴值表示为负数可能有帮助。例如，你知道你的游戏世界的底部是 `worldy` 的值。因此 `worldy` 减去地面的高度（在这个示例中是 97）是你的玩家正常站立的位置。如果你的角色是 64 像素高，那么地面减去 128 正好是你的玩家的两倍高。事实上，一个放置在 128 像素处平台大约是相对于你的玩家的两层楼高度。一个平台在 -320 处比三层楼更高。等等。
>
>
>

正像你现在可能所知的，如果你不使用它们，你的类和函数是没有价值的。添加这些代码到你的设置部分（第一行只是上下文，所以添加最后两行）：

```
enemy_list  = Level.bad( 1, eloc )
ground_list = Level.ground( 1,0,worldy-97,1080,97 )
plat_list   = Level.platform( 1 )
```

并把这些行加到你的主循环（再一次，第一行仅用于上下文）：

```
enemy_list.draw(world)  # 刷新敌人
ground_list.draw(world)  # 刷新地面
plat_list.draw(world)  # 刷新平台
```

### 瓷砖平台

瓷砖类游戏世界更容易制作，因为你只需要在前面绘制一些块，就能在游戏中一再使用它们创建每个平台。在像 [OpenGameArt.org](https://opengameart.org/content/simplified-platformer-pack) 这样的网站上甚至有一套瓷砖供你来使用。

`Platform` 类与在前面部分中的类是相同的。

`ground` 和 `platform` 在 `Level` 类中，然而，必须使用循环来计算使用多少块来创建每个平台。

如果你打算在你的游戏世界中有一个坚固的地面，这种地面是很简单的。你只需要从整个窗口的一边到另一边“克隆”你的地面瓷砖。例如，你可以创建一个 X 和 Y 值的列表来规定每个瓷砖应该放置的位置，然后使用一个循环来获取每个值并绘制每一个瓷砖。这仅是一个示例，所以不要添加这到你的代码：

```
# Do not add this to your code
gloc = [0,656,64,656,128,656,192,656,256,656,320,656,384,656]
```

不过，如果你仔细看，你可以看到所有的 Y 值是相同的，X 值以 64 的增量不断地增加 —— 这就是瓷砖的大小。这种重复是精确地，是计算机擅长的，因此你可以使用一点数学逻辑来让计算机为你做所有的计算：

添加这些到你的脚本的设置部分：

```
gloc = []
tx   = 64
ty   = 64

i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1

ground_list = Level.ground( 1,gloc,tx,ty )
```

现在，不管你的窗口的大小，Python 会通过瓷砖的宽度分割游戏世界的宽度，并创建一个数组列表列出每个 X 值。这里不计算 Y 值，因为在平的地面上这个从不会变化。

为了在一个函数中使用数组，使用一个 `while` 循环，查看每个条目并在适当的位置添加一个地面瓷砖：

```
def ground(lvl,gloc,tx,ty):
    ground_list = pygame.sprite.Group()
    i=0
    if lvl == 1:
        while i < len(gloc):
            ground = Platform(gloc[i],worldy-ty,tx,ty,'tile-ground.png')
            ground_list.add(ground)
            i=i+1

    if lvl == 2:
        print("Level " + str(lvl) )

    return ground_list
```

除了 `while` 循环，这几乎与在上面一部分中提供的瓷砖类平台的 `ground` 函数的代码相同。

对于移动的平台，原理是相似的，但是这里有一些技巧可以使它简单。

你可以通过它的起始像素（它的 X 值）、距地面的高度（它的 Y 值）、绘制多少瓷砖来定义一个平台，而不是通过像素绘制每个平台。这样，你不必操心每个平台的宽度和高度。

这个技巧的逻辑有一点复杂，因此请仔细复制这些代码。有一个 `while` 循环嵌套在另一个 `while` 循环的内部，因为这个函数必须考虑每个数组项的三个值来成功地建造一个完整的平台。在这个示例中，这里仅有三个平台以 `ploc.append` 语句定义，但是你的游戏可能需要更多，因此你需要多少就定义多少。当然，有一些不会出现，因为它们远在屏幕外，但是一旦当你进行滚动时，它们将呈现在眼前。

```
def platform(lvl,tx,ty):
    plat_list = pygame.sprite.Group()
    ploc = []
    i=0
    if lvl == 1:
        ploc.append((200,worldy-ty-128,3))
        ploc.append((300,worldy-ty-256,3))
        ploc.append((500,worldy-ty-128,4))
        while i < len(ploc):
            j=0
            while j <= ploc[i][2]:
                plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'tile.png')
                plat_list.add(plat)
                j=j+1
            print('run' + str(i) + str(ploc[i]))
            i=i+1

    if lvl == 2:
        print("Level " + str(lvl) )

    return plat_list
```

要让这些平台出现在你的游戏世界，它们必须出现在你的主循环中。如果你还没有这样做，添加这些行到你的主循环（再一次，第一行仅被用于上下文）中：

```
        enemy_list.draw(world)  # 刷新敌人
        ground_list.draw(world) # 刷新地面
        plat_list.draw(world)   # 刷新平台
```

启动你的游戏，根据需要调整你的平台的放置位置。如果你看不见屏幕外产生的平台，不要担心；你不久后就可以修复它。

到目前为止，这是游戏的图片和代码：

*到目前为止，我们的 Pygame 平台。*

```
#!/usr/bin/env python3
# draw a world
# add a player and player control
# add player movement
# add enemy and basic collision
# add platform

# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.

import pygame
import sys
import os

'''
Objects
'''

class Platform(pygame.sprite.Sprite):
    # x location, y location, img width, img height, img file
    def __init__(self,xloc,yloc,imgw,imgh,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img)).convert()
        self.image.convert_alpha()
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc

class Player(pygame.sprite.Sprite):
    '''
    Spawn a player
    '''
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.score = 1
        self.images = []
        for i in range(1,9):
            img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect  = self.image.get_rect()

    def control(self,x,y):
        '''
        control player movement
        '''
        self.movex += x
        self.movey += y

    def update(self):
        '''
        Update sprite position
        '''

        self.rect.x = self.rect.x + self.movex
        self.rect.y = self.rect.y + self.movey

        # moving left
        if self.movex < 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[self.frame//ani]

        # moving right
        if self.movex > 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[(self.frame//ani)+4]

        # collisions
        enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in enemy_hit_list:
            self.health -= 1
            print(self.health)

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.health -= 1
            print(self.health)

class Enemy(pygame.sprite.Sprite):
    '''
    Spawn an enemy
    '''
    def __init__(self,x,y,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img))
        #self.image.convert_alpha()
        #self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        '''
        enemy movement
        '''
        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance*2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1

class Level():
    def bad(lvl,eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0],eloc[1],'yeti.png') # spawn enemy
            enemy_list = pygame.sprite.Group() # create enemy group
            enemy_list.add(enemy)              # add enemy to group

        if lvl == 2:
            print("Level " + str(lvl) )

        return enemy_list

    def loot(lvl,lloc):
        print(lvl)

    def ground(lvl,gloc,tx,ty):
        ground_list = pygame.sprite.Group()
        i=0
        if lvl == 1:
            while i < len(gloc):
                print("blockgen:" + str(i))
                ground = Platform(gloc[i],worldy-ty,tx,ty,'ground.png')
                ground_list.add(ground)
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return ground_list

'''
Setup
'''
worldx = 960
worldy = 720

fps = 40 # frame rate
ani = 4  # animation cycles
clock = pygame.time.Clock()
pygame.init()
main = True

BLUE  = (25,25,200)
BLACK = (23,23,23 )
WHITE = (254,254,254)
ALPHA = (0,255,0)

world = pygame.display.set_mode([worldx,worldy])
backdrop = pygame.image.load(os.path.join('images','stage.png')).convert()
backdropbox = world.get_rect()
player = Player() # spawn player
player.rect.x = 0
player.rect.y = 0
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10 # how fast to move

eloc = []
eloc = [200,20]
gloc = []
#gloc = [0,630,64,630,128,630,192,630,256,630,320,630,384,630]
tx = 64 #tile size
ty = 64 #tile size

i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1
    print("block: " + str(i))

enemy_list = Level.bad( 1, eloc )
ground_list = Level.ground( 1,gloc,tx,ty )

'''
Main loop
'''
while main == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()
            main = False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(-steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps,0)
            if event.key == ord('q'):
                pygame.quit()
                sys.exit()
                main = False

#    world.fill(BLACK)
    world.blit(backdrop, backdropbox)
    player.update()
    player_list.draw(world) #refresh player position
    enemy_list.draw(world)  # refresh enemies
    ground_list.draw(world)  # refresh enemies
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)
```

（LCTT 译注：到本文翻译完为止，该系列已经近一年没有继续更新了~）

---

via: <https://opensource.com/article/18/7/put-platforms-python-game>

作者：[Seth Kenlon](https://opensource.com/users/seth) 选题：[lujun9972](https://github.com/lujun9972) 译者：[robsean](https://github.com/robsean) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


3 款在 Fedora 中管理个人财务的应用

# 3 款在 Fedora 中管理个人财务的应用

网上有很多可以用来管理你个人财务的服务。虽然它们可能很方便，但这通常也意味着将你最宝贵的个人数据放在你无法监控的公司。也有些人对这些不太在意。

无论你是否在意，你可能会对你自己系统上的应用感兴趣。这意味着如果你不想，你的数据永远不会离开自己的计算机。这三款之一可能就是你想找的。

### HomeBank

HomeBank 是一款可以管理多个账户的全功能软件。它很容易设置并保持更新。它有多种方式画出你的分类和负债，以便你可以看到资金流向何处。它可以通过官方 Fedora 仓库下载。

要安装 HomeBank，请打开“软件中心”，搜索 “HomeBank”，然后选择该应用。单击“安装”将其添加到你的系统中。HomeBank 也可以通过 Flatpak 安装。

### KMyMoney

KMyMoney 是一个成熟的应用，它已经存在了很长一段时间。它有一系列稳定的功能，可帮助你管理多个帐户，包括资产、负债、税收等。KMyMoney 包含一整套用于管理投资和进行预测的工具。它还提供大量报告，以了解你的资金运作方式。

要安装它，请使用软件中心，或使用命令行：

```
$ sudo dnf install kmymoney
```

### GnuCash

用于个人财务的最受欢迎的免费 GUI 应用之一是 GnuCash。GnuCash 不仅可以用于个人财务。它还有管理企业收入、资产和负债的功能。这并不意味着你不能用它来管理自己的账户。从查看[在线教程和指南](https://www.gnucash.org/viewdoc.phtml?rev=3&lang=C&doc=guide)开始了解。

打开“软件中心”，搜索 “GnuCash”，然后选择应用。单击“安装”将其添加到你的系统中。或者如上所述使用 `dnf install` 来安装 “gnucash” 包。

它现在可以通过 Flathub 安装，这使得安装变得简单。如果你没有安装 Flathub，请查看 [Fedora Magazine 上的这篇文章](https://fedoramagazine.org/install-flathub-apps-fedora/)了解如何使用它。这样你也可以在终端使用 `flatpak install gnucash` 命令。

---

照片由 [Fabian Blank](https://unsplash.com/photos/pElSkGRA2NU?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 拍摄，发布在 [Unsplash](https://unsplash.com/search/photos/money?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 上。

---

via: <https://fedoramagazine.org/3-apps-to-manage-personal-finances-in-fedora/>

作者：[Paul W. Frields](https://fedoramagazine.org/author/pfrields/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


# 用 GHTorrent 和 Libraries.io 查询 10 年的 GitHub 数据

>
> 有一种方法可以在没有任何本地基础设施的情况下使用开源数据集探索 GitHub 数据。
>
>
>

我一直在寻找新的数据集，以用它们来展示我们团队工作的力量。[CHAOSSEARCH](https://chaossearch.io/) 可以将你的 [Amazon S3](https://aws.amazon.com/s3/) 对象存储数据转换为完全可搜索的 [Elasticsearch](https://www.elastic.co/) 式集群。使用 Elasticsearch API 或 [Kibana](https://www.elastic.co/products/kibana) 等工具，你可以查询你所要找的任何数据。

当我找到 [GHTorrent](http://ghtorrent.org) 项目进行探索时，我很兴奋。GHTorrent 旨在通过 GitHub API 构建所有可用数据的离线版本。如果你喜欢数据集，这是一个值得一看的项目，甚至你可以考虑[捐赠一个 GitHub API 密钥](http://ghtorrent.org/services.html)。

### 访问 GHTorrent 数据

有许多方法可以访问和使用 [GHTorrent 的数据](http://ghtorrent.org/downloads.html)，它以 [NDJSON](http://ndjson.org) 格式提供。这个项目可以以多种形式提供数据，包括用于恢复到 [MySQL](https://en.wikipedia.org/wiki/MySQL) 数据库的 [CSV](https://en.wikipedia.org/wiki/Comma-separated_values)，可以转储所有对象的 [MongoDB](https://www.mongodb.com/)，以及用于将数据直接导出到 Google 对象存储中的 Google Big Query（免费）。 有一点需要注意：这个数据集有从 2008 年到 2017 年几乎完整的数据集，但从 2017 年到现在的数据还不完整。这将影响我们确定性查询的能力，但它仍然是一个令人兴奋的信息量。

我选择 Google Big Query 来避免自己运行任何数据库，那么我就可以很快下载包括用户和项目在内的完整数据库。CHAOSSEARCH 可以原生分析 NDJSON 格式，因此在将数据上传到 Amazon S3 之后，我能够在几分钟内对其进行索引。CHAOSSEARCH 平台不要求用户设置索引模式或定义其数据的映射，它可以发现所有字段本身（字符串、整数等）。

随着我的数据完全索引并准备好进行搜索和聚合，我想深入了解看看我们可以发现什么，比如哪些软件语言是 GitHub 项目最受欢迎的。

（关于格式化的说明：下面这是一个有效的 JSON 查询，我们不会在这里正确格式化以避免滚动疲劳。要正确格式化它，你可以在本地复制它并发送到命令行实用程序，如 [jq](https://stedolan.github.io/jq/)。）

```
{"aggs":{"2":{"date_histogram":{"field":"root.created_at","interval":"1M","time_zone":"America/New_York","min_doc_count":1}}},"size":0,"_source":{"excludes":[]},"stored_fields":["*"],"script_fields":{},"docvalue_fields":["root.created_at","root.updated_at"],"query":{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[{"match_phrase":{"root.language":{"query":""}}}]}}}
```

对于那些近年来跟踪开源语言状态的人来说，这个结果并不令人惊讶。

[JavaScript](https://en.wikipedia.org/wiki/JavaScript) 仍然是卫冕冠军，虽然有些人认为 JavaScript 正在逐渐消失，但它仍然是 800 磅重的大猩猩，很可能会保持这种状态一段时间。[Java](/resources/java) 面临类似的谣言，但这些数据表明它是开源生态系统的重要组成部分。

考虑到像 [Docker](/resources/what-docker) 和 [Kubernetes](/resources/what-is-kubernetes) 这样的项目的流行，你可能会想，“Go（[Golang](https://golang.org/)）怎么样？”这是一个提醒的好时机，这里讨论的 GitHub 数据集包含一些空缺，最明显的是在 2017 年之后我看到 Golang 项目随处可见，而这里并没有显示。我希望用完整的 GitHub 数据集重复此搜索，看看它是否会改变排名。

现在让我们来探讨项目创建的速度。 （提醒：这是为了便于阅读而合并的有效 JSON。）

```
{"aggs":{"2":{"date_histogram":{"field":"root.created_at","interval":"1M","time_zone":"America/New_York","min_doc_count":1}}},"size":0,"_source":{"excludes":[]},"stored_fields":["*"],"script_fields":{},"docvalue_fields":["root.created_at","root.updated_at"],"query":{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[{"match_phrase":{"root.language":{"query":""}}}]}}}
```

我们可以看到创建新项目的速度，也会给人留下深刻的印象，从 2012 年左右开始大幅增长：

既然我知道了创建项目的速度以及用于创建这些项目的最流行的语言，我还想知道这些项目选择的开源许可证。遗憾的是，这个 GitHub 项目数据集中并不存在这些数据，但是 [Tidelift](https://tidelift.com) 的精彩团队在 [Libraries.io](http://libraries.io/) [数据](https://libraries.io/data) 里发布了一个 GitHub 项目的详细列表，包括使用的许可证以及其中有关开源软件状态的其他详细信息。将此数据集导入 CHAOSSEARCH 只花了几分钟，让我看看哪些开源软件许可证在 GitHub 上最受欢迎：

（提醒：这是为了便于阅读而合并的有效 JSON。）

```
{"aggs":{"2":{"terms":{"field":"Repository License","size":10,"order":{"_count":"desc"}}}},"size":0,"_source":{"excludes":[]},"stored_fields":["*"],"script_fields":{},"docvalue_fields":["Created Timestamp","Last synced Timestamp","Latest Release Publish Timestamp","Updated Timestamp"],"query":{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[{"match_phrase":{"Repository License":{"query":""}}}]}}}
```

结果显示了一些重要的异常值：

如你所见，[MIT 许可证](https://opensource.org/licenses/MIT) 和 [Apache 2.0 许可证](https://opensource.org/licenses/Apache-2.0) 的开源项目远远超过了其他大多数开源许可证，而 [各种 BSD 和 GPL 许可证](https://opensource.org/licenses) 则差得很远。鉴于 GitHub 的开放模式，我不能说我对这些结果感到惊讶。我猜想是用户（而不是公司）创建了大多数项目，并且他们使用 MIT 许可证可以使其他人轻松地使用、共享和贡献。而鉴于有不少公司希望确保其商标得到尊重并为其业务提供开源组件，那么 Apache 2.0 许可证数量高企的背后也是有道理的。

现在我确定了最受欢迎的许可证，我很想看看最少使用的许可证。通过调整我的上一个查询，我将前 10 名逆转为最后 10 名，并且只找到了两个使用 [伊利诺伊大学 - NCSA 开源许可证](https://tldrlegal.com/license/university-of-illinois---ncsa-open-source-license-(ncsa)) 的项目。我之前从未听说过这个许可证，但它与 Apache 2.0 非常接近。看到所有 GitHub 项目中使用了多少个不同的软件许可证，这很有意思。

之后，我针对特定语言（JavaScript）来查看最常用的许可证。（提醒：这是为了便于阅读而合并的有效JSON。）

```
{"aggs":{"2":{"terms":{"field":"Repository License","size":10,"order":{"_count":"desc"}}}},"size":0,"_source":{"excludes":[]},"stored_fields":["*"],"script_fields":{},"docvalue_fields":["Created Timestamp","Last synced Timestamp","Latest Release Publish Timestamp","Updated Timestamp"],"query":{"bool":{"must":[{"match_phrase":{"Repository Language":{"query":"JavaScript"}}}],"filter":[{"match_all":{}}],"should":[],"must_not":[{"match_phrase":{"Repository License":{"query":""}}}]}}}
```

这个输出有一些意外。

尽管使用 `npm init` 创建的 [NPM](https://www.npmjs.com/) 模块的默认许可证是来自 [Internet Systems Consortium（ISC）](https://en.wikipedia.org/wiki/ISC_license) 的许可证，但你可以看到相当多的这些项目使用 MIT 以及 Apache 2.0 的开源许可证。

由于 Libraries.io 数据集中包含丰富的开源项目内容，并且由于 GHTorrent 数据缺少最近几年的数据（因此缺少有关 Golang 项目的任何细节），因此我决定运行类似的查询来查看 Golang 项目是如何许可他们的代码的。

（提醒：这是为了便于阅读而合并的有效 JSON。）

```
{"aggs":{"2":{"terms":{"field":"Repository License","size":10,"order":{"_count":"desc"}}}},"size":0,"_source":{"excludes":[]},"stored_fields":["*"],"script_fields":{},"docvalue_fields":["Created Timestamp","Last synced Timestamp","Latest Release Publish Timestamp","Updated Timestamp"],"query":{"bool":{"must":[{"match_phrase":{"Repository Language":{"query":"Go"}}}],"filter":[{"match_all":{}}],"should":[],"must_not":[{"match_phrase":{"Repository License":{"query":""}}}]}}}
```

结果与 Javascript 完全不同。

Golang 项目与 JavaScript 项目惊人逆转 —— 使用 Apache 2.0 的 Golang 项目几乎是 MIT 许可证的三倍。虽然很难准确地解释为什么会出现这种情况，但在过去的几年中，Golang 已经出现了大规模的增长，特别是在开源和商业化的项目和软件产品公司中。

正如我们上面所了解的，这些公司中的许多公司都希望强制执行其商标策略，因此转向 Apache 2.0 许可证是有道理的。

#### 总结

最后，我通过深入了解 GitHub 用户和项目的数据找到了一些有趣的结果。其中一些我肯定会猜到，但是一些结果对我来说也是惊喜，特别是像很少使用的 NCSA 许可证这样的异常值。

总而言之，你可以看到 CHAOSSEARCH 平台能够快速轻松地找到有趣问题的复杂答案。我无需自己运行任何数据库就可以深入研究这个数据集，甚至可以在 Amazon S3 上以低成本的方式存储数据，因此无需维护。 现在，我可以随时查询有关这些数据的任何其他问题。

你对数据提出了哪些其他问题，以及你使用了哪些数据集？请在评论或推特上告诉我 [@petecheslock](https://twitter.com/petecheslock)。

本文的一个版本最初发布在 [CHAOSSEARCH](https://chaossearch.io/blog/where-are-the-github-users-part-1/)，有更多结果可供发现。

---

via: <https://opensource.com/article/19/5/chaossearch-github-ghtorrent>

作者：[Pete Cheslock](https://opensource.com/users/petecheslock/users/ghaff/users/payalsingh/users/davidmstokes) 选题：[lujun9972](https://github.com/lujun9972) 译者：[wxy](https://github.com/wxy) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


systemd 代码已超 120 万行！五年间翻了一番

Summary: 五年前， Phoronix 发现 systemd 源码树已经接近 55 万行，所以好奇之下，让我们来看看今天的 systemd Git 存储库有多大：现在已超过 120 万行了！

# systemd 代码已超 120 万行！五年间翻了一番

五年前， Phoronix 发现 [systemd 源码树已经接近 55 万行](https://www.phoronix.com/scan.php?page=news_item&px=MTY5NjM)，所以好奇之下，让我们来看看今天的 systemd Git 存储库有多大：现在已超过 120 万行了！

*systemd 代码行数*

在 2017 年超过 [systemd 的代码超过了 100 万行](https://www.phoronix.com/scan.php?page=news_item&px=systemd-2017-Git-Activity)之后，如今在 systemd 的 Git 存储库上运行 GitStats 时，发现它已经有 1,207,302 行了。这 120 万行分布在 3,260 个文件中，来自近 1,400 个不同作者的 40,057 个提交。

*systemd 提交数*

去年，systemd 出现了创纪录的提交数量，但到目前为止，2019 年恐怕很难再次看到突破该记录。到目前为止，今年已有 2,145 个提交，而去年有 6,245 个提交，而 2016 年和 2017 年每年的提交总数不到 4 千个。

Lennart Poettering 依旧是 systemd 最多产的贡献者，今年迄今为止他贡献了超过 32％ 的提交。今年其他多产贡献者包括 Yu Watanabe、ZbigniewJędrzejewski-Szmek、Frantisek Sumsal、Susant Sahani 和 Evgeny Vereshchagin 等人。到目前为止，已有大约 142 人为 systemd 的源代码树做出了贡献。

对于那些对其他系统统计数据感兴趣的人，请参阅[最新的 GitStats 输出](https://phoronix.com/misc/systemd-201905/index.html)。