分类 技术 下的文章

当你正在创建 LXD 容器的时候,你希望它们能被预先配置好。例如在容器一启动就自动执行 apt update来安装一些软件包,或者运行一些命令。

这篇文章将讲述如何用 cloud-init 来对 LXD 容器进行进行早期初始化

接下来,我们将创建一个包含cloud-init指令的LXD profile,然后启动一个新的容器来使用这个profile。

如何创建一个新的 LXD profile

查看已经存在的 profile:

$ lxc profile list
+---------|---------+
| NAME    | USED BY |
+---------|---------+
| default | 11      |
+---------|---------+

我们把名叫 default 的 profile 复制一份,然后在其内添加新的指令:

$ lxc profile copy default devprofile

$ lxc profile list
+------------|---------+
| NAME       | USED BY |
+------------|---------+
| default    | 11      |
+------------|---------+
| devprofile | 0       |
+------------|---------+

我们就得到了一个新的 profile: devprofile。下面是它的详情:

$ lxc profile show devprofile
config:
 environment.TZ: ""
description: Default LXD profile
devices:
 eth0:
 nictype: bridged
 parent: lxdbr0
 type: nic
 root:
 path: /
 pool: default
 type: disk
name: devprofile
used_by: []

注意这几个部分: config:description:devices:name:used_by:,当你修改这些内容的时候注意不要搞错缩进。(LCTT 译注:因为这些内容是 YAML 格式的,缩进是语法的一部分)

如何把 cloud-init 添加到 LXD profile 里

cloud-init 可以添加到 LXD profile 的 config 里。当这些指令将被传递给容器后,会在容器第一次启动的时候执行。

下面是用在示例中的指令:

 package_upgrade: true
 packages:
 - build-essential
 locale: es_ES.UTF-8
 timezone: Europe/Madrid
 runcmd:
 - [touch, /tmp/simos_was_here]

package_upgrade: true 是指当容器第一次被启动时,我们想要 cloud-init 运行 sudo apt upgradepackages: 列出了我们想要自动安装的软件。然后我们设置了 localetimezone。在 Ubuntu 容器的镜像里,root 用户默认的 localeC.UTF-8,而 ubuntu 用户则是 en_US.UTF-8。此外,我们把时区设置为 Etc/UTC。最后,我们展示了如何使用 runcmd 来运行一个 Unix 命令

我们需要关注如何将 cloud-init 指令插入 LXD profile。

我首选的方法是:

$ lxc profile edit devprofile

它会打开一个文本编辑器,以便你将指令粘贴进去。结果应该是这样的

$ lxc profile show devprofile
config:
  environment.TZ: ""
  user.user-data: |
    #cloud-config
    package_upgrade: true
    packages:
      - build-essential
    locale: es_ES.UTF-8
    timezone: Europe/Madrid
    runcmd:
      - [touch, /tmp/simos_was_here]
description: Default LXD profile
devices:
  eth0:
    nictype: bridged
    parent: lxdbr0
    type: nic
  root:
    path: /
    pool: default
    type: disk
name: devprofile
used_by: []

如何使用 LXD profile 启动一个容器

使用 profile devprofile 来启动一个新容器:

$ lxc launch --profile devprofile ubuntu:x mydev

然后访问该容器来查看我们的指令是否生效:

$ lxc exec mydev bash
root@mydev:~# ps ax
 PID TTY STAT TIME COMMAND
 1 ? Ss 0:00 /sbin/init
 ...
 427 ? Ss 0:00 /usr/bin/python3 /usr/bin/cloud-init modules --mode=f
 430 ? S 0:00 /bin/sh -c tee -a /var/log/cloud-init-output.log
 431 ? S 0:00 tee -a /var/log/cloud-init-output.log
 432 ? S 0:00 /usr/bin/apt-get --option=Dpkg::Options::=--force-con
 437 ? S 0:00 /usr/lib/apt/methods/http
 438 ? S 0:00 /usr/lib/apt/methods/http
 440 ? S 0:00 /usr/lib/apt/methods/gpgv
 570 ? Ss 0:00 bash
 624 ? S 0:00 /usr/lib/apt/methods/store
 625 ? R+ 0:00 ps ax
