用命令行做数据科学(中)

97 阅读4分钟

5. 数据清洗(Scrubbing Data)

在前一篇文章中介绍了如何从不同数据源获取数据,包括网络、数据库和文件等。获取到的数据首先要进行格式转换,如从JSON、HTML或者XML转为CSV等我们期望的格式。转换后的数据也可能存在其它到问题,如字段丢失、不一致、字符错误等,需要进行过滤,替换字段,多个文件进行组合等。有很多命令行工具可以完成这些操作,如grep、awk以及pup和jq等。

这节包括如下几部分内容:

  • 数据格式转换
  • 过滤
  • 抽取、替换数据
  • 分割、合并和抽取列
  • 多文件组合

数据转换

创建一个python脚本,如下

$ bat fizzbuzz.py
───────┬────────────────────────────────────────────────────────────────────────
       │ File: fizzbuzz.py
───────┼────────────────────────────────────────────────────────────────────────
   1#!/usr/bin/env python
   2import sys
   34   │ CYCLE_OF_15 = ["fizzbuzz", None, None, "fizz", None,
   5"buzz", "fizz", None, None, "fizz",
   6"buzz", None, "fizz", None, None]
   78def fizz_buzz(n: int) -> str:
   9return CYCLE_OF_15[n % 15] or str(n)
  1011if __name__ == "__main__":
  12try:
  13while (n:= sys.stdin.readline()):
  14print(fizz_buzz(int(n)))
  15except:
  16pass
───────┴──────

该脚本的作用是根据输入的一个整数值返回一个字符串。

执行下面的命令生成100个字符串并保存到文件fb.seq中,\

$ seq 100 |
> /data/ch04/fizzbuzz.py | ➊
> tee fb.seq | trim
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
… with 90 more lines

查找文件fb.seq中字符串fizz和buzz到出现频率,并保存到fb.cnt中,\

$ grep -E "fizz|buzz" fb.seq | ➊
> sort | uniq -c | sort -nr > fb.cnt ➋
 
$ bat -A fb.cnt
───────┬────────────────────────────────────────────────────────────────────────
       │ File: fb.cnt
───────┼────────────────────────────────────────────────────────────────────────
   1   │ ·····27·fizz␊
   2   │ ·····14·buzz␊
   3   │ ······6·fizzbuzz␊
───────┴────────────────────────────────────────────────────────────────────────

➊ 正则表达式匹配,包括fizzbuzz

sort出现了两次,第一次出现是因为uniq要求输入的字符串是排好序的。第二个sort的作用是按频次字段降序排序。

排序后的数据通过awk转换为csv文件,下面的命令行添加了一个header,并将两列数据顺序进行了交换

$ < fb.cnt awk 'BEGIN { print "value,count" } { print $2","$1 }' > fb.csv
 
$ bat fb.csv
───────┬────────────────────────────────────────────────────────────────────────
       │ File: fb.csv
───────┼────────────────────────────────────────────────────────────────────────
   1   │ value,count
   2   │ fizz,27
   3   │ buzz,14
   4   │ fizzbuzz,6
───────┴────────────────────────────────────────────────────────────────────────
 
$ csvlook fb.csv
│ value    │ count │
├──────────┼───────┤
│ fizz     │    27 │
│ buzz     │    14 │
│ fizzbuzz │     6

然后调用rush命令进行显示,rush要求输入的数据是csv格式

$ rush plot -x value -y count --geom col --height 2 fb.csv > fb.png
 
$ display fb.png

Figure 5.1: Counting fizz, buzz, and fizzbuzz

按行处理文本

按行处理文本是最简单的方式。
生成一个文本文件

$ seq -f "Line %g" 10 | tee lines
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10

用三种命令去显示文件的头3行

$ < lines head -n 3
Line 1
Line 2
Line 3
 
$ < lines sed -n '1,3p'
Line 1
Line 2
Line 3
 
$ < lines awk 'NR <= 3' ➊
Line 1
Line 2
Line 3

显示最后3行

$ < lines tail -n 3
Line 8
Line 9
Line 10

删除前3行

$ < lines tail -n +4
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
 
$ < lines sed '1,3d'
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10
 
$ < lines sed -n '1,3!p'
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10

或者

$ < lines head -n -3
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7

显示4、5、6行

$ < lines sed -n '4,6p'
Line 4
Line 5
Line 6
 
