Jekyll 使用技巧

这篇文章转载自文末的三篇引用。

简介

什么是Jekyll

Jekyll 是一个基于 Ruby 的静态网站生成器,而 gem 是 Ruby 的包管理器。bundle 是一个用于管理项目 gem 依赖关系的工具,它通过 GemfileGemfile.lock 文件来跟踪和安装依赖项。

简单来说,gem 是 Ruby 的全局包管理器,而 bundle 是用于管理项目级别的依赖关系的工具。

在使用 Jekyll 时,通常建议使用 bundle 来管理项目的 gem 依赖,以确保每个项目都有一致的 gem 环境。这样可以防止不同项目之间的 gem 版本冲突,确保项目能够正确运行。

安装

首先,你得有 Ruby。对于 Mac 用户,可以使用 Homebrew 安装。

brew install ruby

然后:

$ gem install jekyll

Bundler

Jekyll 3.3.0 之后开始采用 Bundler 方式运行。安装时,需要同时安装 Bundler:

$ gem install jekyll bundler

所以所有命令可能也需要使用 Bundler:

$ bundle exec jekyll serve

命令

使用 jekyll new 创建新站点

$ jekyll new my-jekyll-site

也可以:

$ mkdir my-jekyll-site
$ cd my-jekyll-site
$ jekyll new .

使用 jekyll serve 在本地运行站点

以下三种使用方式是等价的:

$ jekyll serve
$ jekyll server
$ jekyll s

通过 http://localhost:4000 进行访问。

使用 jekyll build 生成站点

以下两种使用方式是等价的:

$ jekyll build
$ jekyll b

通过 --destination 指定目标路径:

$ jekyll build --destination=/path/to/site

使用 jekyll new-theme 创建主题

$ jekyll new-theme my-theme

修改 Gemfile之后重新install

bundle install

image-20231226085533527

基础

目录结构

一个基本的 Jekyll 站点目录结构:

.
├── _config.yml
├── _data
|   └── members.yml
├── _drafts
|   ├── begin-with-the-crazy-ideas.md
|   └── on-simplicity-in-technology.md
├── _includes
|   ├── footer.html
|   └── header.html
├── _layouts
|   ├── default.html
|   └── post.html
├── _posts
|   ├── 2007-10-29-why-every-programmer-should-play-nethack.md
|   └── 2009-04-26-barcamp-boston-4-roundup.md
├── _sass
|   ├── _base.scss
|   └── _layout.scss
├── _site
├── .jekyll-metadata
└── index.html # 也可以是 index.md
  • _config.yml
    • Jekyll 站点的总配置文件,有很多选项也可以通过命令行方式指定。
  • _drafts
    • 未发布的草稿,文件名不需要带有日期。
  • _includes
    • 代码片段,可以通过 include 进行引用。
  • _layouts
    • 布局。布局文件可以被继承,{{ content }} 用于表示被继承者的内容。
  • _posts
    • 文件,命名需要以日期开头:如 2016-12-01-my-article.md
  • _sass
    • sass 文件,可以通过插件完成编译。也可以选择引入原生 CSS,或者 Less 等。
  • _site
    • 目标文件,建议加入到 .gitignore 中。
  • index.html/index.md
    • 首页

_config.yml

_config.yml 是整个站点的整体配置,以下是所有配置项和默认值:

# Where things are
source:       .
destination:  ./_site
plugins_dir:  _plugins
layouts_dir:  _layouts
data_dir:     _data
includes_dir: _includes
collections:
  posts:
    output:   true

# Handling Reading
safe:         false
include:      [".htaccess"]
exclude:      ["node_modules", "vendor/bundle/", "vendor/cache/", "vendor/gems/", "vendor/ruby/"]
keep_files:   [".git", ".svn"]
encoding:     "utf-8"
markdown_ext: "markdown,mkdown,mkdn,mkd,md"

# Filtering Content
show_drafts: null
limit_posts: 0
future:      false
unpublished: false

# Plugins
whitelist: []
gems:      []

# Conversion
markdown:    kramdown
highlighter: rouge
lsi:         false
excerpt_separator: "\n\n"
incremental: false

# Serving
detach:  false
port:    4000
host:    127.0.0.1
baseurl: "" # does not include hostname
show_dir_listing: false

# Outputting
permalink:     date
paginate_path: /page:num
timezone:      null

quiet:    false
verbose:  false
defaults: []

liquid:
  error_mode: warn

# Markdown Processors
rdiscount:
  extensions: []

redcarpet:
  extensions: []

kramdown:
  auto_ids:       true
  footnote_nr:    1
  entity_output:  as_char
  toc_levels:     1..6
  smart_quotes:   lsquo,rsquo,ldquo,rdquo
  input:          GFM
  hard_wrap:      false
  footnote_nr:    1

Front Matter

Jekyll 整个站点的配置是站点根目录下的 _config.yml 文件,而 _layout, _posts 等目录下的文件中也可以有自己的变量。文件头部的 yaml 配置被称作 Front Matter。

Front Matter 默认值

可以使用 defaults 设置一个路径下 Front Matter 默认值。

defaults:
  - scope:
      path: ""
      type: weekly
    values:
      layout: weekly
      title: 技术周刊

忽略文件