root@mydev:~#

如果我们连接得够快,通过 ps ax 将能够看到系统正在更新软件。我们可以从 /var/log/cloud-init-output.log 看到完整的日志:

Generating locales (this might take a while)...
 es_ES.UTF-8... done
Generation complete.

以上可以看出 locale 已经被更改了。root 用户还是保持默认的 C.UTF-8,只有非 root 用户 ubuntu 使用了新的locale 设置。

Hit:1 http://archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:3 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]

以上是安装软件包之前执行的 apt update

The following packages will be upgraded:
 libdrm2 libseccomp2 squashfs-tools unattended-upgrades
4 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 211 kB of archives.

以上是在执行 package_upgrade: true 和安装软件包。

The following NEW packages will be installed:
 binutils build-essential cpp cpp-5 dpkg-dev fakeroot g++ g++-5 gcc gcc-5
 libalgorithm-diff-perl libalgorithm-diff-xs-perl libalgorithm-merge-perl

以上是我们安装 build-essential 软件包的指令。

runcmd 执行的结果如何?

root@mydev:~# ls -l /tmp/
total 1
-rw-r--r-- 1 root root 0 Jan 3 15:23 simos_was_here
root@mydev:~#

可见它已经生效了!

结论

当我们启动 LXD 容器的时候,我们常常需要默认启用一些配置,并且希望能够避免重复工作。通常解决这个问题的方法是创建 LXD profile,然后把需要的配置添加进去。最后,当我们启动新的容器时,只需要应用该 LXD profile 即可。


via: https://blog.simos.info/how-to-preconfigure-lxd-containers-with-cloud-init/

作者:Simos Xenitellis 译者:kaneg 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

是不是总是想要 更快的 torrent 速度?不管现在的速度有多块,但总是无法对此满足。我们对 torrent 速度的痴迷使我们经常从包括 YouTube 视频在内的许多网站上寻找并应用各种所谓的技巧。但是相信我,从小到大我就没发现哪个技巧有用过。因此本文我们就就来看看,为什么尝试提高 torrent 速度是行不通的。

影响速度的因素

本地因素

从下图中可以看到 3 台电脑分别对应的 A、B、C 三个用户。A 和 B 本地相连,而 C 的位置则比较远,它与本地之间有 1、2、3 三个连接点。

若用户 A 和用户 B 之间要分享文件,他们之间直接分享就能达到最大速度了而无需使用 torrent。这个速度跟互联网什么的都没有关系。

  • 网线的性能
  • 网卡的性能
  • 路由器的性能

当谈到 torrent 的时候,人们都是在说一些很复杂的东西,但是却总是不得要点。

LAN wire,network cards,router

现在我们谈论的是本地文件分享,目前较新一点的网卡速率大约 1 Gigabit 左右。常见的路由器大约 300 Megabits 而网线大概能达到 100 Megabits。当然具体的配置可能不一样,我这里只是给一个一般性的例子。

由于你有一个 1 Gigabit 的网卡,你可能会觉得你的下载速度应该达到 125 Megabytes。但是路由器只允许传输 300 Megabits 也就是 30 Megabyte。

即使你把目标降到 30 Megabytes,然而你连接到路由器的电缆/网线的性能最多只有 100 megabits 也就是 10 MegaBytes。这是一个纯粹的瓶颈问题,由一个薄弱的环节影响到了其他强健部分,也就是说这个传输速率只能达到 10 Megabytes,即电缆的极限速度。现在想象有一个 torrent 即使能够用最大速度进行下载,那也会由于你的硬件不够强大而导致瓶颈。

外部因素

现在再来看一下这幅图。用户 C 在很遥远的某个地方。甚至可能在另一个国家。

how torrent works