$ < lines awk '(NR>=4) && (NR<=6)'
Line 4
Line 5
Line 6
 
$ < lines head -n 6 | tail -n 3
Line 4
Line 5
Line 6

显示奇数行

$ < lines sed -n '1~2p'
Line 1
Line 3
Line 5
Line 7
Line 9
 
$ < lines awk 'NR%2'
Line 1
Line 3
Line 5
Line 7
Line 9

显示偶数行

$ < lines sed -n '0~2p'
Line 2
Line 4
Line 6
Line 8
Line 10
 
$ < lines awk '(NR+1)%2'
Line 2
Line 4
Line 6
Line 8
Line 10

按模式处理行

根据文本的内容来处理,如只显示含有chapter字符串的行

$ < alice.txt grep -i chapter ➊
CHAPTER I. Down the Rabbit-Hole
CHAPTER II. The Pool of Tears
CHAPTER III. A Caucus-Race and a Long Tale
CHAPTER IV. The Rabbit Sends in a Little Bill
CHAPTER V. Advice from a Caterpillar
CHAPTER VI. Pig and Pepper
CHAPTER VII. A Mad Tea-Party
CHAPTER VIII. The Queen's Croquet-Ground
CHAPTER IX. The Mock Turtle's Story
CHAPTER X. The Lobster Quadrille
CHAPTER XI. Who Stole the Tarts?
CHAPTER XII. Alice's Evidence

➊ 参数 -i 到作用是不区分大小写

用正则表示匹配文本内容,下面的命令是批评以“CHAPTER xxx. The”开头的行,这里xxx是任意长度的字符串,

$ < alice.txt grep -E '^CHAPTER (.*). The'
CHAPTER II. The Pool of Tears
CHAPTER IV. The Rabbit Sends in a Little Bill
CHAPTER VIII. The Queen's Croquet-Ground
CHAPTER IX. The Mock Turtle's Story
CHAPTER X. The Lobster Quadrille

也可以用-v参数进行反向选择,下面的命令统计非空行数

$ < alice.txt grep -Ev '^\s$' | wc -l

随机选择数据

可以用sample数据选择数据的一个子集,如下

$ seq -f "Line %g" 1000 | sample -r 1%
Line 31
Line 360
Line 402
Line 526
Line 553
Line 625
Line 856

这里按1%的概率选择,即每一行都有1%的概率被选中。

sample命令还可以用于调试命令行,如下面这条命令,没行之间增加1秒延迟,并只执行5秒

$ seq -f "Line %g" 1000 | sample -r 1% -d 1000 -s 5 | ts ➊
Jan 31 10:35:52 Line 7
Jan 31 10:35:53 Line 96
Jan 31 10:35:54 Line 108
Jan 31 10:35:55 Line 171
Jan 31 10:35:56 Line 178
Jan 31 10:35:57 Line 183

➊ ts的作用是在每行之前增加一个时间戳

数据抽取

在之前的例子中,我们要想抽出每个章节的标题,可以用grep和cut结合来实现

$ grep -i chapter alice.txt | cut -d ' ' -f 3-
Down the Rabbit-Hole
The Pool of Tears
A Caucus-Race and a Long Tale
The Rabbit Sends in a Little Bill
Advice from a Caterpillar
Pig and Pepper
A Mad Tea-Party
The Queen's Croquet-Ground
The Mock Turtle's Story
The Lobster Quadrille
Who Stole the Tarts?
Alice's Evidence

cut命令将grep输出的每一行用空格分割,并将分割后第三个字段及之后的字段打印出来。

分割后的字段可能每行不一样,用sed命令实现该功能会更复杂一些

$ sed -rn 's/^CHAPTER ([IVXLCDM]{1,}). (.*)$/\2/p' alice.txt | trim 3
Down the Rabbit-Hole
The Pool of Tears
A Caucus-Race and a Long Tale
… with 9 more lines

cut还可以按字符的位置进行分割

$ grep -i chapter alice.txt | cut -c 9-
I. Down the Rabbit-Hole
II. The Pool of Tears
III. A Caucus-Race and a Long Tale
IV. The Rabbit Sends in a Little Bill
V. Advice from a Caterpillar

grep的-o参数可以让每个匹配单独输出到一行