exclude 用于忽略文件或文件夹,其中 _config.yml 和以.开头的文件或文件夹都会被自动忽略。后续版本,node_modules 等文件夹也被隐式忽略了(参考 _config.yml 章节)。

exclude:
  - Gemfile
  - Gemfile.lock
  - README.md
  - LICENSE

分页

Jekyll 没有内置分页功能,而是提供了一个分页插件 jekyll-paginatejekyll-paginate 仅在特定的默认条件下生效,如果你对网站结构有自己的一套,jekyll-paginate 可能是无法满足需求的。

限制如下:

  • 分页功能必须在 HTML 格式文件中调用,如:index.html
  • 必须使用默认的链接格式 permalink

如果想继续使用,请详细阅读 http://jekyllrb.com/docs/pagination/。这是一个复杂的问题!

文章摘要

Jekyll 提供了文章摘要摘取功能,通过 post.excerpt 就可以获得摘要内容。

我们也可以设置摘取摘要的分隔符:

excerpt_separator: <!--more-->

评论

由于是静态站点,我们没发内建评论系统,因此需要引入一些纯前端就可以使用的评论系统。国外推荐:disqus,国内推荐:duoshuo

Page

可以认为,不在 _post 目录下的页面都是 Page 而不是 Post,其它方面区别不大。

Collection

并不是每个页面都是独立“页面”和以日期为顺序的“博文”,因此 Jekyll 引入了 Collection。Collection 可以根据路径定义一类具有相同属性的页面集合。Collection 也可以通过 Front Matter 设定默认值。

Data

Data 相当于动态页面中的数据库,Jekyll Data 支持 yaml, json, CSV 三种格式,可以通过 site.data 直接访问。

例如:

团队成员有 Fa, Li, Zhang 三人,于是我们在默认路径 _data 创建一个数据文件 member.yml

- name: Fa
- name: Li
- name: Zhang

在页面中显示团队成员列表:

{% for member in site.data.member %}
<ul>
  <li>{{ member.name }}</li>
</ul>
{% endfor %}

Liquid 模板

什么是 Liquid?

Liquid 是一个开源模版语言,由电商公司 Shopify 实现,用 Ruby 编写。Shopify 自己使用 Liquid 来构建自己电商网站模板生态。

详细文档请参考 https://shopify.github.io/liquid/

Jekyll 实用 Liquid 作为模版引擎,构建页面。

变量

<title>
{{ page.title }}
</title>

其中,Jekyll 预设了 site, layout, page, content 四个全局变量。

逻辑判断

Liquid 的逻辑判断跟 Ruby 完全一致。

  • 常见语言中的 if, else if, else 在 Liquid 中的对应是 if, elsif, else。同时,Liquid 也可以使用 Ruby 特有的 unless
  • 常见语言中的 switch, case 在 Liquid 中的对应是 case, when

为了简单,只以 if 为例:

{% if page.disable_syntax_highlight != true %}
<link rel="stylesheet" href="{{ site.assets }}/css/zenburn.css">
{% endif %}

遍历

在 Liquid 中可以通过 for in 语法遍历数组,并且支持一般语言循环中的 continuebreak

除此之外,还可以使用 offsetlimit 控制遍历范围,通过 reversed 进行倒序。

{% for post in site.posts reversed %}
<a href="{{ post.permalink }}">{{ post.title }}</a>
{% endfor %}

详见 https://shopify.github.io/liquid/tags/iteration/

赋值

使用 assign 进行赋值:

{% assign my_variable = false %}

使用 capture 进行捕捉赋值:

{% capture my_variable %}
I am being captured.
{% endcapture %}

Liquid Filters

Liquid Filters 是一种针对 Liquid 中变量的过滤器,语法是:

{{ var | filter: "param" }}

除去 Liquid 自身丰富的过滤器之外,Jekyll 还额外扩展了一些实用的:

  • cgi_escape url_escape xml_escape
    • 对变量进行相应的 escape
  • markdownify scssify sassify jsonify
    • 对变量内容的格式转换
  • where where_exp group_by sort
    • 对变量数据的查询排序等操作

详见 http://jekyllrb.com/docs/templates/#filters

site全局根结点

在全局根结点site中有 site.tags.TAG 和 site.categories.category.CATEGORY 变量,可以列出所有拥有TAG标签或者CATEGORY的文章的列表(这里的变量只能写英文)。 另外site.[CONFIGURATION_DATA]使得所有在_config.yml中的数据都能够通过site变量调用。

循环

和平常的解释性语言很像

{ % for post in site.posts % }
    <a href="{ { post.url } }">{ { post.title } }</a>
{ % endfor % }

if判断

注意逻辑“与或”分别是and,or

{ % if user % }
  Hello 
{ % endif % }

# Same as above
{ % if user != null % }
  Hello 
{ % endif % }

{ % if user.name == 'tobi' % }
  Hello tobi
{ % elsif user.name == 'bob' % }
  Hello bob
{ % endif % }

{ % if user.name == 'tobi' or user.name == 'bob' % }
  Hello tobi or bob
{ % endif % }

{ % if user.name == 'bob' and user.age > 45 % }
  Hello old bob
{ % endif % }

{ % if user.name != 'tobi' % }
  Hello non-tobi
{ % endif % }

