BlueXIII's Blog

热爱技术,持续学习

0%

参考文档

常用查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
db.getCollection('policy_content').find({})

# 条件查询
db.getCollection('policy_content').find({"task_id":1});

# Count
db.getCollection('policy_content').find({"task_id":1}).count();
# 限制条数
db.getCollection('policy_content').find({"task_id":1}).limit(5);
# 分页
db.getCollection('policy_content').find({"task_id":1}).limit(10).skip(5);;
# OR
db.getCollection('policy_content').find({$or:[{column: "2"}, {column1: "24"}]});


# 比较查询 $gt" 、"$gte"、 "$lt"、 "$lte"
db.getCollection('policy_content').find({"task_id":1, "created":{"":10000}}).count();
db.getCollection('policy_content').find({'DataTime':{'$gte':1518278400}}).count()

# 时间
db.getCollection('policy_content').find({"task_id":1, "created":{$gte:"2021-09-01",$lte:"2021-9-11"}})

# 模糊查询
db.getCollection('policy_content').find({ "name" : /mongo/});

# 查询指定条件下指定列
db.getCollection('policy_content').find({}, {"column":1, "InMongoTimet":1});//查询column和InMongoTimet这两个字段

# 建索引
db.policy_content.createIndex({"index_id":1})

参考文档

关注点

  • 法律风险
  • 反爬
  • 文本提取 XPATH、正则、NLP、视觉AI、GNE
  • 分布式爬虫、调度
  • 部署任务分派 Crawlab、K8S
  • URL去重 Redis
  • 自动化渲染技术 Selenium、Puppeteer
  • 消息队列
  • 资源问题 布隆过滤器

文本提取

  • 用归类法,相似的内容归类到一起,然后给一类内容配置提取规则 XPATH
  • 用正则,提取指定标签中的内容;
  • 用深度学习,NLP 语义分析出哪里是有意义的内容,提取出来;
  • 用计算机视觉,让人去点击,然后按照页面相似度分类提取(其实就是归类法的自动化版本);
  • 用算法,计算出文本的密度,然后提取 GNE

去重

  • Redis
  • MySQL
  • MongoDB
  • 内存

自动化渲染

  • Rod
  • Chromedp
  • Puppeteer/Pyppeteer
  • Playwright
  • Selenium
  • Cypress

反爬

  • UA
  • IP
  • 登录
  • token/sign等加密参数
  • 特殊的 headers 头验证
  • aes 和 des 加密某字段
  • 会员权限等级限制
  • web 端的 js 混淆
  • ssl pinning 检测
  • 微信授权登录

商业软件

八爪鱼
后羿

crawlab

https://www.v2ex.com/t/590527

Java库

GNE

https://github.com/kingname/GeneralNewsExtractor

书籍

Python3 网络爬虫宝典

爬虫管理

https://juejin.cn/post/6844903912349122573

人名分词

反爬检测

https://bot.sannysoft.com/

正文提取

GUI

elasticvue

  • chrome插件
  • 查询 + 管理

elasticsearch-head

https://github.com/mobz/elasticsearch-head

  • chrome插件
  • 查询 + 管理

Kaizen

https://www.elastic-kaizen.com/

  • java桌面版
  • 纯查询

elasticsearch-hq

https://github.com/ElasticHQ/elasticsearch-HQ

docker run -p 5000:5000 elastichq/elasticsearch-hq
open http://localhost:5000/

  • docker run
  • 监控 + 简单查询

djavu

https://github.com/appbaseio/dejavu/

1
2
docker run -p 1358:1358 -d appbaseio/dejavu
open http://localhost:1358/

参考文档

https://www.ruanyifeng.com/blog/2017/08/elasticsearch.html
https://cloud.tencent.com/developer/article/1547867
https://juejin.cn/post/6844903919013855240
https://www.elastic.co/guide/cn/elasticsearch/guide/current/query-dsl-intro.html
https://n3xtchen.github.io/n3xtchen/elasticsearch/2017/07/05/elasticsearch-23-useful-query-example
https://www.jianshu.com/p/eb30eee13923
https://zhuanlan.zhihu.com/p/34240906
https://feifeiyum.github.io/2020/02/27/es-mapping/
https://cloud.tencent.com/developer/article/1189279

Kibana教程

https://www.ruanyifeng.com/blog/2017/08/elasticsearch.html
https://www.jianshu.com/p/7ca9e5b02ef6
https://www.elastic.co/guide/cn/kibana/current/setup.html

KibanaDevTool

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 查看
GET /_cat

# 查看Index
GET /_cat/indices?v

# 查看健康
GET /_cat/health?v

# 查看全部mapping
GET /_mapping?pretty=true


# 创建索引
PUT /customer?pretty

# 创建文档
PUT /customer/_doc/1?pretty
{
"name": "John Doe"
}

# 根据ID查询文档
GET /customer/_doc/1?pretty

# 查询所有文档
GET /customer/_search?q=*&sort=name:asc&pretty

# 查看索引的mapping
GET /customer/_mapping?pretty=true

# 查看索引的settings
GET /customer/_settings?pretty=true


GET /bookdb_index/book/_search?q=guide

{
"query": {
"multi_match" : {
"query" : "guide",
"fields" : ["_all"]
}
}
}

查询2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 查询
GET /crawl_log/_search/
{
"query": {
"match_all": {}
}
}

# 基本匹配
GET /bookdb_index/book/_search?q=guide

GET /bookdb_index/book/_search
{
"query": {
"multi_match" : {
"query" : "guide",
"fields" : ["_all"]
}
}
}

# 词条查询
POST /bookdb_index/book/_search
{
"query": {
"term" : {
"publisher": "manning"
}
},
"_source" : ["title","publish_date","publisher"]
}

# 多词条查询
POST /bookdb_index/book/_search
{
"query": {
"terms" : {
"publisher": ["oreilly", "packt"]
}
}
}

# 排序
POST /bookdb_index/book/_search
{
"query": {
"term" : {
"publisher": "manning"
}
},
"_source" : ["title","publish_date","publisher"],
"sort": [
{ "publish_date": {"order":"desc"}}
]
}

# 范围查询
POST /bookdb_index/book/_search
{
"query": {
"range" : {
"publish_date": {
"gte": "2015-01-01",
"lte": "2015-12-31"
}
}
},
"_source" : ["title","publish_date","publisher"]
}

查询3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# match_all
POST /crawl_log/_search
{
"query": {
"match_all": {}
}
}

# term
POST /crawl_log/_search
{
"query":{
"term":{
"task_id":1
}
}
}

# from size
POST /crawl_log/_search
{
"query":{
"term":{
"task_id":1
},
},
"from": 10,
"size": 10
}

# match
POST /crawl_log/_search
{
"query": {
"match": {
"message": "中埃建交五十周年"
}
}
}

# match_phase
POST /crawl_log/_search
{
"query": {
"match_phase": {
"message": "中埃建交五十周年"
}
}
}

日志查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
GET /crawl_log

GET /crawl_log/_search


POST /crawl_log/_search
{
"query": {
"match_all": {}
}
}

POST /crawl_log/_search
{
"query": {
"bool": {
"must": [{
"term": {
"task_id": "1"
}
},
{
"match": {
"level_name": "INFO"
}
}
],
"must_not": [],
"should": []
}
},
"from": 0,
"size": 10,
"sort": [],
"aggs": {}
}

管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 创建一个索引
PUT twitte
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
}

# 创建mapping映射
PUT twitte
{
"settings" : {
"index" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
},
"mappings" : {
"type1" : {
"properties" : {
"field1" : { "type" : "text" }
}
}
}
}

# 查看索引的定义信息
GET /twitte

# 删除索引
DELETE /twitte

别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看别名
GET /_cat/aliases

# 增加一个别名
POST /_aliases
{"actions": [{"add": {"index": "twitte", "alias": "alias1"}}]}

# 删除别名
POST /_aliases
{"actions": [{"remove": {"index": "twitte", "alias": "alias1"}}]}

# 一个别名对应多个索引
POST /_aliases
{"actions": {"add": {"indices": ["twitte", "twitte2"], "alias": "alias1"}}}

# 一个别名对应多个索引(通配符)
POST /_aliases
{"actions": [{"add": {"index": "twitte*", "alias": "alias1"}}]}

索引复制

1
2
3
4
5
6
7
8
9
10
11
POST /_reindex?slices=9&refresh&wait_for_completion=false

POST _reindex?wait_for_completion=false
{
"source": {
"index": "crawl_log"
},
"dest": {
"index": "crawl_log_bak"
}
}

mapping

  • 字符串: text和keyword
  • 整数 : byte, short, integer, long
  • 浮点数: float, double
  • 布尔型: boolean
  • 日期: date
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 创建索引
PUT /my-index
{
"mappings" {
"properties": {
"age": {"type": "integer"},
"email": {"type": "keyword"},
"name": {"type": "text"}
}
}
}

# 添加字段
PUT /my-index/_mapping
{
"properties": {
"content": {"type": "text"}
}
}

清空数据

1
2
3
4
5
6
7
8
# 清空索引
POST /policies_hydrabot/_delete_by_query?refresh&slices=5&pretty
{
"query": {
"match_all": {}
}
}

Curl请求示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
curl -X GET 'http://localhost:9200/_cat/indices?v'
curl -X GET 'http://localhost:9200/_mapping?pretty=true'
curl -X GET 'http://localhost:9200/crawl_log/_mapping?pretty=true'

curl -X PUT 'localhost:9200/weather'

curl -X DELETE 'localhost:9200/weather'