$ < alice.txt grep -oE '\w{2,}' | trim
Project
Gutenberg
Alice
Adventures
in
Wonderland
by
Lewis
Carroll
This
… with 28615 more lines

匹配所有a开头、e结尾的字符串

$ < alice.txt tr '[:upper:]' '[:lower:]' | ➊
> grep -oE '\w{2,}' |
> grep -E '^a.*e$' |
> sort | uniq | sort -nr | trim
available
ate
assistance
askance
arise
argue
are
archive
applicable
apple
… with 25 more lines

tr的作用是将大写转换为小写

替换和删除

tr命令代表的是 translate, to replace or delete ,用于对单个字符串的转换、替换和删除等,如将空格替换为下划线

$ echo 'hello world!' | tr ' ' '_'
hello_world!

替换多个字符

$ echo 'hello world!' | tr ' !' '_?'
hello_world?

使用-d参数来删除字符

$ echo 'hello world!' | tr -d ' !'
helloworld
 
$ echo 'hello world!' | tr -d -c '[a-z]'
helloworld%

第二个命令中-c [a-z]的作用是,保留小写字母,删除其它字符。

大小写转换

$ echo 'hello world!' | tr '[a-z]' '[A-Z]'
HELLO WORLD!
 
$ echo 'hello world!' | tr '[:lower:]' '[:upper:]'
HELLO WORLD!

tr只能处理ASCII字符,非ASCII字符用sed进行处理。

$ echo 'helló világ' | sed 's/[[:lower:]]*/\U&/g'
HELLÓ VILÁG

如果不是处理单个字符,而是字符串,使用sed更方便

$ echo ' hello     world!' |
> sed -re 's/hello/bye/' | ➊
> sed -re 's/\s+/ /g' | ➋
> sed -re 's/\s+//'
bye world!

➊ 将 hello 替换为 bye.
➋ 将任意空格替换为单个空格. g 表示全局替换,即一行中的任意位置都要进行替换.
➌ 没有指定 g ,所以只删除行首的空格.

简化写法

$ echo ' hello     world!' |
> sed -re 's/hello/bye/;s/\s+/ /g;s/\s+//'
bye world!

处理CSV文件

CSV文件包含文件头、数据体和列等,所以不能直接使用上面的方法处理。

body命令

这个命令的作用是读取CSV文件的body部分,其工作原理如下:

  • 读取第一行,保存到 $header变量;
  • 打印header
  • 将剩下的数据传给body后的命令
$ echo -e "value\n7\n2\n5\n3" | body sort -n
value
2
3
5
7

header命令

添加一个header,

$ seq 5 | header -a count
count
1
2
3
4
5

显示header

$ < tips.csv header
bill,tip,sex,smoker,day,time,size

去掉header

$ < iris.csv header -d | trim
5.1,3.5,1.4,0.2,Iris-setosa
4.9,3.0,1.4,0.2,Iris-setosa
4.7,3.2,1.3,0.2,Iris-setosa
4.6,3.1,1.5,0.2,Iris-setosa
5.0,3.6,1.4,0.2,Iris-setosa
5.4,3.9,1.7,0.4,Iris-setosa
4.6,3.4,1.4,0.3,Iris-setosa
5.0,3.4,1.5,0.2,Iris-setosa
4.4,2.9,1.4,0.2,Iris-setosa
4.9,3.1,1.5,0.1,Iris-setosa
… with 140 more lines

类似于tail -n +2。

替换header

$ seq 5 | header -a line | body wc -l | header -r count
count
5

对header进行大小写转换

$ seq 5 | header -a line | header -e "tr '[a-z]' '[A-Z]'"
LINE
1
2
3
4
5

cols命令

cols命令用于处理列,下面的命令将day这一列转换为大写,

$ < tips.csv cols -c day body "tr '[a-z]' '[A-Z]'" | head -n 5 | csvlook
│        day │  bill │  tip │ sex    │ smoker │ time   │ size │
├────────────┼───────┼──────┼────────┼────────┼────────┼──────┤
│ 0001-01-0716.991.01 │ Female │  False │ Dinner │    2 │
│ 0001-01-0710.341.66 │ Male   │  False │ Dinner │    3 │
│ 0001-01-0721.013.50 │ Male   │  False │ Dinner │    3 │
│ 0001-01-0723.683.31 │ Male   │  False │ Dinner │    2

在CSV上执行SQL

csvsql可以对csv文件直接执行sql操作,如下