# Same as above
{ % unless user.name == 'tobi' % }
  Hello non-tobi
{ % endunless % }
# Check for the size of an array
{ % if user.payments == empty % }
   you never paid !
{ % endif % }

{ % if user.payments.size > 0  % }
   you paid !
{ % endif % }

{ % if user.age > 18 % }
   Login here
{ % else % }
   Sorry, you are too young
{ % endif % }

# array = 1,2,3
{ % if array contains 2 % }
   array includes 2
{ % endif % }

# string = 'hello world'
{ % if string contains 'hello' % }
   string includes 'hello'
{ % endif % }

自动生成摘要

  { % for post in site.posts % }
     { { post.url } } { { post.title } }
      { { post.excerpt | remove: 'test' } }
  { % endfor % }

删除指定文本

remove 可以删除变量中的指定内容

{ { post.url | remove: 'http' } }

删除 html 标签

{ { post.excerpt | strip_html } }

代码高亮并且显示行数

{ % highlight ruby linenos % }
\# some ruby code
{ % endhighlight % }

获得数组的大小

{ { array | size } }

赋值

{ % assign index = 1 % }

搜索指定key

# Select all the objects in an array where the key has the given value.
{ { site.members | where:"graduation_year","2014" } } 

排序

{ { site.pages | sort: 'title', 'last' } }

转换成JSON

{ { site.data.projects | jsonify } }

把一个对象变成一个字符串

{ { page.tags | array_to_sentence_string } }

单词的个数

{ { page.content | number_of_words } }

得到数组指定范围的结果集

{ % for post in site.posts limit:20 % }

插件

插件简介

Jekyll 支持使用插件进行扩展,插件的类型分为:Generators、Converters、Commands、Hooks、Liquid Tag、Liquid Filter 等。

如果希望开发插件,请参考 http://jekyllrb.com/docs/plugins/

使用插件

  1. 基于 Gem 的方式

    对于已经发布到 RubyGems 的插件,推荐使用这种方式。只需要在 _config.ymlgems 字段加入相应插件名称即可。

  2. 基于本地文件

    对于没有发布的插件,可以在 _plugins 文件夹中直接引入 *.rb Ruby 源文件。

常用插件

Jekyll Watch

Jekyll “必备”的插件,因为这是 Jekyll 程序的依赖,只是因为程序结构设计被剥离成了插件。它在本地预览时,提供文件变更的自动更新,让我们每次刷新都能自动看到最新的内容。

Jekyll Watch 是自动开启的:

$ jekyll serve --watch

Jekyll Compose

安装了 Jekyll Compose 后,Jekyll 会额外提供一些命令,便于发布管理博文。

创建草稿:

$ jekyll draft

创建新博客:

$ jekyll post

创建新博客:

$ jekyll page

发布草稿:

$ jekyll publish

撤销发布:

$ jekyll unpublish

Jekyll Admin

Jekyll Admin 是一个 CMS 风格的图形化后台管理插件,可以在本地给用户提供服务。

screenshot of Jekyll Admin

Jekyll SEO Tag

Jekyll SEO Tag 帮你生成一大堆 Meta 标签。

Jemoji

你可以通过 Jemoji 在 Jekyll 生成的网站中,加入自己 Emoji 表情。

Emoji 语法采用 GitHub 的语法风格。

Jekyll Mentions

Jekyll Mentions 允许你在文章中直接“@” GitHub 或其它网站用户。

Jekyll Feed

你可以通过 Jekyll Feed 在 Jekyll 生成的网站中,生成 RSS 源。

Jekyll Import

Jekyll Import 支持从一些国外的主流站点导入博文,如 Blogger, WordPress 和 Tumblr 等,同样也支持 RSS 和 CSV 等数据格式导入。

Jekyll Archives

Jekyll Archives 用于生成带标签和分类的『存档』页面。

Jekyll Redirect From

Jekyll Redirect From 提供页面跳转功能,比较简单,也可以自行通过 JavaScript 实现。

GitHub Pages

创建个人主页

创建一个名为 your-github-username.github.io 的 Repo,your-github-username 是你的用户名。

只需在 Repo 中设置 GitHub Pages 的 Source,就可以开启 GitHub Pages,支持 master, gh-pages, master/docs 文件夹三种 Source。

在 GitHub Pages 只可以使用部分插件

由于安全性等原因的考虑,在 GitHub Pages 平台上只能使用白名单中的 7 个 Jekyll 插件。它们分别是:Jekyll Sitemap, Jekyll SEO Tag, github-metadata, Jekyll Feed, Jekyll Redirect From, Jemoji 和 Jekyll Mentions。

详见 https://help.github.com/articles/adding-jekyll-plugins-to-a-github-pages-site/

GitHub Pages 上的 Jekyll 只支持 kramdown

从 2016 年 5 月 1 日起,GitHub Pages 只支持 kramdown 作为 Markdown 引擎。

详见 https://github.com/blog/2100-github-pages-now-faster-and-simpler-with-jekyll-3-0

在根目录下创建 .nojekyll 文件可以跳过 Jekyll 解析

GitHub Pages 支持 Jekyll 或者原始文件。最初 GitHub Pages 只支持 Jekyll,后来 GitHub 允许在 Repo 根目录下添加 .nojekyll 跳过解析。

详见 https://github.com/blog/572-bypassing-jekyll-on-github-pages

你可以使用自己的域名