假设这个用户创建了一个 torrent 而你想去下载它。第一个问题就是我们刚才讨论过的,你不可能跨越 10 Megabyte 这道坎。

第二,由于 C 与本地之间多个有连接点,其中一个点就有可能成为瓶颈所在,可能由于繁重的流量和相对薄弱的硬件导致了缓慢的速度。

做种者与吸血者

关于此已经有了太多的讨论,总的想法就是搜索更多的种子,但要注意上面的那些因素,有一个很好的种子提供者,但是跟我之间的连接不好的话那也是无济于事的。通常,这不可能发生,因为我们也不是唯一下载这个资源的人,一般都会有一些在本地的人已经下载好了这个文件并已经在做种了。

结论

我们尝试搞清楚哪些因素影响了 torrent 速度的好坏。不管我们如何用软件进行优化,大多数时候是这是由于物理瓶颈导致的。我从来不关心那些软件,使用默认配置对我来说就够了。

希望你会喜欢这篇文章,有什么想法敬请留言。


via: http://www.theitstuff.com/increase-torrent-speed-will-never-work

作者:Rishabh Kandari 译者:lujun9972 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

EncryptPad 是一个自由开源软件,它通过简单方便的图形界面和命令行接口来查看和修改加密的文本,它使用 OpenPGP RFC 4880 文件格式。通过 EncryptPad,你可以很容易的加密或者解密文件。你能够像保存密码、信用卡信息等私人信息,并使用密码或者密钥文件来访问。

特性

  • 支持 windows、Linux 和 Max OS。
  • 可定制的密码生成器,可生成健壮的密码。
  • 随机的密钥文件和密码生成器。
  • 支持 GPG 和 EPD 文件格式。
  • 能够通过 CURL 自动从远程远程仓库下载密钥。
  • 密钥文件的路径能够存储在加密的文件中。如果这样做的话,你不需要每次打开文件都指定密钥文件。
  • 提供只读模式来防止文件被修改。
  • 可加密二进制文件,例如图片、视频、归档等。

在这份教程中,我们将学习如何在 Ubuntu 16.04 中安装和使用 EncryptPad。

环境要求

  • 在系统上安装了 Ubuntu 16.04 桌面版本。
  • 在系统上有 sudo 的权限的普通用户。

安装 EncryptPad

在默认情况下,EncryPad 在 Ubuntu 16.04 的默认仓库是不存在的。你需要安装一个额外的仓库。你能够通过下面的命令来添加它 :

sudo apt-add-repository ppa:nilaimogard/webupd8

下一步,用下面的命令来更新仓库:

sudo apt-get update -y

最后一步,通过下面命令安装 EncryptPad:

sudo apt-get install encryptpad encryptcli -y

当 EncryptPad 安装完成后,你可以在 Ubuntu 的 Dash 上找到它。

使用 EncryptPad 生成密钥和密码

现在,在 Ubunntu Dash 上输入 encryptpad,你能够在你的屏幕上看到下面的图片 :

Ubuntu DeskTop

下一步,点击 EncryptPad 的图标。你能够看到 EncryptPad 的界面,它是一个简单的文本编辑器,带有顶部菜单栏。

EncryptPad screen

首先,你需要生成一个密钥文件和密码用于加密/解密任务。点击顶部菜单栏中的 “Encryption->Generate Key”,你会看见下面的界面:

Generate key

选择文件保存的路径,点击 “OK” 按钮,你将看到下面的界面:

select path

输入密钥文件的密码,点击 “OK” 按钮 ,你将看到下面的界面:

last step

点击 “yes” 按钮来完成该过程。

加密和解密文件

现在,密钥文件和密码都已经生成了。可以执行加密和解密操作了。在这个文件编辑器中打开一个文件文件,点击 “encryption” 图标 ,你会看见下面的界面:

Encry operation

提供需要加密的文件和指定输出的文件,提供密码和前面产生的密钥文件。点击 “Start” 按钮来开始加密的进程。当文件被成功的加密,会出现下面的界面:

Success Encrypt

文件已经被该密码和密钥文件加密了。

如果你想解密被加密后的文件,打开 EncryptPad ,点击 “File Encryption” ,选择 “Decryption” 操作,提供加密文件的位置和你要保存输出的解密文件的位置,然后提供密钥文件地址,点击 “Start” 按钮,它将要求你输入密码,输入你先前加密使用的密码,点击 “OK” 按钮开始解密过程。当该过程成功完成,你会看到 “File has been decrypted successfully” 的消息 。

decrypt []

注意:

如果你遗忘了你的密码或者丢失了密钥文件,就没有其他的方法可以打开你的加密信息了。对于 EncrypePad 所支持的格式是没有后门的。


via: https://www.howtoforge.com/tutorial/how-to-install-and-use-encryptpad-on-ubuntu-1604/

作者:Hitesh Jethva 译者:singledo 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

当你在 Web 浏览器或任何 GUI 登录中输入密码时,密码会被标记成星号 ******** 或圆点符号 ••••••••••••• 。这是内置的安全机制,以防止你附近的用户看到你的密码。但是当你在终端输入密码来执行任何 sudosu 的管理任务时,你不会在输入密码的时候看见星号或者圆点符号。它不会有任何输入密码的视觉指示,也不会有任何光标移动,什么也没有。你不知道你是否输入了所有的字符。你只会看到一个空白的屏幕!

看看下面的截图。

正如你在上面的图片中看到的,我已经输入了密码,但没有任何指示(星号或圆点符号)。现在,我不确定我是否输入了所有密码。这个安全机制也可以防止你附近的人猜测密码长度。当然,这种行为可以改变。这是本指南要说的。这并不困难。请继续阅读。

当你在终端输入密码时显示星号

要在终端输入密码时显示星号,我们需要在 /etc/sudoers 中做一些小修改。在做任何更改之前,最好备份这个文件。为此,只需运行:

sudo cp /etc/sudoers{,.bak}

上述命令将 /etc/sudoers 备份成名为 /etc/sudoers.bak。你可以恢复它,以防万一在编辑文件后出错。

接下来,使用下面的命令编辑 /etc/sudoers

sudo visudo

找到下面这行:

Defaults env_reset

在该行的末尾添加一个额外的单词 ,pwfeedback,如下所示。

Defaults env_reset,pwfeedback

然后,按下 CTRL + xy 保存并关闭文件。重新启动终端以使更改生效。

现在,当你在终端输入密码时,你会看到星号。

如果你对在终端输入密码时看不到密码感到不舒服,那么这个小技巧会有帮助。请注意,当你输入输入密码时其他用户就可以预测你的密码长度。如果你不介意,请按照上述方法进行更改,以使你的密码可见(当然,显示为星号!)。

现在就是这样了。还有更好的东西。敬请关注!

干杯!


via: https://www.ostechnix.com/display-asterisks-type-password-terminal/

作者:SK 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

仓库被封禁

在 2018 年 2 月 20 日,我们的开源项目放在 GitHub 上的仓库由于收到了 DMCA Takedown 投诉被封禁,仓库处于不可访问状态。此时在 GitHub 上访问该仓库时,会显示一个公开消息,表明该仓库被封禁的原因。

按照 GitHub DMCA 的规则,GitHub 在确认投诉有效后,会给该仓库的管理员发送一封邮件,提示该仓库需要在 24 小时内清理被投诉的内容,并回复 GitHub 才行——否则,该仓库会被封禁,禁止任何访问和数据导出。

我们在收到该 Takedown 投诉后,会有 24 小时的时间来响应,但由于过年期间,仓库拥有者没有及时看到邮件,未能及时发现这么严重的通知。因此,在过了 24 小时后, Github 按照 DMCA 的规则,进行了仓库的封禁。