$ seq 5 | header -a val | csvsql --query "SELECT SUM(val) AS sum FROM stdin"
sum
15.0

通过pipe方式传给csvsql的文件流,在sql中的table名是stdin。

列的抽取和重排

csvcut用于完成对列的抽取和重排,如只保留数字列

$ < iris.csv csvcut -c sepal_length,petal_length,sepal_width,petal_width | csvlo
ok
│ sepal_length │ petal_length │ sepal_width │ petal_width │
├──────────────┼──────────────┼─────────────┼─────────────┤
│          5.11.43.50.2 │
│          4.91.43.00.2 │
│          4.71.33.20.2 │
│          4.61.53.10.2 │
│          5.01.43.60.2 │
│          5.41.73.90.4 │
│          4.61.43.40.3 │
│          5.01.53.40.2 │
… with 142 more lines

通过-C参数只保留特定的列,如

$ < iris.csv csvcut -C species | csvlook
│ sepal_length │ sepal_width │ petal_length │ petal_width │
├──────────────┼─────────────┼──────────────┼─────────────┤
│          5.13.51.40.2 │
│          4.93.01.40.2 │
│          4.73.21.30.2 │
│          4.63.11.50.2 │
│          5.03.61.40.2 │
│          5.43.91.70.4 │
│          4.63.41.40.3 │
│          5.03.41.50.2 │
… with 142 more lines

或者按序号保留列

$ echo 'a,b,c,d,e,f,g,h,i\n1,2,3,4,5,6,7,8,9' |
> csvcut -c $(seq 1 2 9 | paste -sd,)
a,c,e,g,i
1,3,5,7,9

用cut也可以实现相同的功能,前提是要确保列的值中不包括逗号,

$ echo 'a,b,c,d,e,f,g,h,i\n1,2,3,4,5,6,7,8,9' | cut -d, -f 5,1,3
a,c,e
1,3,5

处理行

和普通文本处理行的方式不同的是,CSV文件处理行时可以对特定的列进行操作。

将size列小于5的行过滤掉

$ csvgrep -c size -i -r "[1-4]" tips.csv
bill,tip,sex,smoker,day,time,size
29.8,4.2,Female,No,Thur,Lunch,6
34.3,6.7,Male,No,Thur,Lunch,6
41.19,5.0,Male,No,Thur,Lunch,5
27.05,5.0,Female,No,Thur,Lunch,6
29.85,5.14,Female,No,Sun,Dinner,5
48.17,5.0,Male,No,Sun,Dinner,6
20.69,5.0,Male,No,Sun,Dinner,5
30.46,2.0,Male,Yes,Sun,Dinner,5
28.15,3.0,Male,Yes,Sat,Dinner,5

用awk也可以对列进行类似操作,如获取 bill 大于40且在 Saturday 或者 Sunday 的账单,

$ < tips.csv awk -F, 'NR==1 || ($1 > 40.0) && ($5 ~ /^S/)'
bill,tip,sex,smoker,day,time,size
48.27,6.73,Male,No,Sat,Dinner,4
44.3,2.5,Female,Yes,Sat,Dinner,3
48.17,5.0,Male,No,Sun,Dinner,6
50.81,10.0,Male,Yes,Sat,Dinner,3
45.35,3.5,Male,Yes,Sun,Dinner,3
40.55,3.0,Male,Yes,Sun,Dinner,2
48.33,9.0,Male,No,Sat,Dinner,4

csvsql对列操作的可读性更高,因为它使用的是列名而不是列的序号,如下

$ csvsql --query "SELECT * FROM tips WHERE bill > 40 AND day LIKE 'S%'" tips.csv

bill,tip,sex,smoker,day,time,size
48.27,6.73,Male,0,Sat,Dinner,4.0
44.3,2.5,Female,1,Sat,Dinner,3.0
48.17,5.0,Male,0,Sun,Dinner,6.0
50.81,10.0,Male,1,Sat,Dinner,3.0
45.35,3.5,Male,1,Sun,Dinner,3.0
40.55,3.0,Male,1,Sun,Dinner,2.0
48.33,9.0,Male,0,Sat,Dinner,4.0

合并列

如果一个字段的值分散在多个列中,比如人名、日期等,就需要将其进行合并后再处理。

比如,有下面这个csv文件,

$ csvlook -I names.csv
 id  last_name  first_name  born 