curl -X PUT 'http://localhost:9200/accounts/person/1' \
-H 'Content-Type: application/json' \
-d '{
"user": "张三",
"title": "工程师",
"desc": "数据库管理"
}'


curl -X POST 'http://localhost:9200/accounts/person' \
-H 'Content-Type: application/json' \
-d '{
"user": "李四",
"title": "工程师",
"desc": "系统管理"
}'


curl 'localhost:9200/accounts/person/1?pretty=true'
curl 'localhost:9200/weather/beijing/abc?pretty=true'

curl -X DELETE 'localhost:9200/accounts/person/1'

curl 'localhost:9200/accounts/person/_search'


curl 'localhost:9200/accounts/person/_search' \
-H 'Content-Type: application/json' \
-d '
{
"query" : { "match" : { "desc" : "软件" }}
}'

MySQL5.6JSON解析

新建存储过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

DELIMITER $$
DROP function if exists json_extract_c$$
CREATE FUNCTION json_extract_c(
details TEXT,
required_field VARCHAR (255)
) RETURNS TEXT CHARSET utf8mb4
DETERMINISTIC
NO SQL
BEGIN
DECLARE search_term, val TEXT;
DECLARE pos INT signed DEFAULT 1;

-- Remove '{' and '}'
SET details = SUBSTRING_INDEX(details, "{", -1);
SET details = SUBSTRING_INDEX(details, "}", 1);
-- Transform '$.xx' to be '"xx"'
SET search_term = CONCAT('"', SUBSTRING_INDEX(required_field,'$.', - 1), '"');

searching: LOOP
SET pos = LOCATE(search_term, details);
-- Keep searching if the field contains escape chars
WHILE pos > 0 AND RIGHT(LEFT(details, pos-1), 1) = '\\'
DO
SET details = SUBSTR(details, pos+LENGTH(search_term));
SET pos = LOCATE(search_term, details);
END WHILE;
-- Return NULL if not found
IF pos <= 0 THEN
RETURN NULL;
END IF;

SET pos = LENGTH(search_term)+pos;
SET details = SUBSTR(details, pos);
SET val = TRIM(details);

-- see if we reach the value that is a leading colon ':'
IF LEFT(val, 1) = ':' THEN
RETURN TRIM(
TRAILING ',' FROM
TRIM(
SUBSTRING_INDEX(
TRIM(
BOTH '"' FROM TRIM(
SUBSTR(
val
, 2
)
)
)
, '"', 1
)
)
);
ELSE
ITERATE searching;
END IF;
END LOOP;
END$$
DELIMITER ;

查询

1
2
3
4
5
6
SELECT offer_id
, terms
, json_extract_c(terms, 'qty') as offer_qty
, json_extract_c(terms, 'discount') as offer_discount
, json_extract_c(terms, 'total') as offer_total
FROM offer;

场景

同事的一个新需求,需要在AIX上以shell脚本的方式,调用sqlplus客户端连接Oracle数据库,调用mysql客户端连接MySQL数据库。
想尽量简化开发,只用纯脚本方式实现,不想替换为Java或其它方案,且不想远程调用其它Linux主机上的客户端。
故需要在AIX上安装两个客户端(只有普通用户权限,没有root权限)。没什么太特别的地方,简单记录一下过程。

sqlplus客户端的安装

Oracle接供了AIX下的InstantClient二进制zip包,其中包含了sqlplus,安装起来比较简单,主要注意一下环境变量的设置。

下载InstantClient

这里是Oracle Instant Client的下载地址 ,目前最新版本是11.2.0.1.0

  1. 首先根据系统,选择”适用于 AIX5L(64 位)的 Instant Client”
  2. 然后再下载Basic和SQLPlus两个编译好的二进制包,注意两个都要下载

上传并解压

将下载好的两个包 basic-11.2.0.1.0-aix-ppc64.zipsqlplus-11.2.0.1.0-aix-ppc64.zip上传至服务器,使用unzip解压至同一目录,例如: /path/to/instantclient。

设置环境变量

编辑.profile文件,加入以下两行:

1
2
export PATH=$PATH:/path/to/instantclient
export LIBPATH=/path/to/instantclient

注意AIX下要设置LIBPATH环境变量,且只能填写InstantClient一个目录,不能用冒号分割加入其它目录。

mysql客户端的安装

MySQL官网 目前只提供5.5~5.7的下载,且二进制ZIP包和RPM/DEB包主要针对Linux的各个发行版,没有对应的AIX版本。

连源码包也是”Generic Linux”的,在AIX下编译的话编译器和依赖都会有问题。Google上可以搜索到MySQL 5.1版本在AIX下成功编译的案例(需要一些trick),不过老源码目前在官网已经下载不到了。
第三方网站bullfreeware提供了5.5版本的RPM包。此种方式有安全风险,不过已经是唯一的方案了。

下载并处理RPM包

  1. bullfreeware 上下载MySQL5.5版本的客户端 MySQL-client-5.5.10-1.aix5.3.ppc.rpm
  2. 因为我们是没有root权限的,所有无法直接安装rpm包,需要将包里的二进制内容提取出来直接拷贝到安装目录:
    将rpm直接使用unzip解压,解出如下的目录结构:

    直接将bin目录下的二进制文件取出来上传至服务器,路径例如: /path/to/mysqlclient

设置环境变量

编辑.profile文件,加入PATH:

1
export PATH=$PATH:/path/to/mysqlclient

至此安装完毕

什么是个人知识管理

个人知识管理,又名PKM(Personal Knowledge Management), 是一种个人收集,验证,存储,搜索,提取,分享知识的过程。 –WIKI
PKM其实是一个很大的命题,包含了很多方面的内容。但我们每个人都在或多或少的使用它,都有自已的一些工具、心得。
以下仅讨论一下具体的笔记、摘录等文档的 编写保存

什么是MarkDown

Markdown是一种轻量级标记语言,它允许人们“使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档”。这种语言吸收了很多在电子邮件中已有的纯文本标记的特性。
MarkDown第一眼看上去这个样子的:

左侧是”源码”,右侧是渲染好的”结果”

MarkDown的优势

MarkDown有很多先天的优势,决定了它非常适合用来编写文档、记录笔记、撰写文章:

  • 首先,MarkDown是 纯文本 ,这意味着我们可以使用任何自已喜欢的文本编辑器来进行写作,例如vi/emacs/SublimeText/Atom/VsCode/记事本/EditPlus/UltraEdit等等。
  • 然后,MarkDown在纯文本的基础上,加入某些极其简单标记,例如用#表示一级标题,##表示二级标题等。相比起HTML,要简单的多,不会对文章内容造成过多的入侵。
  • 虽然没有官方机构,但MarkDown目前已经是事实上的标准,而且已经非常流行,在GitHub等网站中有很好的应用。
  • 市面上目前有许多优秀的MarkDown编辑器(或插件),可以提供更优雅、更舒适的写作体验。
  • MarkDown很容易导出为HTML或PDF格式,而且大多数工具都可以提供自定义的样式。

MarkDown语法速成

MarkDown语法非常简单,下面列出一些基本的标记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
标题:  
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题

文字样式:
**用两个星号标记起来,表示加粗**
*一个星号,表示斜体*
~~这样子表示删除线~~