仓库被封禁后,我们发现无法访问。根据封禁消息的提示,发现原来之前仓库内的某个文件出了问题,侵犯了原作者的版权。原作者向 Github 发送了 DMCA 投诉。而由于我们的未及时处理,导致了仓库的最终被封。当我们发现被封时,已经是深夜了。

紧急商讨方案

在被封禁后,由于已经超过了 24 小时时限,在这个阶段下, GitHub 的文档中给出的解决方案仅有请求 GitHub 来删除该仓库并根据自己手里的仓库数据重建的方案。但对我们来说,这种方案是不可接受的,因为这种方案会导致丢失所有的 issue、PR、Wiki,以及你本地的仓库和远程的仓库之间的版本差异。

我们在群内先找了更新最全的 fork,找到了一个群友提供的,和上游只差 2 个提交的版本,并将其保存下来,作为最后的自救手段。

此外,在查询 DMCA 的过程中,我了解到 DMCA 除了有 DMCA Takedown 以外,还有一个 DMCA Counter Notice,用于反向解除 DMCA 封禁。

DMCA Counter Notice

DMCA Counter Notice 用于向服务商发起申诉,说明 DMCA Takedown 投诉为恶意投诉且并无版权问题。

延展阅读

https://www.plagiarismtoday.com/2010/06/03/7-common-questions-about-dmca-counter-notices/

https://help.github.com/articles/guide-to-submitting-a-dmca-counter-notice/

当时考虑到我们已经错过了窗口期,没办法删除 GitHub 上仓库中的特定文件,所以想通过 DMCA Counter Notice 来解除封禁。

为此,我通过 Github 发给我们的邮件,找到了那份侵权文件,并在他的网站中找到了版权拥有者的邮件,发送邮件说明情况,看看能否通过付费获得授权。但其是挪威人,存在时差,所以我们只能边等待,边想办法。

山重水复疑无路,柳暗花明又一村

在准备 DMCA Counter Notice 时,我们又向 Github 发送了邮件,说明了中国春节的特殊情况,导致我们没有来得及处理文件,请求给我们一个机会让我们处理这些文件。但是迟迟没有回应,无奈之下,多位成员又以成员身份向 GitHub 发送邮件,请求给予帮助。

令人惊奇的是,经过大约 9 个小时的等待,仓库拥有者的请求邮件似乎开小差了,而各位成员的请求邮件得到了响应。Github 回信给大家说,根据其规则,给出了额外的 24 小时窗口期,让我们处理这些文件(后来经过仔细查阅 GitHub 的 DMCA Takedown 规则,对这种错过了第一次窗口期的情况,可以给予第二个,也是最后一个窗口期)。但是这个开启额外的窗口期,需要仓库的拥有者向 GitHub 发送邮件请求。

然后,我们就以仓库拥有者的身份再次向 GitHub 发送了请求,可能是由于时差的原因,又是几个小时没有回应。

与此同时,我们也收到了版权拥有者的回复。很遗憾,原作者不愿意授权,也不打算收费。好在 Github 给的额外窗口期,让我们有了改正错误的机会。

还好,在焦急的等待之中,我们终于收到了 GitHub 的回复,并同时恢复了仓库的访问——宝贵的 24 小时窗口期。

使用 BFG 处理文件

得到了窗口期后,我们开始处理仓库内的文件。

首先,你得清除了现在还在仓库里面的文件,然后再使用下面的方面来清除提交历史中的数据。

推荐阅读

以下文章建议按顺序阅读

https://help.github.com/articles/removing-sensitive-data-from-a-repository/

https://rtyley.github.io/bfg-repo-cleaner/

删除 Git 仓库的历史数据有多种方法,一种是使用 git filter-branch来处理,但是速度极慢。另外一种就是使用 BFG 来处理,我们采用的是 BFG 来处理(BFG 是git filter-branch 首字母的逆转)。

BFG 需要 Java 的运行环境,如果无法运行,请检查你的本地 Java 环境是否安装,或高于 Java 7 。

Java 6 需要使用 bfg v1.12.3版本