├────┼───────────┼────────────┼──────┤
 1   Williams   John        1932 
 2   Elfman     Danny       1953 
 3   Horner     James       1953 
 4   Shore      Howard      1946 
 5   Zimmer     Hans        1957 

通过 sed 命令将 last_name 和 first_name 进行合并

$ < names.csv sed -re '1s/.*/id,full_name,born/g;2,$s/(.*),(.*),(.*),(.*)/\1,\3
\2,\4/g' |
> csvlook -I
 id  full_name      born 
├────┼───────────────┼──────┤
 1   John Williams  1932 
 2   Danny Elfman   1953 
 3   James Horner   1953 
 4   Howard Shore   1946 
 5   Hans Zimmer    1957 

使用 awk 完成相同操作

$ < names.csv awk -F, 'BEGIN{OFS=","; print "id,full_name,born"} {if(NR > 1) {pr
int $1,$3" "$2,$4}}' |
> csvlook -I
 id  full_name      born 
├────┼───────────────┼──────┤
 1   John Williams  1932 
 2   Danny Elfman   1953 
 3   James Horner   1953 
 4   Howard Shore   1946 
 5   Hans Zimmer    1957 

使用 cols 命令

$ < names.csv |
> cols -c first_name,last_name tr "," " " |
> header -r full_name,id,born |
> csvcut -c id,full_name,born |
> csvlook -I
│ id │ full_name     │ born │
├────┼───────────────┼──────┤
│ 1  │ John Williams │ 1932 │
│ 2  │ Danny Elfman  │ 1953 │
│ 3  │ James Horner  │ 1953 │
│ 4  │ Howard Shore  │ 1946 │
│ 5  │ Hans Zimmer   │ 1957 │

使用csvsql命令

$ < names.csv csvsql --query "SELECT id, first_name || ' ' || last_name "\
> "AS full_name, born FROM stdin" | csvlook -I
 id   full_name      born   
├─────┼───────────────┼────────┤
 1.0  John Williams  1932.0 
 2.0  Danny Elfman   1953.0 
 3.0  James Horner   1953.0 
 4.0  Howard Shore   1946.0 
 5.0  Hans Zimmer    1957.0 

如果名字中含有逗号,那么前面几个命令都会失效,只有最后这个csvsql可以正常工作。

组合多个文件

生成三个文件

$ < tips.csv csvcut -c bill,tip | tee bills.csv | head -n 3 | csvlook
│  bill │  tip │
├───────┼──────┤
│ 16.99 │ 1.01 │
│ 10.34 │ 1.66 │
 
$ < tips.csv csvcut -c day,time | tee datetime.csv |
> head -n 3 | csvlook -I
│ day │ time   │
├─────┼────────┤
│ Sun │ Dinner │
│ Sun │ Dinner │
 
$ < tips.csv csvcut -c sex,smoker,size | tee customers.csv |
> head -n 3 | csvlook
│ sex    │ smoker │ size │
├────────┼────────┼──────┤
│ Female │  False │    2 │
│ Male   │  False │    3 │

将三个文件组合到一起

$ paste -d, {bills,customers,datetime}.csv | head -n 3 | csvlook -I
│ bill  │ tip  │ sex    │ smoker │ size │ day │ time   │
├───────┼──────┼────────┼────────┼──────┼─────┼────────┤
│ 16.99 │ 1.01 │ Female │ No     │ 2    │ Sun │ Dinner │
│ 10.34 │ 1.66 │ Male   │ No     │ 3    │ Sun │ Dinner │

-d参数指定用逗号最为分割符。

联接(Joining)

将两个文件联合到一起的时候,有时候不能简单的结合,类似上面那个例子,而是类似数据库表的结合,根据某个字段来连接。
irismeta.csv文件和iris.csv文件有一个共同的列species,

$ csvlook irismeta.csv
│ species         │ wikipedia_url                                │ usda_id │
├─────────────────┼──────────────────────────────────────────────┼─────────┤
│ Iris-versicolor │ http://en.wikipedia.org/wiki/Iris_versicolor │ IRVE2   │
│ Iris-virginica  │ http://en.wikipedia.org/wiki/Iris_virginica  │ IRVI    │
│ Iris-setosa     │                                              │ IRSE    │

通过csvjoin将两个文件结合到一起