插入链接:
[链接名称](http://url.com/)

邮箱链接:
<myname@example.com>

插入图片:
![图片的信息,可为空](这里是图片的url,可以是本地路径,也可以是远程URL)

无序列表:
- Red
- Green
- Blue

有序列表:
1. Bird
2. McHale
3. Parish

内容引用:
> 你
> 一会看我
> 一会看云
> 我觉得
> 你看我时很远
> 你看云时很近

分割线:
`-`加上空格组成,三个以上
- - - - - -

代码高亮:
使用3个连续的`,将代码内容包起来
` ` `
s = 'hello world'
` ` `

选择适合自已的编辑器

MarkDown不需要专门工具就可以编写,任何纯文本编辑器都可以胜任。但选择一款适合自已的工具可以提供代码高亮,实时预览,自定义主题,PDF导出等高级功能,极大的提升我们的写作体验。
以下列出一些比较热门的工具:

全平台:
Atom 不用过多介绍了,GitHub出品,被称为是新时代的文本编辑器,Emacs的精神继承者。通过非富的插件,很好的支持MarkDown,并提供了无限的可能性。
Sublime Text 3 更不用介绍了,同样是利用插件支持。
MarkEditor 商业软件,使用Electron写的跨平台应用。功能强大,缺点是启动速度较慢。

Mac平台:
Mweb 商业软件,作者是国人。
Mou 商业软件,Mac下老牌的编辑器了。
MacDown 功能与Mou基本相同,免费使用。
Typora 所见即所得的编辑器,输入MarkDown标记后会立刻渲染出结果。

Win平台:
MarkdownPad Win下的工具比较少,这是个不错的。

目前笔者常用的是Atom,跨平台,可以很好在工作在macOS和Ubuntu上。另外因为是通用型的编辑器,且插件非富,不仅限于写MarkDown,其它工作也可以胜任。
上面提到的工具各有特色,可以都试用一下再选一款适合自已的。

使用七牛云保存图片

因为MarkDown是纯文本格式,所以决定了它不可能像Microsoft Word那样内嵌图片。
虽然在平时使用MarkDown做个人知识记录的过程中,是极少使用图片的。但如果要撰写给别人看的博客、教程一类的文档,有一些图片总是好的。
MarkDown可以使用本地图片,也可以使用URL远程图片。

不推荐使用本地图片,虽然图片保存在本地比扔在图床上更”安全”,丢失的风险更小,但本地散乱图片会增加我们文件夹的容量,增加文档管理的难度。而且包含本地图片的文章要发布到博客上时,通常需要一张张手工上传图片,费时费力。
所以选择一个靠谱的图床,将图片上传后,以URL的方式嵌入MarkDown是最佳选择。鉴于国内的网络环境,以及前一段时间各大云存储厂商的所做所为,国内靠谱的图床确实不多,这里只推荐 七牛云
七牛云的CEO是许式伟,曾是金山WPS2005的首席架构师,大神级的人物,所以七牛云的技术方面我们不用过多担心。重度用户可以适当付费支持一下,让七牛云良性的发展下去。
七牛云不仅仅是图片存储这么简单,具体的特性不一一介绍了,注册后可以慢慢在官网上看文档。

使用iPic快速上传图片

解决了图床的问题,再来看一下如何使用工具快速将图片上传,并获取生成的URL。
因为每插入一张图片,都要找开网站,点击上传按钮,找到存在本地磁盘上的图片,点击确定,成功后然后再把URL复制下来,实在是很繁琐。
这里推荐一个Mac下的小工具 iPic ,下面是一个演示动画:
https://ww2.sinaimg.cn/large/006tNc79gw1fah02zweq2g30j60as7wh.gif
除了拖拽,还有更快捷的操作方式,只要选中本地图片或者复制网页中的图片,然后按下Command+U快捷键就可自动上传并获取URL。

使用坚果云保存文档并同步

MarkDown文档写好了,保存在本地。一切看上去都很美好,但接下来,我们又有了更多的需求:

  1. 重要的文档,特别是我们辛辛苦苦多年积累下来的知识,只保存在本地是不安全的。不要把鸡蛋放在同一个篮子里,要把它备份到云端。
  2. 要实现多设备间的同步,这样在手机上、iPad上、家里的电脑上可以随时查阅、修改。

坚果云 是国内一款非常类似于DropBox的云存储应用,它提供了 全平台 的非常 快速稳定增量同步 功能。
坚果云可以说是国内硕果仅存的一家了,它的增量同步在国内做的是最好的,与百度云等不同,它专注于”同步”,而不是电影等大文件的存储。
坚果云的客户端做的也是非常用心,Linux下可以全功能完美使用。手机和iPad上的APP做的也不错。希望坚果云好好存活下去。
坚果云的使用非常简单,这里有一个视频教程:
视频教程链接

为什么不直接使用云笔记

目前市面上有非常多的跨平台的云笔记工具可以做到上面的事情,而且可能做的更好。 例如为知笔记、有道云笔记、EverNote、EssentialPIM等,另外还有一些后起之秀。
为什么还要使用 MarkDown + 坚果云 这样看上去略显繁琐的组合?

  • 我需要用纯文的方式来管理知识
    MarkDown相对来说比较”单纯”,而且又有表达”样式”的能力。举个例子,就像我们写代码,当然是要保存”源码”,而不是编译好的”二进制文件”。
    我们对纯文本有绝对的控制能力,纯文本方便检索,更重要的是方便日后的加工,可以导出PDF、HTML,可以分享发布到博客和专栏。
  • 我需要一个能随时替换掉的云服务
    云服务并不可靠,目前大部分国外的服务已经被封锁,国内的因为没有良好的盈利模式,也纷纷关闭,所以不要重度依赖云。既然MarkDown文档首先保存在本地,掌握在自已手中,这也就意味着我可以使用任何云同步服务,不仅仅是坚果云,还可以是OneDrive、DropBox、iCloud,甚至是自已在VPS上搭建的私有Git。
  • 我为什么抛弃了云笔记软件
    之前使用过的工具中,为知笔记是我最喜爱的(个人认为比EverNote做的要更好)。但它也是有一些缺点的,例如虽然支持MarkDown编写,但本质上还是会保存成HTML富文本。另外它的Mac版做的不如Win版优秀,Linux版也不友好。
    另外,使用第三方的工具,所有数据保存在云端,在本地没有原始档的备份,会让人有一种深深的”不安感”。再加上很多工具不提供”导出”功能,这意味着我们一旦使用某个工具,就要一直用下去,谁也无法保证5年后这个厂家还能不能存活下来,继续为我们提供服务。
  • 个人知识管理,不是简单的将文档编写并保存
    接下来,我们会对它进行搜索,加工,导出,分享,而这正是MarkDown的优势所在。

基于以上几点,我最终选择了使用 MarkDown + 坚果云 这样的组合进行个人知识管理,并准备长期使用下去。

Git简介

什么是版本控制

版本控制系统(Version Control System,简称VCS)是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。

按类型可以分为:

  • 本地版本控制系统

    例如RCS(至少我是从来没有用过)
    本地版本控制系统解决了版本的管理问题,再也不用时不时的把工程目录,通过手工拷贝的方式来存档了。但本地版本控制系统的缺点是,无法解决多人协作的问题。

  • 集中化的版本控制系统

    例如CVS,SVN等(公司中SVN应该用的比较多)
    有一个集中管理的服务器,所有开发人员通过客户端连到这台服务器,取出最新的 文件 或者提交更新。管理员可以掌控每个开发者的权限。
    集中化的VCS不但解决了版本控制问题,还可以多人协作。但缺点也是有的,就是太依赖于远程服务器,CVS服务器宕机后,会影响所有人的工作。版本记录只保存在一台服务器上,会有数据丢失风险。

  • 分布式版本控制系统

    例如Git
    客户端并不只提取最新版本的文件,而是把 代码仓库 完整地镜像下来。每一次的提取操作,实际上都是一次对 代码仓库 的完整备份。
    所以并没有”中心服务器”的概念,所谓的”Git服务器”,也同每个人的电脑一样,只是为了多人协作时,方便大家交换数据而已。

什么是Git

Git是目前世界上最先进的分布式版本控制系统(没有之一)
好不好用,看看它的开发者是谁就知道了:Linux之父 Linus Torvalds

小历史: Linux内核社区原本使用的是名为BitKeeper的商业化版本控制工具,2005年,因为社区内有人试图破解BitKeeper的协议,BitMover公司收回了免费使用BitKeeper的权力。
Linus原本可以出面道个歉,继续使用BitKeeper,然而并没有。。。Linus大神仅用了两周时间,自已用C写了一个分布式版本控制系统,于是Git诞生了!

为什么要使用Git

为什么要使用Git,或者说Git相比SVN有什么优势呢?

  • 分布式

  • 分支管理

  • GitHub

安装Git

  • 大多数Linux发行版已经预装了Git,系统默认自带,如果不带。。可以源码make安装或使用yum/apt等直接安装,过程不赘述了。
  • macOS下,安装Xcode后,它的CLI工具里应该会包含Git了。或者使用brew手工安装一下。
  • Windows下,可以直接下载安装 msysGit 。 或者如果你的机器上已经有Cygwin,也可以直接用在它下面安装Git。
  • 图形工具推荐使用 SourceTree,查看分支非常直观 。IntelliJ IDEA等IDE也会自带一些图形化的工具,在合并代码时很高效。

学习路径

  • 首先,忘掉SVN/CVS,不要把Git的各种操作与它们做类比,切记。
  • 刚开始不要依赖图形客户端。首先应该将精力用在理解原理上 -> 然后掌握一些基本CLI命令,动手操作实践 -> 最后在实际工作中使用GUI工具以提高效率。
  • 重度Windows用户使用Git时,与平时熟悉GUI的环境会有些违和感,毕竟Git是Linux下的产物,Git遵循Linux的哲学,Simple,简单直接,但Simple并不等于Easy。需要转换一下思维。

了解Git的工作原理

记录文件整体快照

Git和其他版本控制系统的主要差别在于,Git只关心文件数据的 整体 是否发生变化,而大多数其他系统则只关心 文件内容 的具体差异。

SVN在每个版本中,以单一文件为单位,记录各个文件的差异:

Git在每个版本中,以当时的全部文件为单位,记录一个快照:

大多数操作都在本地执行

Git的绝大多数操作都只需要访问本地文件和资源,不用连网。因为你的本机上,就已经是完整的代码库了。这样一来,在无法连接公司内网的环境中,也可以愉快的写代码了。
例如,如果想看当前版本的文件和一个月前的版本之间有何差异,Git会取出一个月前的快照和当前文件作一次差异运算,而不用每次都请求远程服务器。

时刻保持数据完整性

在保存到Git之前,所有数据都要进行内容的校验和(checksum)计算,并将此结果作为数据的唯一标识和索引。
这项特性作为Git的设计哲学,建在整体架构的最底层。所以如果文件在传输时变得不完整,或者磁盘损坏导致文件数据缺失,Git都能立即察觉。
Git使用SHA-1算法计算数据的校验和,通过对文件的内容或目录的结构计算出一个SHA-1哈希值,作为指纹字符串。该字串由40个十六进制字符组成,看起来就像是:
24b9da6552252987aa493b52f8696cd6d3b00373
Git的工作完全依赖于这类指纹字串,所以你会经常看到这样的哈希值。实际上,所有保存在 Git数据库中的东西都是用此哈希值来作索引的,而不是靠文件名。

多数操作仅添加数据

常用的Git操作大多仅仅是把数据添加到数据库,很难让Git执行任何不可逆操作。在Git中一旦提交快照之后就完全不用担心丢失数据,特别是养成定期推送到其他仓库的习惯的话。

文件的三种状态

对于任何一个文件,在 Git 内都只有三种状态:已提交(committed) 已修改(modified) 已暂存(staged)
已提交表示该文件已经被安全地保存在本地数据库中了;
已修改表示修改了某个文件,但还没有提交保存;
已暂存表示把已修改的文件放在下次提交时要保存的清单中。

由此我们看到 Git 管理项目时,文件流转的三个工作区域:Git 的工作目录,暂存区域,以及本地仓库。

每个项目都有一个名为.git的目录,它是 Git用来保存元数据和对象数据库的地方。该目录非常重要,每次克隆镜像仓库的时候,实际拷贝的就是这个目录里面的数据。
从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作目录。这些文件实际上都是从Git目录中的压缩对象数据库中提取出来的,接下来就可以在工作目录中对这些文件进行编辑。
所谓的暂存区域只不过是个简单的文件,一般都放在 Git 目录中。有时候人们会把这个文件叫做索引文件,不过标准说法还是叫暂存区域。

基本的 Git 工作流程如下:

  1. 在工作目录中修改某些文件。
  2. 对修改后的文件进行快照,然后保存到暂存区域。
  3. 提交更新,将保存在暂存区域的文件快照永久转储到 Git 目录中。

所以,我们可以从文件所处的位置来判断状态:如果是Git目录中保存着的特定版本文件,就属于已提交状态;如果作了修改并已放入暂存区域,就属于已暂存状态;如果自上次取出后,作了修改但还没有放到暂存区域,就是已修改状态。

创建版本库

有两种取得Git项目仓库的方法。第一种是在现存的目录下,通过导入所有文件来创建新的Git仓库。 第二种是从已有的Git仓库克隆出一个新的镜像仓库来。

在目录中创建新仓库

如果一个目录还没有使用Git进行管理,只需到此项目所在的目录,执行git init,初始化后,在当前目录下会出现一个名为.git的目录

1
2
3
$ mkdir learngit
$ cd learngit
$ git init

从已有的仓库克隆

如果Git项目已经存在,可以使用git clone从远程服务器上复制一份出来,Git支持多种协议:

1
2
3
$ git clone mobgit@134.32.51.60:learngit.git  #使用SSH传输协议
$ git clone git://134.32.51.60/learngit.git #使用Git传输协议
$ git clone https://134.32.51.60/learngit.git #使用HTTPS传输协议

版本库基本操作

检查当前文件状态

使用git status命令可以查看文件的状态

1
2
3
4
$ git status
On branch master
Initial commit
nothing to commit (create/copy files and use "git add" to track)

出现如上的提示,说明现在的工作目录相当干净,所有已跟踪文件在上次提交后都未被更改过。

现在我们做一些改动,添加一个readme.txt进去,然后再看一下状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat>readme.txt
hello git
^C

git status
On branch master

Initial commit

Untracked files:
(use "git add <file>..." to include in what will be committed)
readme.txt

nothing added to commit but untracked files present (use "git add" to track)

Untracked files显示了这个新创建的readme.txt处于未跟跟踪状态

跟踪新文件

使用git add命令开始跟踪一个新文件

1
2
3
4
5
6
7
8
9
$ git status
On branch master

Initial commit

Changes to be committed:
(use "git rm --cached <file>..." to unstage)

new file: readme.txt


readme.txt已 被跟踪 ,并处于 暂存状态

将本次修改暂存

现在我们再对readme.txt进行修改,添加一行,再执行git status查看状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ git status
On branch master

Initial commit

Changes to be committed:
(use "git rm --cached <file>..." to unstage)

new file: readme.txt

Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: readme.txt

可以看到readme.txt 不仅出现在了Changes to be committed,还出现在了Changes not staged for commit
由此可见,Git关心的是 Changes ,而不是文件本身。
再次执行git add,可以将 本次修改 提交到暂存区,Changes not staged for commit提示消失

提交更新

使用git commit命令将暂存区中的内容提交至版本库,工作区又是干净的了

1
2
3
4
5
6
7
8
9
10
$ git commit -m "my first commit"
[master (root-commit) 6c8912a] my first commit
1 file changed, 2 insertions(+)
create mode 100644 readme.txt

$ git status
On branch master
Your branch is based on 'origin/master', but the upstream is gone.
(use "git branch --unset-upstream" to fixup)
nothing to commit, working tree clean

注意:一定要使用-m参数加入注释,认真描述本次的提交具体做了些什么,这对于以后我们查询历史记录非常重要。

如果觉得使用暂存区过于繁琐,可以在commit时直接使用-a参数,Git就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过git add步骤。

1
$ git commit -a -m "my first commit"

查看历史

使用git log命令可以查看历史记录

1
2
3
4
5
6
7
8
9
10
11
12
$ git log
commit 43c5d337ffdd76f33ce5f5f90103d57e55474956
Author: BlueXIII <bluexiii@163.com>
Date: Thu Dec 8 14:45:59 2016 +0800

this is my second commit

commit 6c8912ad2a8e90a7ba32cc8578fd0069a205221b
Author: BlueXIII <bluexiii@163.com>
Date: Thu Dec 8 14:38:09 2016 +0800

my first commit

可以看到,每次更新都有一个SHA-1校验和、作者的名字和电子邮件地址、提交时间、提交说明。

撤消操作

撤消操作在这里这里不做重点描述了,只列出几个常用命令。
修改最后一次提交:
git commit –amend
取消已经暂存的文件:
git reset HEAD readme.txt
取消对文件的修改:
git checkout – readme.txt

远程仓库

之前介绍了在本地仓库的一些操作。但当与他人协作开发某个项目时,需要至少使用一个远程仓库,以便推送或拉取数据,分享各自的工作进展。

克隆远程库

之前已经在讲新建仓库时已经提到,如何克隆远程库,这里再重复列一遍:

1
2
3
$ git clone mobgit@134.32.51.60:learngit.git  #使用SSH传输协议
$ git clone git://134.32.51.60/learngit.git #使用Git传输协议
$ git clone https://134.32.51.60/learngit.git #使用HTTPS传输协议

查看绑定的远程库

如果之前我们使用的git clone命令直接克隆了一个远程仓库到本机,Git就已经默认绑定了一个名为origin的远程库。当然我们还可以手工绑定其它远程库,远程仓库可以有多个。
使用git remote -v命令列出我们绑定了哪些远程库:

1
2
3
$ git remote -v
origin mobgit@134.32.51.60:learngit.git (fetch)
origin mobgit@134.32.51.60:learngit.git (push)

接下来还可以使用git remote show origin来查看这个名为origin的远程库的更详细的信息,这里先不细讲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ git remote show origin
* remote origin
Fetch URL: mobgit@134.32.51.60:learngit.git
Push URL: mobgit@134.32.51.60:learngit.git
HEAD branch (remote HEAD is ambiguous, may be one of the following):
dev
master
serverfix
serverfix2
Remote branches:
dev tracked
master tracked
serverfix tracked
serverfix2 tracked
Local branches configured for 'git pull':
dev merges with remote dev
master merges with remote master
serverfix merges with remote serverfix
serverfix2 merges with remote serverfix2
Local refs configured for 'git push':
dev pushes to dev (up to date)
master pushes to master (up to date)
serverfix pushes to serverfix (up to date)
serverfix2 pushes to serverfix2 (up to date)

手工添加一个远程仓库

我们先让管理员新建一个名为learngit2的远程仓库,再使用remote add命令将它添加进来,取名为repo2

1
2
3
4
5
6
7
$ git remote add repo2 mobgit@134.32.51.60:learngit2.git

$ git remote -v
origin mobgit@134.32.51.60:learngit.git (fetch)
origin mobgit@134.32.51.60:learngit.git (push)
repo2 mobgit@134.32.51.60:learngit2.git (fetch)
repo2 mobgit@134.32.51.60:learngit2.git (push)

现在我们有origin和repo2两个远程仓库了

从远程仓库抓取数据

使用git fetch [remote-name]从远程仓库抓取数据,注意fetch命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支(关于分支稍后讲解)
例如要抓取名为origin远程仓库:

1
$ git fetch origin

推送数据到远程仓库

使用git push [remote-name] [branch-name]将本机的工作成果推送到远程仓库
例如要将本地的master分支推送到origin远程仓库上:

1
$ git push origin master

分支

也许到之前为止,大家会觉得Git和Svn除了实现原理不同以及实现了分布式之外,在日常使用上并没有什么太大的区别(甚至更繁琐)。但接下来的分支,才是Git的精髓部分。

为什么要使用分支

举个简单的例子:假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
于是你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
相比于Svn等工具,Git创建、切换分支的开销是非常小的,Git鼓励 频繁使用分支

分支的原理

要理解分支,需要继续深入一下Git的工作原理

Git如何储存数据

在Git中提交时,会保存一个提交对象(commit object),该对象包含一个指向暂存内容快照的指针,并同时包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针(首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先)。

假设在工作目录中有三个文件已经 修改 过,准备将它们暂存后提交。
git add暂存操作时,会对 每一个文件 计算校验和,然后把当前版本的文件快照使用 blog对象 保存到Git仓库中(为提高性能,若文件没有变化,Git不会再次保存)。将它们的SHA-1校验和加入到暂存区域等待提交。
git commit提交操作,时,Git首先会计算 每一个子目录 的校验和,然后将这些校验和保存为 tree对象 。 然后Git会创建一个 commit对象 ,它包含指向这个树对象的指针及注释、提交人、邮箱等信息。
现在,Git仓库中有五个对象:三个blob 对象(保存着文件快照);一个树对象(记录着目录结构和blob对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。

单个提交对象在仓库中的数据结构:

多个提交对象之间的链接关系:

分支是什么

Git 中的分支,其实本质上仅仅是个指向commit对象的可变指针。Git会使用master作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次commit对象的master分支。它在每次提交的时候都会自动向前移动。

创建名为testing的新的分支,本质上就是创建一个指针,可以使用git branch命令:

1
$ git branch testing

当前工作在哪个分支

Git 是如何知道你当前在哪个分支上工作的呢?其实答案也很简单,它还保存着一个名为HEAD的特别指针。它是一个指向你正在工作中的本地分支的指针。

切换分支时发生了什么

切换分支,本质上就是移动HEAD指针。
要切换到其他分支,可以执行git checkout命令。我们现在转换到刚才新建的testing分支:

1
$ git checkout testing

分支切换的实际操作

为了更好的理解分支,我们接下来模拟实际工作中的场景,进行一系列的切换操作。
现在我们已经处于testing分支了,目前testing分支和master分支都是指向同一个commit,所以我们的工作区的内容现在还没有什么变化。
现在,我们要在testing分支上做一些文件修改,然后commit:

1
2
echo "testing branch">>readme.txt
git commit -a -m "modify on testing branch"


提交后,产生了一个新的commit对象,并且HEAD随着当前testing分支一起向前移动。而master分支则是停在原地不动。

我们可以试着使用git checkout命令切回master分支,看看发生了什么:

1
$ git checkout master


这条命令做了两件事:

  1. 它把HEAD指针移回到 master 分支。
  2. 把工作目录中的文件换成了master分支所指向的快照内容。

我们试着在master上再做一些改动并commit:

1
2
echo "testing master">>readme.txt
git commit -a -m "modify on master branch"


现在分支变成了上图所示,我们可以在master与testing间随时切换,并修改工作区的文件内容。必要时再将这两个分支合并。

分支新建与合并的实际操作

接下来,再以一个比较长的真实的工作场景进行举例

我们首先在master分支上进行工作,并提交了几次更新,测试无误后编译发布至生产系统。

之后我们决定要修补问题追踪系统上的53号问题,这时可以使用git checkout -b命令快速创建一个分支并切换过去:

1
$ git checkout -b iss53

这相当于执行了下面这两条命令:

1
2
$ git branch iss53
$ git checkout iss53

我们在iss53分支上写了一些代码,并commit

1
2
$ vi index.html
$ git commit -a -m 'fixed the broken email address'

iss53上的工作还没完成,突然接到通知,生产系统有一个紧急BUG需要立刻修复。所以我们首先切回master分支,然后在master的基础上,又新建出一个hotfix分支来修复BUG。

1
2
3
4
$ git checkout master    #回到master分支
$ git checkout -b hotfix #新建一个hotfix分支,并切过去
$ vim index.html #修改一些东西,修复BUG
$ git commit -a -m 'fixed the broken email address' #提交hotfix

在hotfix分支上搞定BUG之后,我们切回master分支,使用git merge把刚才的hotfix合并进来

1
2
3
4
5
6
$ git checkout master  #切换回master分支
$ git merge hotfix #将hotfix分支的修改,合并到当前master分支来(注意merge的方向,是从其它分支,合到当前分支)。
Updating f42c576..3a0874c
Fast-forward
README | 1 -
1 file changed, 1 deletion(-)


备注:本次合并时出现了“Fast forward”的提示。由于当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把 master 分支指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。

这时hotfix分支已经没用了,可以删掉了

1
$ git branch -d hotfix    #只是删除了一个指针

现在回到之前未完成的53号问题上,继续写一些代码

1
2
3
$ git checkout iss53
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'

在问题53相关的工作完成之后,可以合并回master分支。实际操作同前面合并hotfix分支差不多,只需回到master分支,运行git merge命令指定要合并进来的分支。

1
2
3
4
5
6
7
$ git checkout master
$ git merge iss53
Auto-merging README
Merge made by the 'recursive' strategy.
README | 1 +
1 file changed, 1 insertion(+)


请注意,这次合并操作的底层实现,并不同于之前 hotfix 的并入方式。因为这次你的开发历史是从更早的地方开始分叉的。由于当前 master 分支所指向的提交对象(C4)并不是 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。


这次,Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。

有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,需要手工来处理冲突。

1
2
3
4
5
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Git作了合并,但没有提交,它会停下来等你解决冲突。

1
2
3
4
5
6
7
8
9
10
11
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")

Unmerged paths:
(use "git add <file>..." to mark resolution)

both modified: index.html

no changes added to commit (use "git add" and/or "git commit -a")

任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记。

1
2
3
4
5
6
7
8
$ vi index.html
<<<<<<< HEAD
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53

可以看到 ======= 隔开的上半部分是 HEAD,即master,下半部分是在iss53分支中的内容。
手工合并代码后,把 <<<<<<<,======= 和 >>>>>>> 这些行也一并删除。这时可以用git commit来提交了。

分支策略

实际开发中,对于分支的管理,已经有很多最佳实践,大多数情况下,我们只需要遵守一些基本原则:

  • 首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面工作。
  • 平时的开发工作都放在dev分支上,也就是说,dev分支是不稳定的。到某个时候,比如测试通过,需要1.2版本发布时,再把dev分支合并到master上,在master分支编译发布1.2版本。
  • 针对新需求、修复等具体的任务,每次都在dev分支上开一个新的任务分支出来,工作完成后,再向dev分支上合并就可以了。名称没有特别的规范,可以是人名,例如:zhangsan,也可以是任务名、需求编号等,例如:iss03、feature04、hotfix。

远程分支

之前讨论过远程仓库,接着又学习了分支,当二者结合到一起时,又会产生一些有趣的东西。

远程分支的概念

远程分支(remote branch),即远程仓库中的分支。同步到本地后,与本地分支不同的是,它们 无法移动 ;且只有在Git进行网络交互时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。我们用 (远程仓库名)/(分支名) 这样的形式表示远程分支(例如origin/master)。

如果我们在本地master分支做了些改动,与此同时,其他人向远程仓库推送了他们的更新,那么服务器上的master分支就会向前推进,而于此同时,我们在本地的提交历史正朝向不同方向发展。(不过只要你不和服务器通讯,你的 origin/master 指针仍然保持原位不会移动。)

可以运行git fetch origin来同步远程服务器上的数据到本地。该命令首先找到origin是哪个服务器,然后从上面获取你尚未拥有的数据,更新你本地的数据库,然后把origin/master的指针移到它最新的位置上。

可以使用git remote命令查看远程仓库的详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ git remote -v    #列出远程服务器清单
origin mobgit@134.32.51.60:learngit.git (fetch)
origin mobgit@134.32.51.60:learngit.git (push)

$ git remote show origin #查询某一个远程服务器的详情
* remote origin
Fetch URL: mobgit@134.32.51.60:learngit.git
Push URL: mobgit@134.32.51.60:learngit.git
HEAD branch (remote HEAD is ambiguous, may be one of the following):
dev
master
Remote branches:
dev tracked
master tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)

跟踪远程分支

从远程分支checkout出来的本地分支,称为跟踪分支 (tracking branch)。跟踪分支是一种和某个远程分支有直接联系的本地分支。
在跟踪分支里输入 git push,Git 会自行推断应该向哪个服务器的哪个分支推送数据。同样,在这些分支里运行 git pull 会获取所有远程索引,并把它们的数据都合并到本地分支中来。

在克隆仓库时,Git 通常会自动创建一个名为 master 的分支来跟踪 origin/master。这正是 git push 和 git pull 一开始就能正常工作的原因。

1
2
3
$ git checkout -b serverfix origin/serverfix
或简化为:
$ git checkout --track origin/serverfix

这会新建并切换到serverfix本地分支,其内容同远程分支origin/serverfix一致。

推送本地分支

要想和其他人分享某个本地分支,你需要把它推送到一个你拥有写权限的远程仓库。
例如本地有一个serverfix分支需要和他人一起开发,可以运行 git push (远程仓库名) (分支名):

1
2
3
4
5
6
7
$ git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To git@github.com:schacon/simplegit.git
* [new branch] serverfix -> serverfix

或者加入–set-upstream设置跟踪后,以后直接使用git push就可以推送了:

1
2
git push --set-upstream origin serverfix

GitHub

[GitHub](https://github.com)是一个面向开源及私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub。

GitHub本身没有什么好学的,随便看就知道怎么用了 知乎:怎样使用GitHub
重点是,GitHub上有非常多优秀的个人项目值得我们学习,我们也可以将自已的代码发布上去。可以看成是程序员的博客吧,只贴代码,不废话。
在GitHub上发布开源项目是免费的,但是私有项目收费。

GitLab

GitLab是一个用Ruby on Rails写的开源的版本管理系统,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目。它拥有与Github类似的功能,能够浏览源代码,管理缺陷和注释。
可以管理团队对仓库的访问,它非常易于浏览提交过的版本并提供一个文件历史库。团队成员可以利用内置的简单聊天程序(Wall)进行交流。它还提供一个代码片段收集功能可以轻松实现代码复用,便于日后有需要的时候进行查找。
GitLab是目前搭建内部Git服务器的首选,当然如果要求不高的话,我们也可以直接使用SSH协议来快速搭建Git服务端。

常用Git命令清单


更多内容请直接参考 阮一峰的网络日志

推荐文档

不要指忘2小时的培训能带来多大的收益,最简单高效的方式,还是要多看优秀的文档。
本文大量参(chao)考(xi)了以下两部文档:
廖雪峰的在线教程 适合快速上手
Pro Git中文版 中文第一版

引言

起因是某个同事接到了领导安排下来的一个需求,要在一个Web应用(Java+Tomcat)中,记录用户登录时的IP地址和MAC地址,用于安全审计,于是咨询我如何实现。

第一反应是,这个需求本身是不成立的,根据以往的了解,MAC地址应该是过不了路由器的才对。
以往做开发,都是用engineer的思维:先动手做,遇到问题再解决问题。但这个需求,应当用scientist的思维去思考:首先确定能不能做,然后才是怎么做。

翻查了一些资料,想来证实”为什么WEB服务器,可以获取到客户端的IP地址,但获取不到MAC地址“,看着看着才发现,这是个挺大的命题,够写一篇BLOG了。

PS:由于个人对这块内容了解的不够彻底,本文很可能会有谬误,请读者先不要太当真,另外希望平台组的同事给予指证。

先说结论

我所认为的结论应该是这样的:

  1. 获取远程主机的IP地址是可行的(先不讨论使用代理的情况)
  2. 本地网络下,当我们已知一个IPv4地址后,可以通过ARP请求,获取对应的MAC地址。换句话说,MAC地址,只在本地网络下才有意义。
  3. 但我们无法透过路由器,获取其它网络下的IPv4节点的MAC地址。

下面一步步解释一下。

HTTP

先从HTTP说起。
HTTP是一个应用层的协议,它建立在TCP协议之上。
HTTP请求就是用来发送一段文本。关于这段文本如何组织,第一行写什么,第二行写什么,哪里加一个空行,就是HTTP协议所要规范的内容。
举个直接的例子,下面是一个简单的HTTP GET请求,有兴趣可以用telnet模拟一下。

1
2
3
4
5
6
7
8
GET http://www.fiddler2.com/UpdateCheck.aspx?isBeta=False HTTP/1.1
User-Agent: Fiddler/4.6.3.50306 (.NET 4.0; WinNT 5.1.2600 SP3; zh-CN; 2xx86; Auto Update; Full Instance; Extensions: Geoedge, AutoSaveExt, HostsFile, SAZClipboardFactory, EventLog, SimpleFilter, Timeline, APITesting)
Pragma: no-cache
Host: www.fiddler2.com
Accept-Language: zh-CN
Referer: http://fiddler2.com/client/4.6.3.50306
Accept-Encoding: gzip, deflate
Connection: close

我们可以看到,HTTP的这段请求中,完全找不到客户端的MAC地址,甚至连IP地址都没有描述。
那IP地址是从哪里取到的呢?接下来我们再深入一点,看下一个内容:Socket

Socket

HTTP的客户端和服务端,是通过Socket进行连接的。

Socket是什么呢?Socket是对OSI模型第4层-传输层中的TCP/IP协议的封装。Socket本身并不是协议,而是一个调用接口(API)。Socket和TCP/IP协议没有必然的联系。但通过Socket,我们才能使用TCP/IP协议。应用层不必了解TCP/IP协议细节,直接通过对Socket接口函数的调用完成数据在IP网络的传输。

Socket包含了网络通信必须的五种信息:连接使用的协议,本地主机的IP地址,本地进程的协议端口,远地主机的IP地址,远地进程的协议端口

所以,因为有了Socket,客户端和服务端完全不需要了解底层细节,直接通过调用Socket来实现就可以了。

这也就是为什么服务器端可以获取到客户端的IP地址的原因,因为Socket中包含了远地主机的IP地址。(当然,通过代理服务器进行访问的除外,这种要依靠HTTP协议的X-Forwarded-For头来确认IP,不在本次的讨论范围中)

那为什么无法获取到客户端的MAC地址呢?很简单,同理,因为Socket中无法取到MAC地址。。。

如果继续发问,为什么Socket中都既然都包含IP地址了,为什么偏偏不包含MAC地址信息呢?看来我们还要更深入一点,看一下OSI模型吧。

OSI七层模型

首先祭出这张经典的OSI七层模型图,计算机网络的基石,请先盯着看一会儿,认真复习一下

这里还有一张OSI七层模型与TCP/IP四层模型的对照图

为了方便理解,再放上一张更直观的,每一层对应的数据型式和主要协议的示意图

通过上图大体可以知道:

  • MAC地址是记录在第2层-数据链路层的
  • IP地址是记录在第3层-网络层的
  • 端口号(TCP/UDP)是位于于第4层-传输层的
  • HTTP请求报文是记录在第7层-应用层的

当打开一个URL时,究竟发生了什么

下面举个栗子,当我们在浏览器中打开一个链接后,看看OSI各层倒底发生了什么:
这里撇开DNS解析之类东西,只说一下HTTP报文的发送

1、发送端

首先来看一下发送端(浏览器所在的主机)。参照第一张OSI模型图,按照从上向下的顺序来看。应用层数据其实只有那么几行文本,然后往下,每过一层,都要被加上首部/尾部。这个过程就像是一层一层的穿衣服

HTTP请求文本:

最后到了数据链路层之后,数据就变成了这个肥肥的样子,最后转换成0和1的电信号发出:


下面看看,现实中,每一层都做了些什么(现实中的分层和OSI模型还是有区别的):

  • 第7,6,5层,也就是OSI中的应用层,表示层,会话层(也是TCP/IP分层中的应用层),它创建了一个HTTP请求(例如 GET / HTTP/1.1),并交给下一层。一个普通的HTTP GET请求就是几行纯文本。
    处理这三层,是浏览器WEB服务器所要做的工作,浏览器发出请求,WEB服务器做出响应。

  • 第4,3层,也就是传输层/网络层。TCP/IP栈将上层的数据分成包(packets),并将它送往下一层-数据链路层。

IP地址、端口号记录在这两层中
处理这两层,是操作系统要做的工作,操作系统将这两层封装为了Socket,方便调用。

  • 第2层,数据链路层,将包(package)封装成帧(frame),并将它送到下一层物理层。

MAC地址记录在这一层中
这一层的工作,交由网卡来处理。

  • 第1层,物理层,使用电信号来传送0和1。
    最后这一层就是传输介质的工作了,例如双绞线、光纤、同轴电缆。

2、数据流转

数据发出去后,再看一下数据在网络上的流转。
数据一般要经过交换机、路由器等网络设备,层层转发,这些设备所做的事情就像是: 脱掉一件或几件衣服,做一些修修补补,然后再重新穿回去

  • 这里先以L2交换机为例看一下,因为L2交换机会认别到帧这一层,记录/学习MAC地址,并将帧发送到目的地。

通过上面这张图,我们就可以理解,MAC地址在本地网络下的重要作用了。也理解了,本地网络下,是可以查出每个节点的MAC地址的。

  • 下面,再来看一下路由器。当一个LAN希望连接到另一个LAN时,就必须使用路由器设备了。当然,可以通过构建超大型的LAN,来避免使用路由器,但这时,交换机就需要管理大量的MAC地址,同时进行大量的广播通信,设备的负担就会相当大。路由器会进行路由选择,让数据达到下一跳。

经过路由器后,为了能到达下一跳,数据链路层中的MAC地址就被篡改了,下面这张图很能说明问题:

3、接收端

最后看一下接收端(WEB服务器所在的主机)。参照第一张OSI模型图,按照从下至上的顺序来看,它要做的事情是: 将衣服一件一件全部脱掉 ,最后WEB服务器就取到了最初的应用层数据。

4、结论

所以,当一个以太网帧到达目的主机后,其中的MAC地址早已经不是原来客户端的MAC了,操作系统的Socket自然也无法获取原始的MAC地址了。

可不可以通过其它方式获取MAC地址

上面已经证明了,WEB服务端,是无法获取客户端的MAC地址的。
那么,能不能通过一些trick来绕道实现呢?
想了想,大概可以有如下的思路:

  1. 通过JavaScrip获取客户端的某块网卡的MAC地址,最好还要同时获取路由表,挑选出走默认路由的那块网卡。
  2. 将MAC地址,偷偷写入应用层的HTTP协议的Header中(类似Content-Type和User-Agent)
  3. 服务端直接读Header,就可以取到Mac地址了

那么这个思路可不可行呢?

  • 如果客户端是浏览器的话,首先第一步就会被打脸,我想也没有哪个浏览器会允许这种不安全的操作的。但如果硬要通过IE下的ActiveX来取的话,貌似也不是不可以。。。
  • 但如果客户端是APP的话,通过WebView+JS Bridge方式调用私有的API,应该也是可行的。。。

记录MAC地址的意义

最后的最后,不禁思考,获取MAC的意义在哪里呢?
如果单纯是为了取证和审计,我想意义是不大的,甚至不如直接记录IP地址。
因为:

  • 对于普通的正常用户而言,通过IP地址,是可以直接定位操作人的,即使IP是动态的。
  • 对于初级以上Hacker来说,伪造MAC地址和伪造IP地址一样简单,甚至更简单。

所以,一般的安全管控要求下,还是只记录IP吧。

参考资料

什么是IntelliJ IDEA


简单来说,IntelliJ Idea是目前最好的Java集成开发环境,没有之一。

为什么要迁移到Intellij IDEA

Java应该是非常依赖IDE的一门语言了,恐怕也没有几个大牛能够直接用Vim/Emacs愉快的手撸代码,Java程序员离开了IDE基本上生活不能自理,IDE的重要性不言而喻。
最近在推行、部署各种自动化工具,但个人认为,相比于各种工具,一个好的IDE更是能 极大的提升 工作效率以及编码的爽快感。
目前团队中使用的开发工具主要还是Eclipse和NetBeans,所以就想借这篇文章安利一下,希望团队中更多的人能开始尝试并喜欢上IntelliJ IDEA。

目前常用的IDE主要有3个:

  • Eclipse
  • NetBeans
  • IntelliJ IDEA

其中,Eclipse及其衍生品是最为我们所熟知的,Eclipse开源,历史悠久,插件丰富,对一些老项目兼容性也比较好,另外有很多基于Eclipse的项目也很流行,比如STS、MyEclipse。

NetBeans使用的人稍微少一些,但它有很多比Eclipse更为优秀的地方,例如免费、Oracle自家出品,对于新标准的支持非常好、有中文语言包、界面布局更合理等。

但从目前的统计数据来看,IntelliJ Idea的市场份额已经超过了Eclipse。

IntelliJ IDEA作为一个商业化的IDE,而且是一个非常昂贵的商业化IDE(499刀/年),能够被如此多的开发人员认可,肯定有它非凡之处,简单罗列几点:

  • 黑色的Darcula主题,我想应该把这个放在第一位
  • 智能化的代码提示、自动补全、重复代码检查、快速修复
  • 更加智能的重构工具,支持跨语言的重构
  • 人性化的调试工具
  • 包含大量的内建工具,开箱即用的,无须配置大量的插件
  • 默认支持git/svn等多种版本控制工具
  • 默认支持Maven, Gradle, Ant等多种构建工具
  • 内建反编译工具,方便查看没有源码的老旧jar包
  • 内建数据库管理工具,可以抛弃toad/Navicat这些东西了
  • 内建REST API测试工具,可以抛弃Postman之类的测试工具了
  • 支持Tomcat、JBoss、WebLogic等主流应用服务器
  • Google的助攻,Android开发环境由最初基于Eclipse的ADT,迁移到了基于IDEA的Android Studio

导入一个工程

要想开始体验IntelliJ IDEA,首先要新建一个工程(Project)或导入已有的工程。这里我们选择从版本库中导入一个已有的工程。

在欢迎界面中点击最下方的 Check out from Version Control

可以看到,IntelliJ IDEA支持GitHub、CVS、Git、Svn等多种版本库。

这里我们尝试导入一个GitHub中的工程,填写自已的GitHub用户名、密码

在下拉列表中选择好一个工程后,点击 Clone

如果项目是用Gradle/Maven构建的,还会弹出一个构建工具配置界面

导入完成后,可以看到主界面了

项目结构

IntelliJ IDEA中的项目结构和Eclipse有很大的不同。

  • Eclipse中有workspace的概念,可以向workspace中添加多个工程。
  • IDEA没有workspace,一个窗口中只能打开一个工程(Project),但一个工程中可以包含多个模块(module),这种方式更符合多模块应用开发的需求。

界面左侧的Project栏,可以查看项目中的文件结构,通过Cmd+1快捷键调出/关闭。

使用Cmd+; 快捷键,可以打开项目设置界面,在这里配置模块、库、Facets、Artifacts等。

Facets

Facets可以认为是项目支持的特性,例如JPA、Spring、Hibernate等,当一个项目被IDEA扫描完毕之后,它会自动为每个模块添加相应用Facets,当然也可以手工进行添加

Artifacts

Artifacts是项目的打包部署设置。
例如,对于WEB工程而言,IDEA通过配置Artifacts,将编译输出的class文件,与jsp/html/css页面等静态文件,以特定的目录结构合并到一起。

目录图标

  • 蓝色 - 源码根目录
  • 绿色 - 测试代码目录
  • 红色 - 需要排除的目录,例如class/build等包含二进制文件的目录
  • 黄色 - 资源目录,application.yml等会放在这里面

点击Project栏中右上角齿轮图标,可以调整目录的展示方式,比如合并中间空目录等:

快捷键对比

下面是一份Eclipse与IntelliJ IDEA的快捷键比对表:

如果不想使用IDEA的默认快捷键,想沿用之前的一些习惯,IDEA也提供了几套其它的配置,使用 Ctrl+` 调出菜单

选择 3-Keymap 即可切换配置

但建议还是尽量适应IntelliJ IDEA的默认快捷键配置吧。

下面有几个有趣的与其它IDE的区别:

  • Cmd+D
    在Eclipse中,Cmd+D是删除一行,但在IntelliJ IDEA中,D是指duplicate,复制一行。
  • Cmd+S
    在其它编辑器中,我们通常会频繁使用Cmd+S进行保存,但在IDEA中,完全不需要这么做。IntelliJ IDEA的自动保存功能做的非常好,不用担心会丢失进度,Cmd+S只能给我们带来一些心理上的”安全感”。

界面布局

IntelliJ IDEA的默认布局非常的简洁,几乎只有一个编辑器界面,甚至还可以通过Ctrl+Cmd+F全屏显示,以获得更加沉浸式的体验。

点击界面最左下角的显示器形状的图标,可以用来切换布局模式,调出或隐藏周边的工具栏

在使用精简布局时,如果要进行文件定位、打开某个侧边栏等操作,就需要使用一些快捷建进行操作了。

一般是使用Cmd+数字来显示侧边栏,例如Cmd+1显示Project栏,Cmd+9显示版本控制栏,具体数字界面上会有提示。

另外,Cmd+E不但可以显示最近使用过的文件,还可以在左侧选择打开各个侧边栏,非常好用。

同样的,Ctrl+Tab快捷键可以实现类似的功能,并且效率更高

快速查找

使用Cmd+O可以弹出快速 查找类名 窗口,输入类名关键字筛选,可以快速打开类所在的源文件

使用Cmd+Shift+O可以弹出快速 查找文件名 窗口,输入文件名关键字,可以打开任意文件

还有一个更方便的方式,如果不确定要找什么,双击shift键,可以弹出”Search Everywhere”窗口,输入关键词搜索一切吧。

当然,还可以使用之前提到过的Cmd+E,查看曾经打开过的文件。

常用设置

缩进方式配置

使用空格与TAB键缩进,在码农界争论至今还没有定论,那就不妨根据自已的喜好手工设置一下吧。

首先使用Cmd+,打开全局设置界面,在Editor → Editor Tabs中可以找到相应的选项:

界面主题配置

Appearance & Behavior → Appearance中,可以设置喜欢的主题,字体、字号等

这里当然是首推Darcula主题。

代码编辑

首先要记忆的一个快捷键是Cmd+F12,可以打开 class outline 窗口,查看文件中的各个方法。

类似的也可以通过Ctrl+H打开 type hierarchy 侧边栏

另外,还可以使用Alt+F7,打开 Find Usages 底边栏,显示方法的使用者。

自动补全

使用Ctrl+Space,可以进行代码自动补全。

Ctrl+Shift+Space,是更加智能的代码补全,它会猜测你的意图,只展示最有用的结果

另外Alt+Enter也十分有用,当使用了一个类,但它还没有被导入时,可以用它来快速导入

代码生成

使用Cmd+N可以自动生成代码,比如Getter/Setter/toString()等等

代码重构

最简单的,可以通过右键菜单或Shift+F6进行重命名,可以是文件,也可以是类名、方法名、变量名等,点击确认后IntelliJ IDEA会跨语言检索全部代码(包括.html/.js),找到所有相关的地方一起修改。

插入模版

输入特定的关键字,按TAB后,可以直接在光标处插入模板代码。

例如,首先输入psvm

然后按下TAB键,就会自动插入一个main方法的模版,相当快捷

其它重要的快捷键

  • 格式化代码 - Cmd+Alt+L
  • 编译 - Cmd+F9
  • 关闭某个侧边栏 - Shift+ESC

查找快捷键的快捷键

到现在为止,已经接触了IntelliJ IDEA的不少快捷键了。
其实对于学习一个文本编辑器也好、IDE也好,要想用的得心应手,记忆大量的快捷键都是少不了的步骤。

但如果记不清快捷键怎么办,去哪里查呢?幸好IntelliJ IDEA为我们提供了一个终级快捷键 Cmd+Shift+A,这是一个用来 查找快捷键的快捷键 !其它所有的快捷键都忘了也没有关系,只要记得这一个。。。

配置Tomcat

对于Web应用,如果想在本机直接运行测试,除了使用Maven等构建工具内嵌的Application Server之外,还可以选择直接在IntelliJ中配置一个。

注意请提前配置好Artifacts,然后点击顶部右侧的 Run Configuration 按钮,在新窗口中点击左上角加号,选择Tomcat即可:

代码检查

在向版本库中提交时,可以在右侧选择进行一些提交前的处理,如代码检查、格式化等:

收藏夹

在Tab页上点击右键,可以选择Add to Favorites

然后在左下方的Favorites栏中就可以看到了

版本控制

在窗口左下方的 Version Control 栏中,可以查看到做过变更的文件

在文件上右键,可以进行代码差异比对,回滚等操作

在窗口右上方的工具栏中,可以快速Update/Commit

在窗口右下方的工具栏中,可以查看/新建/切换Git分支

内嵌工具

菜单栏中,Tools一项中,可以找到很多内嵌的工具,比如很常用的Rest测试工具

数据源配置

在右侧的Database工具栏中,可以配置常用的数据源

配置好之后,就可以使用内嵌的数据库查询管理工具了,另外在编写配置文件或SQL/JPQL时,也会有对应用智能提示

官方文档

以上介绍的,只是IntelliJ IDEA众多功能中的冰山一角,想要了解更多,请进一步参考官方文档:

另外官网上也提供了很多的视频教程,其中有不少小技巧。

需求简介

最近厂里有一个新闻采集类的需求,细节大体如下:

  1. 模拟登录一个内网网站(SSO)
  2. 抓取新闻(支持代理服务器的方式访问)
  3. 加工内容样式,以适配手机屏幕
  4. 将正文中的图片转存到自已的服务器,并替换标签中的url
  5. 图片存储服务器需要复用已有的FastDFS分布式文件系统
  6. 采集结果导入生产库
  7. 支持日志打印

初学Python3,正好用这个需求练练手,最后很惊讶的是只用200多行代码就实现了,如果换成Java的话大概需要1200行吧。果然应了那句老话:人生苦短,我用Python

登录页面抓包

第一步当然是抓包,然后再根据抓到的内容,模拟进行HTTP请求。

常用的抓包工具,有Mac下的Charles和Windows下的Fiddler。
它们的原理都是在本机开一个HTTP或SOCKS代理服务器端口,然后将浏览器的代理服务器设置成这个端口,这样浏览器中所有的HTTP请求都会先经过抓包工具记录下来了。

这里推荐尽量使用Fiddler,原因是Charles对于cookie的展示是有bug的,举个例子,真实情况:请求A返回了LtpaToken这个cookie,请求B中返回了sid这个cookie。但在Charles中的展示是:请求A中已经同时返回了LtpaToken和sid两个cookie,这就很容易误导人了。
另外Fiddler现在已经有了Linux的Beta版本,貌似是用类似wine的方式实现的。

如果网站使用了单点登录,可能会涉及到手工生成cookie。所以不仅需要分析每一条HTTP请求的request和response,以及带回来的cookie,还要对页面中的javascript进行分析,看一下是如何生成cookie的。

模拟登录

将页面分析完毕之后,就可以进行模拟HTTP请求了。
这里有两个非常好用的第三方库, requestBeautifulSoup

requests 库是用来代替urllib的,可以非常人性化的的生成HTTP请求,模拟session以及伪造cookie更是方便。
BeautifulSoup 用来代替re模块,进行HTML内容解析,可以用tag, class, id来定位想要提取的内容,也支持正则表达式等。

具体的使用方式直接看官方文档就可以了,写的非常详细,这里直接给出地址:
requests官方文档
BeautifulSoup官方文档

通过pip3来安装这两个模块:

1
2
3
sudo apt-get install python3-pip
sudo pip3 install requests
sudo pip3 install beautifulsoup4

导入模块:

1
2
import requests
from bs4 import BeautifulSoup

模拟登录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def sso_login():
# 调用单点登录工号认证页面
response = session.post(const.SSO_URL,
data={'login': const.LOGIN_USERNAME, 'password': const.LOGIN_PASSWORD, 'appid': 'np000'})

# 分析页面,取token及ltpa
soup = BeautifulSoup(response.text, 'html.parser')
token = soup.form.input.get('value')
ltpa = soup.form.input.input.input.get('value')
ltpa_value = ltpa.split(';')[0].split('=', 1)[1]

# 手工设置Cookie
session.cookies.set('LtpaToken', ltpa_value, domain='unicom.local', path='/')

# 调用云门户登录页面(2次)
payload = {'token': token}
session.post(const.LOGIN_URL, data=payload, proxies=const.PROXIES)
response = session.post(const.LOGIN_URL, data=payload, proxies=const.PROXIES)
if response.text == "success":
logging.info("登录成功")
return True
else:
logging.info("登录失败")
return False

这里用到了BeautifulSoup进行HTML解析,取出页面中的token、ltpa等字段。
然后使用session.cookies.set伪造了一个cookie,注意其中的domain参数,设置成1级域名。
然后用这个session,去调用网站页面,换回sid这个token。并可以根据页面的返回信息,来简单判断一下成功还是失败。

列表页面抓取

登录成功之后,接下来的列表页面抓取就要简单的多了,不考虑分页的话,直接取一个list出来遍历即可。

1
2
3
4
5
6
7
8
9
10
11
def capture_list(list_url):
response = session.get(list_url, proxies=const.PROXIES)
response.encoding = "UTF-8"
soup = BeautifulSoup(response.text, 'html.parser')
news_list = soup.find('div', 'xinwen_list').find_all('a')
news_list.reverse()
logging.info("开始采集")
for news_archor in news_list:
news_cid = news_archor.attrs['href'].split('=')[1]
capture_content(news_cid)
logging.info("结束采集")

这里使用了response.encoding = "UTF-8"来手工解决乱码问题。

新联页面抓取

新闻页面抓取,涉及到插临时表,这里没有使用每三方库,直接用SQL方式插入。
其中涉及到样式处理与图片转存,另写一个模块pconvert来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def capture_content(news_cid):
# 建立DB连接
conn = mysql.connector.connect(user=const.DB_USERNAME, password=const.DB_PASSWORD, host=const.DB_HOST,
port=const.DB_PORT, database=const.DB_DATABASE)
cursor = conn.cursor()

# 判断是否已存在
cursor.execute('select count(*) from material_prepare where news_cid = %s', (news_cid,))
news_count = cursor.fetchone()[0]
if news_count > 0:
logging.info("采集" + news_cid + ':已存在')
else:
logging.info("采集" + news_cid + ':新增')
news_url = const.NEWS_BASE_URL + news_cid
response = session.post(news_url, proxies=const.PROXIES)
response.encoding = "UTF-8"
soup = BeautifulSoup(response.text, 'html.parser')
# logging.info(soup)
news_title = soup.h3.text.strip()[:64]
news_brief = soup.find('div', 'brief').p.text.strip()[:100]
news_author = soup.h5.span.a.text.strip()[:100]
news_content = soup.find('table', 'unis_detail_content').tr.td.prettify()[66:-7].strip()
# 样式处理
news_content = pconvert.convert_style(news_content)
# 将图片转存至DFS并替换URL
news_content = pconvert.convert_img(news_content)
# 入表
cursor.execute(
'INSERT INTO material_prepare (news_cid, title, author, summary, content, add_time, status) VALUES (%s, %s, %s, %s, %s, now(), "0")'
, [news_cid, news_title, news_author, news_brief, news_content])
# 提交
conn.commit()
cursor.close()

样式处理

文本样式处理,还是要用到BeautifulSoup,因为原始站点上的新闻内容样式是五花八门的,根据实际情况,一边写一个test函数来生成文本,一边在浏览器上慢慢调试。

1
2
3
4
5
6
7
8
9
def convert_style(rawtext):
newtext = '<div style="margin-left: 0px; margin-right:0px; letter-spacing: 1px; word-spacing:2px;line-height: 1.7em; font-size:18px;text-align:justify; text-justify:inter-ideograph">' \
+ rawtext + '</div>'
newtext = newtext.replace(' align="center"', '')
soup = BeautifulSoup(newtext, 'html.parser')
img_tags = soup.find_all("img")
for img_tag in img_tags:
del img_tag.parent['style']
return soup.prettify()

图片转存至DFS

因为原始站点是在内网中的,采集下来的HTML中,标签的地址是内网地址,所以在公网中是展现不出来的,需要将图片转存,并用新的URL替换原有的URL。

1
2
3
4
5
6
7
8
9
def convert_img(rawtext):
soup = BeautifulSoup(rawtext, 'html.parser')
img_tags = soup.find_all("img")
for img_tag in img_tags:
raw_img_url = img_tag['src']
dfs_img_url = convert_url(raw_img_url)
img_tag['src'] = dfs_img_url
del img_tag['style']
return soup.prettify()

图片转存最简单的方式是保存成本地的文件,然后再通过nginx或httpd服务将图片开放出去:

1
2
3
4
pic_name = raw_img_url.split('/')[-1]
pic_path = TMP_PATH + '/' + pic_name
with open(pic_path, 'wb') as pic_file:
pic_file.write(pic_content)

但这里我们需要复用已有的FastDFS分布式文件系统,要用到它的一个客户端的库fdfs_client-py
fdfs_client-py不能直接使用pip3安装,需要直接使用一个python3版的源码,并手工修改其中代码。操作过程如下:

1
2
3
4
5
6
7
git clone https://github.com/jefforeilly/fdfs_client-py.git
cd dfs_client-py
vi ./fdfs_client/storage_client.py
将第12行 from fdfs_client.sendfile import * 注释掉
python3 setup.py install

sudo pip3 install mutagen

客户端的使用上没有什么特别的,直接调用upload_by_buffer,传一个图片的buffer进去就可以了,成功后会返回自动生成的文件名。

1
2
3
4
5
6
7
8
9
from fdfs_client.client import *
dfs_client = Fdfs_client('conf/dfs.conf')
def convert_url(raw_img_url):
response = requests.get(raw_img_url, proxies=const.PROXIES)
pic_buffer = response.content
pic_ext = raw_img_url.split('.')[-1]
response = dfs_client.upload_by_buffer(pic_buffer, pic_ext)
dfs_img_url = const.DFS_BASE_URL + '/' + response['Remote file_id']
return dfs_img_url

其中dfs.conf文件中,主要就是配置一下 tracker_server

日志处理

这里使用配置文件的方式处理日志,类似JAVA中的log4j吧,首先新建一个log.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[loggers]
keys=root

[handlers]
keys=stream_handler,file_handler

[formatters]
keys=formatter

[logger_root]
level=DEBUG
handlers=stream_handler,file_handler

[handler_stream_handler]
class=StreamHandler
level=DEBUG
formatter=formatter
args=(sys.stderr,)

[handler_file_handler]
class=FileHandler
level=DEBUG
formatter=formatter
args=('logs/pspider.log','a','utf8')

[formatter_formatter]
format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s

这里通过配置handlers,可以同时将日志打印到stderr和文件。
注意args=('logs/pspider.log','a','utf8') 这一行,用来解决文本文件中的中文乱码问题。

日志初始化:

1
2
3
4
import logging
from logging.config import fileConfig

fileConfig('conf/log.conf')

日志打印:

1
logging.info("test")

完整源码

到此为止,就是如何用Python3写一个爬虫的全部过程了。
采集不同的站点,肯定是要有不同的处理,但方法都是大同小异。
最后,将源码做了部分裁剪,分享在了GitHub上。
https://github.com/xiiiblue/pspider
最后,将源码做了部分裁剪,分享在了GitLab上。
http://git.si-tech.com.cn/guolei/pspider