BFG 的处理过程比较简单,首先,你需要下载 BFG。

wget http://repo1.maven.org/maven2/com/madgag/bfg/1.13.0/bfg-1.13.0.jar

然后克隆你的仓库到本地,比如 bestony/test

git clone --mirror  git://github.com/bestony/test.git

克隆时用不用 --mirror 模式都可以,但是后续命令上会有所差距,所以我还是推荐大家使用镜像模式,毕竟按照官方的文档走,出现了问题也好搜索。(镜像模式克隆的仓库和远程仓库完全一样,但是不能直接看到仓库里面的文件,而且也不能允许 git 的各种命令)

克隆到本地后,执行 BFG 命令来处理文件。

cd test.git
java -jar bfg.jar --delete-files "filename"

这里需要注意的是,filename 不支持目录路径,只能是文件名,而不能是 dir/filename 这样的形式,所以添加参数时你要注意这个。对于有同样名字的文件却想只删除某个目录的情况,可能就没有办法了。

此外,默认情况下, BFG 不会处理最新的提交,它认为你的最新提交应该是干净的(不包含需要删除的敏感数据),如果你要删除的文件是最新的提交(比如你最新的一个提交是删除那些敏感数据),可以加入--no-blob-protection参数来强制清除,也可以再添加一个提交,使包含了要被删除文件的提交不是最新的提交。

java -jar bfg.jar --delete-files "filename" --no-blob-protection

BFG 处理完成后,会提示你使用 git 命令进行垃圾回收,你需要执行如下命令来操作:

cd test.git # 进入目标目录
git reflog expire --expire=now --all && git gc --prune=now --aggressive # 垃圾回收
这里需要注意的是,如果你删除多个文件,每次删除后执行和多个文件都删除后效果一样,所以建议你删除多个文件后再进行垃圾回收,会更省时一些。

处理完成后,将数据推送到远端即可(需要关闭 GitHub 上仓库设置里面对强制推送的防护):

git push

执行完成后,就可以到远端上去看了,你的文件会被删除,相关的提交不会被删除,但是提交里面不包含该文件了。

在推送时,可能会提示你有些更改被拒绝了,这些更改如果是和 Pull Request 有关的,你可以不需要在意,这是 GitHub 自身的问题。Github 设定 Pull Request 是只读不可改的。所以我们无法修改这些信息。

具体可以参考 https://github.com/rtyley/bfg-repo-cleaner/issues/36

至此,我们将文件进行了删除处理,并清除了相关的数据。

后续处理

在完成文件及历史数据的删除后,我们将我们的删除结果回复了 Github ,等待 Github 的确认。GitHub 会在 24 小时收到该回复后,会通知投诉方进行确认。如果投诉方无异议,此事就此结束,不会再有下一步动作。如果有异议,则会重新进行此流程。

此外,由于 GitHub 存在垃圾缓存回收的时间差,所以你推送到 GitHub 上的数据虽然并无需要被删除的文件,但是依旧在一定时间内可以看到。这种缓存只能请 GitHub 自行操作删除。此外,与要删除的文件相关的 Pull Request 也需要 GitHub 来删除——因为用户是没有权限删除 Pull Request 的。这些请求也可以一并发给 GitHub 来操作(但似乎 GitHub 并不热衷执行这些请求,只要被投诉的文件访问不到即可,也就是说,如果没有被投诉历史数据,其实或许并不用大动干戈清理历史……)。

这种清除操作还有一个副作用就是,所有之前 fork 的仓库,由于主仓库被封禁而导致各个 fork 仓库的 remote 意外地变为另外一个仓库(该仓库是最早的一个 fork 仓库)。而主仓库恢复之后,我们并没有找到好的办法将 remote 恢复回原来的主仓库。因此,需要所有成员重新 fork 主仓库并从缓慢的 GitHub 克隆到本地。

余思

这个惊魂事件当中,我们首先要反思的是,我们对版权问题的认识不足,这是一切问题的根源。因此,这之后,我们对既有数据进行了排查。