在 Source 的根路径下,创建 CNAME 写入域名,然后把 DNS 解析到 GitHub Pages 的 IP:192.30.252.153192.30.252.154

详见 https://help.github.com/articles/using-a-custom-domain-with-github-pages/

你也可以为账号下任意项目创建独立的项目页面

任何一个项目的 Repo,都可以开启这个项目的 GitHub Pages。开启方式同个人主页。

如项目:crispgm/gsm,设置完成后,就可以通过 https://crispgm.github.io/gsm/ 进行访问。

你可以通过 site.github 获得 Repo 的信息

由上面所说的 github-metadata 提供服务,通过 GitHub API 获取 Repo 的信息。当然,在本地环境下,也可以通过手动安装 github-metadata 来使用。

私有 Repo 也可以开启 GitHub Pages

GitHub 的付费用户建立的私有项目 Private Repo,也可以开启 GitHub Pages。

GitHub Pages 无法在使用 gh-pages 分支作为源的情况下关闭

有些奇怪的设定,真想关闭直接删除掉,在 Source 选择 None 就好。

参考资料


Laravel Eloquent 左联时进行筛选

在 laravel Eloquent ORM 中我们经常用到 with 这一方法来关联表。普通的使用场景也很简单,例如

class Talent{
    public function account()
    {
        return $this->belongsTo('App\Models\Account', 'account_uuid');
    }
}

使用 Talent::with(‘account’) 就可以获取到关联数据。如果希望左联查询,可以在行内使用如下语句实现:

$yesterdayCreateTalent = Talent::with(['account' => function ($q) {
  $q->yesterday();
}])->get();

也可以拆分方法进行使用。

// Talent
public function test()
{
   return $this->belongsTo('App\Models\Account', 'account_uuid')->yesterday();
   // or
   // return $this->account()->yesterday();
}

Laravel Eloquent 使用 groupBy 获取每个group的数据和

最近写到相关的代码,直接上代码做个记录。

public function scopeUserSum($query)
{
    return $query->groupBy('username')->selectRaw('sum(data) as sum, username')->orderBy('sum', 'desc');
}

如果只是纯粹求一列的和,在builder上直接用sum即可。

参考资料


Shell 拉起进程后获得 pid,$0,$?,$!等用法

起因是在前一篇《Linux 网络监控与统计实例》(/tech/2017/04/13/linux-network-monitor-and-stats-example.html)中杀死 tcpdump 的代码在流量监控项目中是有bug的,源码如下:

#tcpdump监听网络
tcpdump -v -i $eth -tnn > /tmp/tcpdump_temp 2>&1 &
sleep 10
clear
kill `ps aux | grep tcpdump | grep -v grep | awk '{print $2}'`

这个代码中最后一句 kill 的结果实质上是杀死正在运行中的 tcpdump。然而如果系统中原先已存在tcpdump,就会造成不可预期的结果。

比较适当的做法是记录运行 tcpdump 时的 pid,最后再 kill 掉。相关的代码其实非常简单:

$! # Shell最后运行的后台Process的PID

所以代码调整后的结果就是

# tcpdump监听网络
tcpdump -v -i $eth -tnn > $tmpfile1 2>&1 &
local pid=$!
sleep 60
kill $pid

除了 $!, Shell 还有下面这些特殊的用法:

变量 说明
$$ Shell本身的PID(ProcessID)
$! Shell最后运行的后台Process的PID
$? 最后运行的命令的结束代码(返回值)
$- 使用Set命令设定的Flag一览
$* 所有参数列表。如”$*“用「”」括起来的情况、以”$1 $2 … $n”的形式输出所有参数。
$@ 所有参数列表。如”$@”用「”」括起来的情况、以”$1” “$2” … “$n” 的形式输出所有参数。
$# 添加到Shell的参数个数
$0 Shell本身的文件名
$1~$n 添加到Shell的各参数值。$1是第1参数、$2是第2参数…。
!! 上一个运行的命令
n! history中第n个运行的命令

参考资料


supervisor 安装与配置

Supervisor是一个进程控制系统,由python编写。可以很方便的用来启动、重启、关闭进程(不仅仅是 Python 进程)。 它有以下一些特点

  • 可以配置同时启动的进程数,而不需要一个个启动
  • 可以根据程序的退出码来判断是否需要自动重启
  • 可以配置进程初始化的环境,包括目录,用户,umask,关闭进程所需要的信号等等
  • 有web界面进行管理

下面简单介绍一下 supervisor 的安装使用过程。更复杂的 group 等设置可以参考我文末的参考资料。

安装

使用python包管理工具pip进行安装:

pip install supervisor

配置

安装完 supervisor 之后,运行echo_supervisord_conf 命令输出默认的配置项,重定向到一个配置文件里:

echo_supervisord_conf > /etc/supervisord.conf

下面是我经过简化过的配置:

[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)

[supervisord]
logfile=/var/local/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB        ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10           ; (num of main logfile rotation backups;default 10)
loglevel=info                ; (log level;default info; others: debug,warn,trace)
pidfile=/tmp/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false               ; (start in foreground if true;default false)
minfds=1024                  ; (min. avail startup file descriptors;default 1024)
minprocs=200                 ; (min. avail process descriptors;default 200)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