$ csvjoin -c species iris.csv irismeta.csv | csvcut -c sepal_length,sepal_width,
species,usda_id | sed -n '1p;49,54p' | csvlook
│ sepal_length │ sepal_width │ species         │ usda_id │
├──────────────┼─────────────┼─────────────────┼─────────┤
│          4.63.2 │ Iris-setosa     │ IRSE    │
│          5.33.7 │ Iris-setosa     │ IRSE    │
│          5.03.3 │ Iris-setosa     │ IRSE    │
│          7.03.2 │ Iris-versicolor │ IRVE2   │
│          6.43.2 │ Iris-versicolor │ IRVE2   │
│          6.93.1 │ Iris-versicolor │ IRVE2   │

也可以用csvsql完成

$ csvsql --query 'SELECT i.sepal_length, i.sepal_width, i.species, m.usda_id FRO
M iris i JOIN irismeta m ON (i.species = m.species)' iris.csv irismeta.csv | sed
 -n '1p;49,54p' | csvlook
│ sepal_length │ sepal_width │ species         │ usda_id │
├──────────────┼─────────────┼─────────────────┼─────────┤
│          4.63.2 │ Iris-setosa     │ IRSE    │
│          5.33.7 │ Iris-setosa     │ IRSE    │
│          5.03.3 │ Iris-setosa     │ IRSE    │
│          7.03.2 │ Iris-versicolor │ IRVE2   │
│          6.43.2 │ Iris-versicolor │ IRVE2   │
│          6.93.1 │ Iris-versicolor │ IRVE2   │

处理HTML/XML和JSON文件

获取一个html文件

$ curl -sL 'http://en.wikipedia.org/wiki/List_of_countries_and_territories_by_bo
rder/area_ratio' > wiki.html

该html文件中有个table

$ grep wikitable -A 21 wiki.html
<style data-mw-deduplicate="TemplateStyles:r1049062077">.mw-parser-output table.
static-row-numbers{counter-reset:rowNumber}body.skin-minerva .mw-parser-output .
static-row-numbers2.sortable{counter-reset:rowNumber -1}body.skin-minerva .mw-pa
rser-output .static-row-numbers2.sortable.static-row-header-two{counter-reset:ro
wNumber -2}.mw-parser-output table.static-row-numbers tr::before{display:table-c
ell;padding:0 0.5em;text-align:right}.mw-parser-output table.static-row-numbers
tr::before{content:""}body:not(.skin-minerva) .mw-parser-output .static-row-numb

用 pup 命令抽取这个table

$ < wiki.html pup 'table.wikitable tbody' | tee table.html | trim
<tbody>
 <tr>
  <th>
   Country or territory
  </th>
  <th>
   Total length of land borders (km)
  </th>
  <th>
   Total surface area (km
… with 3458 more lines

pup 命令后面跟的参数是一个CSS选择器。用xml2json将html文件转换为json格式

$ < table.html xml2json > table.json
 
$ jq . table.json | trim 20
{
  "tbody": {
    "tr": [
      {
        "th": [
          {
            "$t": "Country or territory"
          },
          {
            "$t": "Total length of land borders (km)"
          },
          {
            "$t": [
              "Total surface area (km",
              ")"
            ],
            "sup": {
              "$t": "2"
            }
          },
… with 3950 more lines

jq 是一个非常强大的 json 数据处理工具,用它将json数据抽取并转为 csv 文件

$ < table.json jq -r '.tbody.tr[1:][] | [.td[]["$t"]] | @csv' | header -a rank,c
ountry,border,surface,ratio > countries.csv
$ csvlook --max-column-width 28 countries.csv
│ rank                         │   country │        border │ surface │ ratio │
├──────────────────────────────┼───────────┼───────────────┼─────────┼───────┤
│ Vatican City                 │      3.200.447.273… │       │
│ Monaco                       │      4.402.002.200… │       │
│ San Marino                   │     39.0061.000.639… │       │
│ Liechtenstein                │     76.00160.000.465… │       │
│ Sint Maarten  (Netherlands)  │     10.2034.000.300… │       │
│ Andorra                      │    120.30468.000.257… │       │
│ Gibraltar  (United Kingdom)  │      1.206.000.200… │       │
│ Saint Martin (France)        │     10.2054.000.189… │       │
… with 238 more lines

(待续)

原文 mp.weixin.qq.com/s?__biz=MzI…