其次,GitHub 在这种事件的处置上,我们认为也并不够好。这么严重的处置(整个库封禁),仅仅通过一份普通的邮件通知,而且仅仅给出 24 小时的时间窗口。而 GitHub 其实掌握了仓库拥有者的更可靠、更及时的联系方式,比如说手机短信,也完全可以在 GitHub 的网页界面上以显目的方式提醒。另外,虽然 DMCA 规则中提到了可以容情第二个时间窗口,但是似乎这个附加窗口期是后来才改变的政策,在前面的流程说明中并未提及,很容易忽视。

其三,由于封禁会导致对该仓库的所有访问均不可进行,这不仅包括了提交数据,也包括了并没有存在于 Git 仓库中的 issue、PR 和 Wiki 等数据,而 GitHub 不会让你在封禁的情况下有机会导出这些数据。所以,有机会的话,各种数据还是有个备份的好。

最后,感谢在这个事件中,所有不离不弃支持我们的成员,感谢小白进行的仓库清理工作。

相关阅读

Python 中的 urllib.parse 模块提供了很多解析和组建 URL 的函数。

解析url

urlparse() 函数可以将 URL 解析成 ParseResult 对象。对象中包含了六个元素,分别为:

  • 协议(scheme)
  • 域名(netloc)
  • 路径(path)
  • 路径参数(params)
  • 查询参数(query)
  • 片段(fragment)
from urllib.parse import urlparse

url='http://user:pwd@domain:80/path;params?query=queryarg#fragment'

parsed_result=urlparse(url)

print('parsed_result 包含了',len(parsed_result),'个元素')
print(parsed_result)

结果为:

parsed_result 包含了 6 个元素
ParseResult(scheme='http', netloc='user:pwd@domain:80', path='/path', params='params', query='query=queryarg', fragment='fragment')

ParseResult 继承于 namedtuple,因此可以同时通过索引和命名属性来获取 URL 中各部分的值。

为了方便起见, ParseResult 还提供了 usernamepasswordhostnameportnetloc 进一步进行拆分。

print('scheme  :', parsed_result.scheme)
print('netloc  :', parsed_result.netloc)
print('path    :', parsed_result.path)
print('params  :', parsed_result.params)
print('query   :', parsed_result.query)
print('fragment:', parsed_result.fragment)
print('username:', parsed_result.username)
print('password:', parsed_result.password)
print('hostname:', parsed_result.hostname)
print('port    :', parsed_result.port)

结果为:

scheme  : http
netloc  : user:pwd@domain:80
path    : /path
params  : params
query   : query=queryarg
fragment: fragment
username: user
password: pwd
hostname: domain
port    : 80

除了 urlparse() 之外,还有一个类似的 urlsplit() 函数也能对 URL 进行拆分,所不同的是, urlsplit() 并不会把 路径参数(params)路径(path) 中分离出来。

当 URL 中路径部分包含多个参数时,使用 urlparse() 解析是有问题的:

url='http://user:pwd@domain:80/path1;params1/path2;params2?query=queryarg#fragment'

parsed_result=urlparse(url)

print(parsed_result)
print('parsed.path    :', parsed_result.path)
print('parsed.params  :', parsed_result.params)

结果为:

ParseResult(scheme='http', netloc='user:pwd@domain:80', path='/path1;params1/path2', params='params2', query='query=queryarg', fragment='fragment')
parsed.path    : /path1;params1/path2
parsed.params  : params2

这时可以使用 urlsplit() 来解析:

from urllib.parse import urlsplit
split_result=urlsplit(url)

print(split_result)
print('split.path    :', split_result.path)
# SplitResult 没有 params 属性

结果为:

SplitResult(scheme='http', netloc='user:pwd@domain:80', path='/path1;params1/path2;params2', query='query=queryarg', fragment='fragment')
split.path    : /path1;params1/path2;params2