[include]
files = /etc/supervisor/*.conf

然后新建文件夹来存放各种进程的配置文件:

mkdir /etc/supervisor

目前我是用来管理 laravel 的进程,在 /etc/supervisor/ 里添加文件 laravel-wechat.conf

[program:wechat]
directory = /var/local/fpm-pools/wechat/www ; 程序的启动目录
command = php /var/local/fpm-pools/wechat/www/artisan queue:work --sleep=3 --daemon --tries=3
process_name=%(program_name)s_%(process_num)02d
autostart = true     ; 在 supervisord 启动的时候也自动启动
;startsecs = 5        ; 启动 5 秒后没有异常退出,就当作已经正常启动了
autorestart = true   ; 程序异常退出后自动重启
startretries = 3     ; 启动失败自动重试次数,默认是 3
user = www-data          ; 用哪个用户启动
redirect_stderr = true  ; 把 stderr 重定向到 stdout,默认 false
numprocs = 8
stdout_logfile_maxbytes = 20MB  ; stdout 日志文件大小,默认 50MB
stdout_logfile_backups = 20     ; stdout 日志文件备份数
; stdout 日志文件,需要注意当指定目录不存在时无法正常启动,所以需要手动创建目录(supervisord 会自动创建日志文件)
stdout_logfile = /var/local/log/wechat/wechat.queue.log

; 可以通过 environment 来添加需要的环境变量,一种常见的用法是修改 PYTHONPATH
; environment=PYTHONPATH=$PYTHONPATH:/path/to/somewhere

启动后命令行界面输入 supervisorctl 进入控制界面,如下则说明 supervisor 启动成功、laravel 进程配置成功

启动 supervisord

# 使用默认的配置文件 /etc/supervisord.conf
supervisord
# 明确指定配置文件
supervisord -c /etc/supervisord.conf
# 使用 user 用户启动 supervisord
supervisord -u user

supervisorctl 命令介绍

# 停止某一个进程,program_name 为 [program:x] 里的 x
supervisorctl stop program_name
# 启动某个进程
supervisorctl start program_name
# 重启某个进程
supervisorctl restart program_name
# 结束所有属于名为 groupworker 这个分组的进程 (start,restart 同理)
supervisorctl stop groupworker:
# 结束 groupworker:name1 这个进程 (start,restart 同理)
supervisorctl stop groupworker:name1
# 停止全部进程,注:start、restart、stop 都不会载入最新的配置文件
supervisorctl stop all
# 载入最新的配置文件,停止原有进程并按新的配置启动、管理所有进程
supervisorctl reload
# 根据最新的配置文件,启动新配置或有改动的进程,配置没有改动的进程不会受影响而重启
supervisorctl update

注意:显示用 stop 停止掉的进程,用 reload 或者 update 都不会自动重启。

开机自动启动 Supervisord

Supervisord 默认情况下并没有被安装成服务,它本身也是一个进程。

# 下载脚本
sudo su - root -c "sudo curl https://gist.githubusercontent.com/howthebodyworks/176149/raw/d60b505a585dda836fadecca8f6b03884153196b/supervisord.sh > /etc/init.d/supervisord"
# 设置该脚本为可以执行
sudo chmod +x /etc/init.d/supervisord
# 设置为开机自动运行
sudo update-rc.d supervisord defaults
# 试一下,是否工作正常
service supervisord stop
service supervisord start 

参考资料


Linux 网络监控与统计实例

最近写到相关的代码,做个记录。

知识补充

首先补充本文用到的 Shell/Linux 的一些知识。

  • 数组
  • grep
  • awk
  • sed
  • ss
  • tcpdump

数组

ArrayName=("element 1" "element 2" "element 3") ## 定义数组
${distro[0]}        ## 访问数组
${#distro[@]}       ## 数组长度

grep

实例:

ifconfig | grep -E -o "^[a-z0-9]+" | grep -v "lo"
ifconfig | grep -A 1 eth0

手册:

grep [OPTIONS] PATTERN [FILE...]
grep [OPTIONS] [-e PATTERN | -f FILE] [FILE...]

Matcher Selection
   -E, --extended-regexp 正则
General Output Control
    -o, --only-matching 仅匹配非空格
    -v, --invert-match 非匹配
Context Line Control
    -A NUM, --after-context=NUM 打印匹配行之后第几行

awk

以前写过一篇Linux命令之awk,详细内容可以进去查看。这里只涉及需要用到的。

实例:

cat xxx | awk -F'[: ]+' '$0~/inet addr:/{printf $4"|"}
awk -v eth=$eth -F'[: ]+' '{if ($0 ~eth){print $3,$11}}

手册:

awk '/search pattern1/ {Actions}    
     /search pattern2/ {Actions}' file    
     
* search pattern是正则表达式
* Actions 输出的语法
* 在Awk 中可以存在多个正则表达式和多个输出定义
* file 输入文件名
* 单引号的作用是包裹起来防止shell截断

背景概念:

awk默认是以行为单位处理文本的 awk中的两个术语:

    记录(默认就是文本的每一行)
    字段 (默认就是每个记录中由空格或TAB分隔的字符串)

$0就表示一个记录,$1表示记录中的第一个字段。

解释:

-F'[: ]+' 以:或者空格作为分隔符
$0表示一个记录(行)
~ 表示模式开始。/ /中是模式。
/inet addr:/这就是一个正则表达式的匹配
{printf $4"|"} 打印按照分隔符分割所成数组的第四个字段

-v 是将 Shell 变量 $eth 的值传给 awk 变量。

sed

sed在处理文本文件的时候,会在内存上创建一个模式空间,然后把这个文件的每一行调入模式空间用相应的命令处理,处理完输出;接着处理下一行,直到最后。

cat xxx | sed -e 's/|$//' 
cat xxx | sed  -n '1,3p' ## 打印1~3行

手册:

sed [选项]  [定址commands] [inputfile]

关于定址:
定址可以是0个、1个、2个;通知sed去处理文件的哪几行。
0个:没有定址,处理文件的所有行
1个:行号,处理行号所在位置的行
2个:行号、正则表达式,处理被行号或正则表达式包起来的行

/proc/net/dev

Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
  ppp1: 2007461   25873    0    0    0     0          0         0 33320940   30484    0    0    0     0       0          0
    lo: 443577110 1042128    0    0    0     0          0         0 443577110 1042128    0    0    0     0       0          0
  ppp0: 5894011   36473    0    0    0     0          0         0 72146111   64491    0    0    0     0       0          0
docker0:       0       0    0    0    0     0          0         0        0       0    0    0    0     0       0          0
  eth0: 59705148255 56461407    0    0    0     0          0         0 49418615742 41712001    0    0    0     0       0          0

ss

ss命令可以用来获取socket统计信息,它可以显示和netstat类似的内容。

tcpdump

tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤。

基本上tcpdump总的的输出格式为:系统时间 来源主机.端口 > 目标主机.端口 数据包参数

-i eth0  # 监视指定网络接口的数据包 

例子

代码如下:下载地址

#!/bin/bash
 
#write by zhumaohai(admin#centos.bz)
#author blog: www.centos.bz
 
 
#显示菜单(单选)
display_menu(){
local soft=$1
local prompt="which ${soft} you'd select: "
eval local arr=(\${${soft}_arr[@]})
while true
do
    echo -e "#################### ${soft} setting ####################\n\n"
    for ((i=1;i<=${#arr[@]};i++ )); do echo -e "$i) ${arr[$i-1]}"; done
    echo
    read -p "${prompt}" $soft
    eval local select=\$$soft
    if [ "$select" == "" ] || [ "${arr[$soft-1]}" == ""  ];then
        prompt="input errors,please input a number: "
    else
        eval $soft=${arr[$soft-1]}
        eval echo "your selection: \$$soft"             
        break
    fi
done
}
 
#把带宽bit单位转换为人类可读单位
bit_to_human_readable(){
    #input bit value
    local trafficValue=$1
 
    if [[ ${trafficValue%.*} -gt 922 ]];then
        #conv to Kb
        trafficValue=`awk -v value=$trafficValue 'BEGIN{printf "%0.1f",value/1024}'`
        if [[ ${trafficValue%.*} -gt 922 ]];then
            #conv to Mb
            trafficValue=`awk -v value=$trafficValue 'BEGIN{printf "%0.1f",value/1024}'`
            echo "${trafficValue}Mb"
        else
            echo "${trafficValue}Kb"
        fi
    else
        echo "${trafficValue}b"
    fi
}
 
#判断包管理工具
check_package_manager(){
    local manager=$1
    local systemPackage=''
    if cat /etc/issue | grep -q -E -i "ubuntu|debian";then
        systemPackage='apt'
    elif cat /etc/issue | grep -q -E -i "centos|red hat|redhat";then
        systemPackage='yum'
    elif cat /proc/version | grep -q -E -i "ubuntu|debian";then
        systemPackage='apt'
    elif cat /proc/version | grep -q -E -i "centos|red hat|redhat";then
        systemPackage='yum'
    else
        echo "unkonw"
    fi
 
    if [ "$manager" == "$systemPackage" ];then
        return 0
    else
        return 1
    fi   
}
 
 
#实时流量
realTimeTraffic(){
    local eth=""
    local nic_arr=(`ifconfig | grep -E -o "^[a-z0-9]+" | grep -v "lo" | uniq`)
    local nicLen=${#nic_arr[@]}
    if [[ $nicLen -eq 0 ]]; then
        echo "sorry,I can not detect any network device,please report this issue to author."
        exit 1
    elif [[ $nicLen -eq 1 ]]; then
        eth=$nic_arr
    else
        display_menu nic
        eth=$nic
    fi   
 
    local clear=true
    local eth_in_peak=0
    local eth_out_peak=0
    local eth_in=0
    local eth_out=0
 
    while true;do
        #移动光标到0:0位置
        printf "\033[0;0H"
        #清屏并打印Now Peak
        [[ $clear == true ]] && printf "\033[2J" && echo "$eth--------Now--------Peak-----------"
        traffic_be=(`awk -v eth=$eth -F'[: ]+' '{if ($0 ~eth){print $3,$11}}' /proc/net/dev`)
        sleep 2
        traffic_af=(`awk -v eth=$eth -F'[: ]+' '{if ($0 ~eth){print $3,$11}}' /proc/net/dev`)
        #计算速率
        eth_in=$(( (${traffic_af[0]}-${traffic_be[0]})*8/2 ))
        eth_out=$(( (${traffic_af[1]}-${traffic_be[1]})*8/2 ))
        #计算流量峰值
        [[ $eth_in -gt $eth_in_peak ]] && eth_in_peak=$eth_in
        [[ $eth_out -gt $eth_out_peak ]] && eth_out_peak=$eth_out
        #移动光标到2:1
        printf "\033[2;1H"
        #清除当前行
        printf "\033[K"   
        printf "%-20s %-20s\n" "Receive:  $(bit_to_human_readable $eth_in)" "$(bit_to_human_readable $eth_in_peak)"
        #清除当前行
        printf "\033[K"
        printf "%-20s %-20s\n" "Transmit: $(bit_to_human_readable $eth_out)" "$(bit_to_human_readable $eth_out_peak)"
        [[ $clear == true ]] && clear=false
    done
}
 
#流量和连接概览
trafficAndConnectionOverview(){
    if ! which tcpdump > /dev/null;then
        echo "tcpdump not found,going to install it."
        if check_package_manager apt;then
            apt-get -y install tcpdump
        elif check_package_manager yum;then
            yum -y install tcpdump
        fi
    fi
 
    local reg=""
    local eth=""
    local nic_arr=(`ifconfig | grep -E -o "^[a-z0-9]+" | grep -v "lo" | uniq`)
    local nicLen=${#nic_arr[@]}
    if [[ $nicLen -eq 0 ]]; then
        echo "sorry,I can not detect any network device,please report this issue to author."
        exit 1
    elif [[ $nicLen -eq 1 ]]; then
        eth=$nic_arr
    else
        display_menu nic
        eth=$nic
    fi
 
    echo "please wait for 10s to generate network data..."
    echo
    #当前流量值
    local traffic_be=(`awk -v eth=$eth -F'[: ]+' '{if ($0 ~eth){print $3,$11}}' /proc/net/dev`)
    #tcpdump监听网络
    tcpdump -v -i $eth -tnn > /tmp/tcpdump_temp 2>&1 &
    sleep 10
    clear
    kill `ps aux | grep tcpdump | grep -v grep | awk '{print $2}'`

    #10s后流量值
    local traffic_af=(`awk -v eth=$eth -F'[: ]+' '{if ($0 ~eth){print $3,$11}}' /proc/net/dev`)
    #打印10s平均速率
    local eth_in=$(( (${traffic_af[0]}-${traffic_be[0]})*8/10 ))
    local eth_out=$(( (${traffic_af[1]}-${traffic_be[1]})*8/10 ))
    echo -e "\033[32mnetwork device $eth average traffic in 10s: \033[0m"
    echo "$eth Receive: $(bit_to_human_readable $eth_in)/s"
    echo "$eth Transmit: $(bit_to_human_readable $eth_out)/s"
    echo

    local regTcpdump=$(ifconfig | grep -A 1 $eth | awk -F'[: ]+' '$0~/inet addr:/{printf $4"|"}' | sed -e 's/|$//' -e 's/^/(/' -e 's/$/)\\\\\.[0-9]+:/')
  
    #新旧版本tcpdump输出格式不一样,分别处理
    if awk '/^IP/{print;exit}' /tmp/tcpdump_temp | grep -q ")$";then
        #处理tcpdump文件
        awk '/^IP/{print;getline;print}' /tmp/tcpdump_temp > /tmp/tcpdump_temp2
    else
        #处理tcpdump文件
        awk '/^IP/{print}' /tmp/tcpdump_temp > /tmp/tcpdump_temp2
        sed -i -r 's#(.*: [0-9]+\))(.*)#\1\n    \2#' /tmp/tcpdump_temp2
    fi
    
    awk '{len=$NF;sub(/\)/,"",len);getline;print $0,len}' /tmp/tcpdump_temp2 > /tmp/tcpdump

    #统计每个端口在10s内的平均流量
    echo -e "\033[32maverage traffic in 10s base on server port: \033[0m"
    awk -F'[ .:]+' -v regTcpdump=$regTcpdump '{if ($0 ~ regTcpdump){line="clients > "$8"."$9"."$10"."$11":"$12}else{line=$2"."$3"."$4"."$5":"$6" > clients"};sum[line]+=$NF*8/10}END{for (line in sum){printf "%s %d\n",line,sum[line]}}' /tmp/tcpdump | \
    sort -k 4 -nr | head -n 10 | while read a b c d;do
        echo "$a $b $c $(bit_to_human_readable $d)/s"
    done
    echo -ne "\033[11A"
    echo -ne "\033[50C"
    echo -e "\033[32maverage traffic in 10s base on client port: \033[0m"
    awk -F'[ .:]+' -v regTcpdump=$regTcpdump '{if ($0 ~ regTcpdump){line=$2"."$3"."$4"."$5":"$6" > server"}else{line="server > "$8"."$9"."$10"."$11":"$12};sum[line]+=$NF*8/10}END{for (line in sum){printf "%s %d\n",line,sum[line]}}' /tmp/tcpdump | \
    sort -k 4 -nr | head -n 10 | while read a b c d;do
            echo -ne "\033[50C"
            echo "$a $b $c $(bit_to_human_readable $d)/s"
    done   
        
    echo

    #统计在10s内占用带宽最大的前10个ip
    echo -e "\033[32mtop 10 ip average traffic in 10s base on server: \033[0m"
    awk -F'[ .:]+' -v regTcpdump=$regTcpdump '{if ($0 ~ regTcpdump){line=$2"."$3"."$4"."$5" > "$8"."$9"."$10"."$11":"$12}else{line=$2"."$3"."$4"."$5":"$6" > "$8"."$9"."$10"."$11};sum[line]+=$NF*8/10}END{for (line in sum){printf "%s %d\n",line,sum[line]}}' /tmp/tcpdump | \
    sort -k 4 -nr | head -n 10 | while read a b c d;do
        echo "$a $b $c $(bit_to_human_readable $d)/s"
    done
    echo -ne "\033[11A"
    echo -ne "\033[50C"
    echo -e "\033[32mtop 10 ip average traffic in 10s base on client: \033[0m"
    awk -F'[ .:]+' -v regTcpdump=$regTcpdump '{if ($0 ~ regTcpdump){line=$2"."$3"."$4"."$5":"$6" > "$8"."$9"."$10"."$11}else{line=$2"."$3"."$4"."$5" > "$8"."$9"."$10"."$11":"$12};sum[line]+=$NF*8/10}END{for (line in sum){printf "%s %d\n",line,sum[line]}}' /tmp/tcpdump | \
    sort -k 4 -nr | head -n 10 | while read a b c d;do
        echo -ne "\033[50C"
        echo "$a $b $c $(bit_to_human_readable $d)/s"
    done 

    echo
    #统计连接状态
    local regSS=$(ifconfig | grep -A 1 $eth | awk -F'[: ]+' '$0~/inet addr:/{printf $4"|"}' | sed -e 's/|$//')
    ss -an | grep -v -E "LISTEN|UNCONN" | grep -E "$regSS" > /tmp/ss
    echo -e "\033[32mconnection state count: \033[0m"
    awk 'NR>1{sum[$(NF-4)]+=1}END{for (state in sum){print state,sum[state]}}' /tmp/ss | sort -k 2 -nr
    echo
    #统计各端口连接状态
    echo -e "\033[32mconnection state count by port base on server: \033[0m"
    awk 'NR>1{sum[$(NF-4),$(NF-1)]+=1}END{for (key in sum){split(key,subkey,SUBSEP);print subkey[1],subkey[2],sum[subkey[1],subkey[2]]}}' /tmp/ss | sort -k 3 -nr | head -n 10   
    echo -ne "\033[11A"
    echo -ne "\033[50C"
    echo -e "\033[32mconnection state count by port base on client: \033[0m"
    awk 'NR>1{sum[$(NF-4),$(NF)]+=1}END{for (key in sum){split(key,subkey,SUBSEP);print subkey[1],subkey[2],sum[subkey[1],subkey[2]]}}' /tmp/ss | sort -k 3 -nr | head -n 10 | awk '{print "\033[50C"$0}'   
    echo   
    #统计端口为80且状态为ESTAB连接数最多的前10个IP
    echo -e "\033[32mtop 10 ip ESTAB state count at port 80: \033[0m"
    cat /tmp/ss | grep ESTAB | awk -F'[: ]+' '{sum[$(NF-2)]+=1}END{for (ip in sum){print ip,sum[ip]}}' | sort -k 2 -nr | head -n 10
    echo
    #统计端口为80且状态为SYN-RECV连接数最多的前10个IP
    echo -e "\033[32mtop 10 ip SYN-RECV state count at port 80: \033[0m"
    cat /tmp/ss | grep -E "$regSS" | grep SYN-RECV | awk -F'[: ]+' '{sum[$(NF-2)]+=1}END{for (ip in sum){print ip,sum[ip]}}' | sort -k 2 -nr | head -n 10
}
 
main(){
    while true; do
        echo -e "1) real time traffic.\n2) traffic and connection overview.\n"
        read -p "please input your select(ie 1): " select
        case  $select in
            1) realTimeTraffic;break;;
            2) trafficAndConnectionOverview;break;;
            *) echo "input error,please input a number.";;
        esac
    done   
}
 
main

获得本机 ip

ifconfig | grep -A 1 $eth | awk -F'[: ]+' '$0~/inet addr:/{printf $4"|"}' | sed -e 's/|$//' -e 's/^/(/' -e 's/$/)\\\\\.[0-9]+:/'

结果:

(103.29.71.237)\\.[0-9]+:#

接口流量/proc/net/dev

awk -F'[: ]+' '{if ($0 ~eth0){print $3,$11}}' /proc/net/dev

tcpdump监听网络

tcpdump -v -i eth0 -tnn > /tmp/tcpdump_temp 2>&1 &

杀死进程

kill `ps aux | grep tcpdump | grep -v grep | awk '{print $2}'`

统计10s内速率

awk -F'[ .:]+' -v regTcpdump=$regTcpdump '{if ($0 ~ regTcpdump){line="clients > "$8"."$9"."$10"."$11":"$12}else{line=$2"."$3"."$4"."$5":"$6" > clients"};sum[line]+=$NF*8/10}END{for (line in sum){printf "%s %d\n",line,sum[line]}}' /tmp/tcpdump | \
sort -k 4 -nr | head -n 10 | while read a b c d;do
    echo "$a $b $c $(bit_to_human_readable $d)/s"
done

这一段太复杂了,看不懂Orz

参考资料