V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
codehole
V2EX  ›  程序员

相见恨晚的 Shell 文本处理诀窍

  •  5
     
  •   codehole ·
    pyloque · 2018-04-02 09:44:33 +08:00 · 9495 次点击
    这是一个创建于 2429 天前的主题,其中的信息可能已经有所发展或是发生改变。

    小编编程资质一般,刚出道的时候使用的是 windows 来做程序开发,平时 linux 命令的知识仅限于在学校里玩 ubuntu 的时候学到的那丁点。在一次偶然看见项目的主程敲着复杂的 shell 单行命令来处理日志的时候感到惊讶不已。后来自己自学了一点 shell 编程,刚看完一本书没过多久就忘记了,因为工作中用到的实在太少,而且命令如此之多,学了一个忘了另一个,始终摸不着门道在哪。

    直到某天灵感爆发,发现了一个窍门之后,才牢牢地把握住了 shell 指令的精髓。

    用写 SQL 查询的思维写 shell 命令

    写 SQL 小编非常在行,毕业第一年的时候 SQL 就写的行云流水。经常别人写了一个存储过程来干某件事的时候,哥用一条语句搞定。自然这样的语句也是被不少人吐槽的,难以看懂。

    偶然一天我将一个数据表导入成一个 CSV 文件的时候发现了这个窍门。如果把这个 CSV 文件看成一个数据表,把各种 shell 指令看成 SQL 的查询条件,这两种数据处理方式在思维模式上就没有什么区别了。

    然后就开始仔细研究了一番,又有了好多惊人的发现。原来 shell 指令除了查询之外还可以做修改,相当于 SQL 的 DML 操作。shell 指令除了能做单表数据处理之外还可以实现类似于 SQL 多表的 JOIN 操作。连排序和聚合功能也能轻松搞定。

    首先下载本章用到的数据,该数据有 20 多 M,建议耐心等待。

    git clone https://github.com/pyloque/shellquery_ppt.git
    

    第一个文件 groups.txt 表示小组,有三个字段,分别是小组 ID、小组名称和小组创建时间

    第二个文件 rank_items.txt 代表行为积分。字段分别是行为唯一 ID、行为类型、行为关联资源 ID、行为时间和行为积分。行为类型包含 group 单词的是和小组相关的积分行为。其它行为还有与帖子、用户、问题、文章相关的。

    文本文件等价于数据表 table

    数据表是有模式的数据,每个列都有特定的含义。表的模式信息可以在数据库的元表里找到。

    CSV 文本文件也是有模式的数据,只不过它的列信息只存在于用户的大脑里。文件里只有纯粹的数据和数据分隔符。CSV 文本文件的记录之间使用换行符分割,列之间使用制表符或者逗号等符号进行分隔。

    数据表的行记录等价于 CSV 文本文件的一行数据。数据表一行的列数据可以使用名称指代,但是 CSV 行的列数据只能用位置索引,表达能力上相比要差一截。

    在测试阶段,我们使用少量行的数据进行测试,这个时候可以使用 head 指令只吐出 CSV 文本文件的前 N 行数据,它相当于 SQL 的 limit 条件。同样也可以使用 tail 指令吐出文件的倒数前 N 行数据。使用 cat 指令吐出所有。

    # 看前 5 行
    bash> head -n 5 groups.txt
    205;"真要瘦不瘦不罢休";"2012-11-23 13:42:38+08"
    28;"健康朝九晚五";"2010-10-20 16:20:43+08"
    280;"核谐家园";"2013-04-17 17:11:49.545351+08"
    38;"创意科技";"2010-10-20 16:20:44+08"
    39;"死理性派";"2010-10-20 16:20:44+08"
    
    # 看倒数 5 行
    bash> tail -n 5 groups.txt
    69;"吃货研究所";"2010-11-10 14:35:34+08"
    27;"DIY";"2010-10-20 16:20:43+08"
    33;"心事鉴定组";"2010-10-20 16:20:44+08"
    275;"盗梦空间";"2013-03-21 23:35:39.249583+08"
    197;"万有青年养成计划";"2012-11-14 11:39:50+08"
    
    # 显示所有
    bash> cat groups.txt
    ...
    

    数据过滤等价于查询条件 where

    数据过滤一般会使用 grep 或者 awk 指令。grep 用来将整个行作为文本来进行搜索,保留满足指定文本条件的行,或者是保留不满足匹配条件的行。awk 可以用来对指定列内容进行文本匹配或者是数字匹配。

    # 显示包含‘技术’单词的行
    bash> cat groups.txt | grep 技术
    73;"美丽也是技术活";"2010-11-10 15:08:59+08"
    279;"灰机与航空技术";"2013-04-12 13:30:31.617491+08"
    243;"科学技术史";"2013-01-24 12:48:44.06041+08"
    
    # 显示即包含单词‘技术’又包含‘灰机’的行
    bash> cat groups.txt | grep 技术 | grep 灰机
    279;"灰机与航空技术";"2013-04-12 13:30:31.617491+08"
    
    # 显示小组 ID 小于 30 的行 -F 限定分隔符 后面是一个 awk 脚本
    # awk 一门简单的编程语言,它处理的对象是以行为单位
    # $0 表示整行内容 $1 代表第一列内容
    # awk 分 4 段,选择端|起始段|处理段|结束段
    # filter BEGIN{} {} END{}
    # 选择端起到过滤行的作用,选择成功的行进入处理段
    # 起始端在第一个行处理之前进行,结束段在最后一个行处理完成之后进行,只进行依次
    # 处理段就是对选择成功的行依次处理,依次处理一行
    # 这些段都是可选的
    # 参考 awk 简明教程 https://coolshell.cn/articles/9070.html
    bash> cat groups.txt | awk -F';' '$1<30  {print $0}'
    28;"健康朝九晚五";"2010-10-20 16:20:43+08"
    29;"爱宠";"2010-10-20 16:20:44+08"
    27;"DIY";"2010-10-20 16:20:43+08"
    

    限定字段输出

    我们经常使用列名称来限定 SQL 的输出对象。

    SQL> select id, user from group
    同样对于文本文件,我们可以使用 cut 指令或者 awk 来完成。
    
    # 只显示前 3 行的第一列和第二列,保留分隔符 -d 指明分隔符
    bash> cat groups.txt | head -n 3 | cut -d';' -f1 -f2
    205;"真要瘦不瘦不罢休"
    28;"健康朝九晚五"
    280;"核谐家园"
    # 只显示前 3 行的第一列和第二列,用空格作为分隔符
    bash> cat groups.txt | head -n 3 | awk -F';' '{print $1" "$2}'
    205 "真要瘦不瘦不罢休"
    28 "健康朝九晚五"
    280 "核谐家园"
    

    聚合

    数据聚合也是 shell 里经常使用到的命令,最常用的可能就是用 wl 来统计行数,其实也可以使用 awk 来完成更加复杂的统计功能。

    # 总共多少行
    bash> cat groups.txt | wc -l
    216
    # 用 awk 实现,遇到一行对变量 l 加 1,最后输出 l 变量的值,也即行数
    bash> cat groups.txt | awk '{l+=1} END{print l}'
    awk 还可以完成类似于 group by 的功能,这个脚本就要复杂一点
    
    # 因为命令太长,下面用了 shell 命令续行符"\"
    # 统计每行的名称长度[去掉前后两个引号],将相同长度的进行聚合统计数量
    # awk 不识别 unicode,所以长度都是按字节算的,可以使用 gawk 工具来取代
    # awk 支持字典数据结构和循环控制语句,所以可以干聚合的事
    bash> cat groups.txt | awk -F';' '{print length($2)-2}' | \
        > awk '{g[$1]+=1} END{for (l in g) print l,"=",g[l]}'
    22 = 1
    3 = 2
    4 = 1
    24 = 9
    6 = 6
    ...
    

    排序和去重

    排序命令是一种消耗内存的运算,它需要将全部的内容放置到内存的数组里,然后使用排序算法进行内容排序后输出。shell 的排序就是 sort 命令,sort 可以按字符排序也可以按数字排序。

    # 以分号作为分隔符,排序第一列小组的 ID
    # 默认按字符进行排序
    bash> cat groups.txt | sort -t';' -k1 | head -n 5
    102;"说文解字";"2012-03-19 18:10:47+08"
    103;"广告研发局";"2012-03-21 17:50:02+08"
    104;"掀起你的内幕来";"2012-03-26 17:23:11+08"
    105;"一分钟学堂";"2012-03-28 17:06:37+08"
    106;"泥瓦匠";"2012-04-11 21:30:34+08"
    
    # 加上-n 选项按数字进行排序
    bash> cat groups.txt | sort -t';' -n -k1 | head -n 5
    27;"DIY";"2010-10-20 16:20:43+08"
    28;"健康朝九晚五";"2010-10-20 16:20:43+08"
    29;"爱宠";"2010-10-20 16:20:44+08"
    30;"性 情";"2010-10-20 16:20:44+08"
    31;"谋杀 现场 法医";"2010-10-20 16:20:44+08"
    
    # 加上-r 选项倒排
    bash> cat groups.txt | sort -t';' -n -r -k1 | head -n 5
    303;"怎么玩小组";"2013-06-05 13:18:06.079734+08"
    302;"**精选";"2013-06-05 13:15:52.187787+08"
    301;"土木建筑之家";"2013-06-05 13:14:58.968257+08"
    300;"NBA 那些事儿";"2013-06-03 15:50:14.415515+08"
    299;"数据江湖";"2013-05-30 17:27:10.514241+08"
    

    去重的命令时 uniq,但是跟 SQL 的 distinct 不一样,uniq 一般和 sort 配合使用,它要求去重的对象必须是排过序的,否则就不能起到去重的效果。distinct 一般是在内存里记录一个 Set 放入所有的值,然后查询新值是否在 Set 中。uniq 只记录一个值,就是上一行的值,然后看新行的值是否和上一行的值一样。

    # 打印第二列小组名称的长度的所有可能的值的个数
    # awk 打印长度,sort -n 按长度数字排序, uniq 去重,wc -l 统计个数
    bash> cat groups.txt | awk -F';' '{print length($2)-2}' | sort -n | uniq | wc -l
    21
    
    # 我们再看看,如果不排序会怎样
    bash> cat groups.txt | awk -F';' '{print length($2)-2}' | uniq | wc -l
    166
    
    # 很明显这个值不是我们期望的
    

    组合命令的效率

    一个复杂的单行命令可以有非常多的单条指令组成,每个指令都会对应着一个进程。进程和进程之间使用管道将输入输出串接起来,形如人体蜈蚣。

    第一个进程处理了一行数据后从输出吐了出来,成了第二个进程的输入,在第二个进程对第一行数据进行处理的过程中,第一个进程又可以继续处理后面的行。

    如此就形成了一个流水线结构,每个进程都在并行的进行数据处理。整个组合命令的效率将取决于所有命令中最慢的一条。

    排序操作又不同于其它操作,它需要等待所有的数据都接受完成才能决定第一个输出。所以排序是一个即占用内存又耗费时间的操作,它会导致后续进程的饥饿感。

    进程替换操作符 <()

    有很多指令可以接受一个文件名作为参数,然后对这个文件进行文本处理。如果输入不是文件而是由一串命令生成的动态文件怎么办呢?也许你会想到先将这一串命令输出到临时文件中再将这个临时文件名作为指令的输入,处理完毕后再删除这个临时文件。

    # 首先创建临时文件
    bash> mktemp
    /var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp
    
    # 输出到临时文件
    bash> cat groups.txt | grep 技术 > /var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp
    
    # 处理临时文件,统计临时文件的行数
    bash> cat /var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp | wc -l
    3
    
    # 删除临时文件
    bash> rm /var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp
    

    但是本文的主题是单行 shell 命令。你很难使用单行命令来实现上面提到的临时文件法。这时我们就需要借助于一个高级语法:进程替换。

    # 等价于上面的临时文件法,进程替换符号<()
    bash> cat <(cat groups.txt | grep 技术) | wc -l
    3
    

    进程替换的原理也是临时文件法,只是这里的文件路径是 /dev/fd/<n>。

    连表 Join 操作

    当两个数据表有关联时,可以使用 join 操作进行连表查询。同样 shell 也有特殊的方法可以关联两个文件的内容进行查询,这个命令在 shell 里面也是 join。考虑到性能,join 指令要求两个输入文件的 join 字段必须是排序的。

    # rank_items 表里面的行为类型字段有个值为 hot_group,它表示小组因为活跃而上了热门小组
    # 然后系统给这个小组累积了一个 score,比如
    # hot_group 后面跟的是小组 ID,最后的值 1 表示 score 积分
    bash> cat rank_items.txt | grep hot_group | head -n 5
    "5aa19d6a-3482-4a92-ae20-f26218d8debd";"hot_group";"96";"2013-06-03 21:43:58.62761+08";1
    "6ae0f144-33af-432b-a9af-db51938e8faf";"hot_group";"48";"2013-06-03 21:44:05.050322+08";1
    "55dcb43e-e2c0-43d2-8ed7-dbec6771e7b4";"hot_group";"185";"2013-06-05 18:14:08.406047+08";1
    "98a54f24-fdef-4029-ad79-90055423f5c3";"hot_group";"31";"2013-06-03 21:47:28.476056+08";1
    "4284d4d5-41b9-4dfd-ada9-537332c5cbd6";"hot_group";"63";"2013-06-01 10:07:18.58019+08";1
    
    # 现在我们来聚合一下所有小组的各自积分,然后排序取前 5 名
    # 用 grep 过滤只保留包含 hot_group 的行
    # 筛选字段,只保留小组 ID 和积分字段,因为小组 ID 前后有引号,所以得用 substr 去掉引号
    # 用 awk 的聚合功能累积各小组的积分
    # sort -n -r 按积分数字倒排,再 head -n 5 取前 5 名展示出来
    bash> cat rank_items.txt| grep hot_group | \
    awk -F';' '{print substr($3, 2, length($3)-2)";"$5}' | \
    awk -F';' '{scores[$1]+=$2} END{for(id in scores) print id";"scores[id]}' | \
    sort -t';' -n -r -k2 | head -n 5
    63;5806
    30;4692
    69;4605
    73;3177
    27;2801
    
    # 接下来我们将上面的结果和 groups.txt 文件 join 起来,以显示小组 ID 对应的名称
    # -t 指定分隔符,两个输入分隔符必须一致
    # -1 1 -2 1 表示取第一个输入文件的第一个字段和第二个输入文件的第一个字段来 join
    # -o1.1,1.2,2.2 表示输出第一个输入文件的第一第二字段和第二个输入文件的第二字段
    bash> join -t';' -1 1 -2 1 -o1.1,1.2,2.2 \
    <(sort -t';' -k1 groups.txt) \
    <(cat rank_items.txt| grep hot_group | \
    awk -F';' '{print substr($3, 2, length($3)-2)";"$5}' | \
    awk -F';' '{scores[$1]+=$2} END{for(id in scores) print id";"scores[id]}' | \
    sort -t';' -n -r -k2 | head -n 5)
    63;"Geek 笑点低";5806
    69;"吃货研究所";4605
    73;"美丽也是技术活";3177
    # 我们看到结果只有 3 条,原因是有 30 和 27 两个 ID 在 groups.txt 里面找不到。
    

    推荐资源

    《 Unix Shell 编程》
    《 The AWK programming language 》
    《 Sed & Awk 101 Hacks 》
     GNU Parallel http://www.gnu.org/software/parallel/
    

    阅读相关文章,请关注公众号 [码洞]

    第 1 条附言  ·  2018-04-02 18:58:36 +08:00

    63 条回复    2018-04-03 09:27:45 +08:00
    zhujian198
        1
    zhujian198  
       2018-04-02 09:50:41 +08:00
    收藏了,谢谢分享
    Immortal
        2
    Immortal  
       2018-04-02 10:03:09 +08:00
    感谢分享
    感觉又复习了一遍
    thomas070
        3
    thomas070  
       2018-04-02 10:05:38 +08:00
    感谢分享 思路清晰
    vegito2002
        4
    vegito2002  
       2018-04-02 10:06:56 +08:00 via iPad
    为什么这篇公众号还没推呢
    AllOfMe
        5
    AllOfMe  
       2018-04-02 10:07:12 +08:00
    谢谢!学习了
    tees
        6
    tees  
       2018-04-02 10:09:07 +08:00
    谢谢分享。
    PythonAnswer
        7
    PythonAnswer  
       2018-04-02 10:12:43 +08:00 via Android
    1 用正确的工具干正确的事。

    2 会就行,没必要熟练掌握。
    wackyjazz1
        8
    wackyjazz1  
       2018-04-02 10:14:24 +08:00
    謝謝分享,又重新學習了 Shell
    johnj
        9
    johnj  
       2018-04-02 10:21:46 +08:00
    这个思维好。学习了。谢谢!
    kunluanbudang
        10
    kunluanbudang  
       2018-04-02 12:36:26 +08:00 via Android
    @Livid


    麻烦详细看看这个帖子!
    codehole
        11
    codehole  
    OP
       2018-04-02 12:36:36 +08:00 via Android
    @vegito2002 推了,有一段时间了
    codehole
        12
    codehole  
    OP
       2018-04-02 12:37:16 +08:00 via Android
    @kunluanbudang 打小报告?
    codehole
        13
    codehole  
    OP
       2018-04-02 12:37:37 +08:00 via Android
    @PythonAnswer 支持支持
    mmqc
        14
    mmqc  
       2018-04-02 12:39:02 +08:00 via Android
    谢谢分享
    omph
        15
    omph  
       2018-04-02 12:47:44 +08:00
    有人已走的更远
    https://github.com/harelba/q/
    congeec
        16
    congeec  
       2018-04-02 13:17:35 +08:00 via iPhone   ❤️ 1
    想法不错,早就有人这么干了,Google 一下就不用费时费力写这篇文章

    处理 csv,有现成的命令行工具

    而且这 shell 的水平....真不适合做教程


    最后发现又是公众号....
    codehole
        17
    codehole  
    OP
       2018-04-02 13:37:56 +08:00
    @omph 感谢分享
    ghos
        18
    ghos  
       2018-04-02 13:43:59 +08:00 via Android
    赞!讲的很清晰
    codehole
        19
    codehole  
    OP
       2018-04-02 14:12:25 +08:00 via Android
    @ghos 好东西要记得分享哦😁
    codehole
        20
    codehole  
    OP
       2018-04-02 14:13:02 +08:00 via Android
    @thomas070 谢谢夸奖
    gimp
        21
    gimp  
       2018-04-02 14:19:01 +08:00
    中间的配图让人不舒服。
    codehole
        22
    codehole  
    OP
       2018-04-02 14:26:32 +08:00
    @gimp 哈哈
    xwhxbg
        23
    xwhxbg  
       2018-04-02 14:32:04 +08:00
    写的好,可惜我连 SQL 都不是特别 6
    crane2018
        24
    crane2018  
       2018-04-02 14:36:10 +08:00
    最后那个小姑娘是谁家的孩子,非常标致👍
    qiutianaimeili
        25
    qiutianaimeili  
       2018-04-02 14:39:06 +08:00   ❤️ 2
    讲的好好的,干嘛出现什么人体蜈蚣?让人很不舒服,不是每个人都是重口味,看文章的心情都没了,
    laqow
        26
    laqow  
       2018-04-02 14:42:18 +08:00 via Android
    awk 语句能一次干完的事情就给 awk 干不就好了,中间掺点 shell 重用的时候很麻烦
    luoer
        27
    luoer  
       2018-04-02 15:10:22 +08:00
    bash> cat <(cat groups.txt | grep 技术) | wc -l
    这行语句为什么写的这么复杂
    bash> grep 技术 groups.txt | wc -l 这样不行么
    ant2017
        28
    ant2017  
       2018-04-02 15:13:18 +08:00
    配图有毒
    longbye0
        29
    longbye0  
       2018-04-02 15:18:03 +08:00
    两个图严重降低了可读性
    codehole
        30
    codehole  
    OP
       2018-04-02 15:41:55 +08:00
    @luoer 你说的没错,这个例子只是用来说明如何使用<()
    codehole
        31
    codehole  
    OP
       2018-04-02 16:09:45 +08:00
    @laqow 你的思维很严谨嘛 👍
    muziki
        32
    muziki  
       2018-04-02 16:14:08 +08:00 via iPhone
    @crane2018 日本童星 演过环太平洋

    把 grep 换成 ripgrep 这个速度快很多
    gogotanc
        33
    gogotanc  
       2018-04-02 16:14:15 +08:00 via Android
    最近有用到,这里整理得更完整呀
    toono
        34
    toono  
       2018-04-02 16:14:18 +08:00
    配图让人不舒服。。。
    codehole
        35
    codehole  
    OP
       2018-04-02 16:24:57 +08:00
    @muziki 知识渊博 👍
    nxtxiaolong
        36
    nxtxiaolong  
       2018-04-02 16:34:24 +08:00
    只是为了使用 bash 处理而处理么~增加问题复杂度,csv 不是能直接导入关系型数据库么~
    codehole
        37
    codehole  
    OP
       2018-04-02 16:55:55 +08:00
    @nxtxiaolong 方法也行,如果你那么喜欢输入数据库用户名密码等参数的话
    guanhui07
        38
    guanhui07  
       2018-04-02 17:01:39 +08:00
    当做复习了
    nxtxiaolong
        39
    nxtxiaolong  
       2018-04-02 17:09:25 +08:00
    @codehole:)总比写这样的 bash 轻松一些,shell 中的这些文本处理的用来处理处理日志文件还行~
    327beckham
        40
    327beckham  
       2018-04-02 17:10:41 +08:00
    哈哈,有的自己常用,有的自己不常用,学习到了,谢谢分享
    codehole
        41
    codehole  
    OP
       2018-04-02 17:20:39 +08:00
    @toono 这也是特色哈
    rrfeng
        42
    rrfeng  
       2018-04-02 17:23:57 +08:00 via Android
    一看就是广告
    hotea
        43
    hotea  
       2018-04-02 17:26:46 +08:00
    最后的小姑娘是谁?
    l00t
        44
    l00t  
       2018-04-02 17:28:29 +08:00
    文本里有个分隔符比如;这种你打算怎么办。
    codehole
        45
    codehole  
    OP
       2018-04-02 17:34:59 +08:00 via Android
    @l00t awk 可以指定其它分隔符
    Hardrain
        46
    Hardrain  
       2018-04-02 17:39:25 +08:00
    楼主没提到 sed?(除了推荐的书)
    另外处理多文件时配合 find 的-exec 也是不错的方案

    自己倒是除了 awk 大致都会
    codehole
        47
    codehole  
    OP
       2018-04-02 17:40:32 +08:00 via Android
    @Hardrain 怕内容太长,砍掉了
    codehole
        48
    codehole  
    OP
       2018-04-02 17:52:36 +08:00
    @nxtxiaolong 等你写习惯了,shell 处理起来也很爽
    codehole
        49
    codehole  
    OP
       2018-04-02 18:40:07 +08:00
    @rrfeng 应该说是 99+%的内容
    k9982874
        50
    k9982874  
       2018-04-02 18:52:36 +08:00 via iPhone   ❤️ 1
    看见这种贴 看都不用看 最后肯定是甩一脸公众号
    @Livid
    broadliyn
        52
    broadliyn  
       2018-04-02 19:40:39 +08:00
    学习。谢谢
    另外给个建议,就是不要把人体蜈蚣这种图片弄上来吧。太 low 了点
    blaxmirror
        53
    blaxmirror  
       2018-04-02 20:10:08 +08:00
    学习了,shell 一直感觉掌握的不够好
    luohuanlhh
        54
    luohuanlhh  
       2018-04-02 21:02:15 +08:00
    好像.有点了解了.
    congeec
        55
    congeec  
       2018-04-02 22:00:06 +08:00 via iPhone
    @luoer grep -c.... 不需要 wc 的
    suxiaohuan
        56
    suxiaohuan  
       2018-04-02 22:34:49 +08:00
    这个思路好清晰
    codehole
        57
    codehole  
    OP
       2018-04-02 22:38:37 +08:00 via Android
    @congeec 有了思维,怎么写关系不大
    rayjoy
        58
    rayjoy  
       2018-04-02 23:09:19 +08:00
    有价值的帖子,收藏了。
    20015jjw
        59
    20015jjw  
       2018-04-03 04:21:21 +08:00 via Android   ❤️ 1
    广告过分了吧 而且教的东西还不如 Google 随手 配的电影截图也是非常恶心
    @Livid
    codehole
        60
    codehole  
    OP
       2018-04-03 05:23:27 +08:00 via Android
    @20015jjw 没看出朋友的水平有多高,居然也能说出这样狂傲的话来
    xiaket
        61
    xiaket  
       2018-04-03 07:16:21 +08:00
    文不对题, 文章太 low, 记得这边不推荐全文转载的.
    codehole
        62
    codehole  
    OP
       2018-04-03 09:27:30 +08:00 via Android
    @xiaket 但是鼓励宣传啊
    codehole
        63
    codehole  
    OP
       2018-04-03 09:27:45 +08:00 via Android
    @xiaket 但是鼓励原创啊
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2458 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 15:55 · PVG 23:55 · LAX 07:55 · JFK 10:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.