若只是要将 URL 后的 fragment 标识拆分出来,可以使用 urldefrag() 函数:

from urllib.parse import urldefrag

url = 'http://user:pwd@domain:80/path1;params1/path2;params2?query=queryarg#fragment'

d = urldefrag(url)
print(d)
print('url     :', d.url)
print('fragment:', d.fragment)

结果为:

DefragResult(url='http://user:pwd@domain:80/path1;params1/path2;params2?query=queryarg', fragment='fragment')
url     : http://user:pwd@domain:80/path1;params1/path2;params2?query=queryarg
fragment: fragment

组建URL

ParsedResult 对象和 SplitResult 对象都有一个 geturl() 方法,可以返回一个完整的 URL 字符串。

print(parsed_result.geturl())
print(split_result.geturl())

结果为:

http://user:pwd@domain:80/path1;params1/path2;params2?query=queryarg#fragment
http://user:pwd@domain:80/path1;params1/path2;params2?query=queryarg#fragment

但是 geturl() 只在 ParsedResultSplitResult 对象中有,若想将一个普通的元组组成 URL,则需要使用 urlunparse() 函数:

from urllib.parse import urlunparse
url_compos = ('http', 'user:pwd@domain:80', '/path1;params1/path2', 'params2', 'query=queryarg', 'fragment')
print(urlunparse(url_compos))

结果为:

http://user:pwd@domain:80/path1;params1/path2;params2?query=queryarg#fragment

相对路径转换绝对路径

除此之外,urllib.parse 还提供了一个 urljoin() 函数,来将相对路径转换成绝对路径的 URL。

from urllib.parse import urljoin

print(urljoin('http://www.example.com/path/file.html', 'anotherfile.html'))
print(urljoin('http://www.example.com/path/', 'anotherfile.html'))
print(urljoin('http://www.example.com/path/file.html', '../anotherfile.html'))
print(urljoin('http://www.example.com/path/file.html', '/anotherfile.html'))

结果为:

http://www.example.com/path/anotherfile.html
http://www.example.com/path/anotherfile.html
http://www.example.com/anotherfile.html
http://www.example.com/anotherfile.html

查询参数的构造和解析

使用 urlencode() 函数可以将一个 dict 转换成合法的查询参数:

from urllib.parse import urlencode

query_args = {
    'name': 'dark sun',
    'country': '中国'
}

query_args = urlencode(query_args)
print(query_args)

结果为:

name=dark+sun&country=%E4%B8%AD%E5%9B%BD

可以看到特殊字符也被正确地转义了。

相对的,可以使用 parse_qs() 来将查询参数解析成 dict。

from urllib.parse import parse_qs
print(parse_qs(query_args))

结果为:

{'name': ['dark sun'], 'country': ['中国']}

如果只是希望对特殊字符进行转义,那么可以使用 quotequote_plus 函数,其中 quote_plusquote 更激进一些,会把 :/ 一类的符号也给转义了。

from urllib.parse import quote, quote_plus, urlencode

url = 'http://localhost:1080/~hello!/'
print('urlencode :', urlencode({'url': url}))
print('quote     :', quote(url))
print('quote_plus:', quote_plus(url))

结果为:

urlencode : url=http%3A%2F%2Flocalhost%3A1080%2F%7Ehello%21%2F
quote     : http%3A//localhost%3A1080/%7Ehello%21/
quote_plus: http%3A%2F%2Flocalhost%3A1080%2F%7Ehello%21%2F

可以看到 urlencode 中应该是调用 quote_plus 来进行转义的。

逆向操作则使用 unquoteunquote_plus 函数:

from urllib.parse import unquote, unquote_plus

encoded_url = 'http%3A%2F%2Flocalhost%3A1080%2F%7Ehello%21%2F'
print(unquote(encoded_url))
print(unquote_plus(encoded_url))

结果为:

http://localhost:1080/~hello!/
http://localhost:1080/~hello!/

你会发现 unquote 函数居然能正确地将 quote_plus 的结果转换回来。