Dust8 的博客

读书百遍其义自见

0%

因为博客转到了 hexo, 它用的是 node, 而 node 的依赖包很容易出错,出错后也很难修复.
所以把环境隔离下比较好,用现在最火的 docker.

因为是转移,所以有些步骤就不说了, 默认你会一些 node, hexo, docker.

资料

步骤

目录结构

- hexo-docker
  - Dockerfile
- hblog
  - package.json
  - source
  - themes
  ...

docker-machine ip

查看虚拟机 ip , 等下就可以用它来打开 blog, 一般是 192.168.99.100.

docker-machine ip

Dockerfile

# pull node 4
FROM node:argon

# make workdir
RUN mkdir -p /usr/src/blog
WORKDIR /usr/src/blog

# install hexo
RUN npm install -g hexo-cli

build image

docker build -t hexo_blog .

run image

  • -v ~/hblog:/usr/src/blog 是把本机的 hblog 目录挂载到 docker 容器里面的
    /usr/src/blog 目录, 修改 hblog 里面的东西就等于修改容器里面 /usr/src/blog 的东西
  • -p 4000:4000, 把容器的 4000 端口映射出来
  • -it, Keep STDIN open even if not attached and Allocate a pseudo-tty
  • bash, 进入容器的bash命令行
docker run -v ~/hblog:/usr/src/blog -p 4000:4000 -it hexo_blog bash

进入之后就可以像在本机一样使用 hexo 了.

使用hexo

因为是转移, package.json 里面有所有的依赖包, 所以先安装它, 在开启 hexo 服务.

npm install
hexo server

在浏览器里面输入 http://192.168.99.100:4000/, 就可以查看博客了.

下次使用

# 列出容器
docker ps -a

# 进入容器
docker start -i {容器id}

# 停止容器
docker stop {容器id}

前言

好几年前就开始接触 dht 了,那时还是 小虾 引起的潮流.
大概原理基本都了解了, 就是代码写的难看,执行效率也不高。
这几年有很多人也接触到这些,开源了不少简洁而又高效的代码。

技术表

B 编码

我用的是 python3, 不是主流,所以要自己写编码和解码,有了 bencoding

官方的库

bencode, 它只支持 python2.

非官方的库

bencoder.pyx, A fast bencode implementation in Cython.

infohash 的抓取

官方的 bootstrap-dht

bootstrap-dht, 它简单的示例了下 dht 启动服务器。

非官方的

  • simDHT, 它是老太太的杰作, 简单又简洁。
    它最先解决了怎么才能高效的认识其他节点,也就是改变发送的 id。id 的高位也就是最前面的影响最大,距离越远。

  • maga, 这是最近发现的,它使用了 asyncio, 非常高效,代码也
    很简洁,推荐看一下。

种子信息的抓取

dht 网络抓取

这种方法不会受限于人,它根据bittorrent协议去下载信息。优点是全,稳, 缺点就是慢.
如果直接根据 announce_peer 去下载,有很多 peer 连不上,有很多 peer 自己都还没下载完成。

其他提供者抓取

这种方法就是去抓像迅雷这样的提供者.优点是快,缺点是不全,要看它们的脸色.

ip 和端口的解析

ip 4 个字节,怎么转成 ‘192.168.1.1’ 这样,用 socket.inet_ntoa,半路出家的坑啊。

路由表

写爬虫完全不需要

效率

maga 那样,效率太高了,cpu 和 网络占用太多。 给它定个限制,一分钟只能发多少个包,
超过的直接就不发了,因为是爬虫,丢包没影响。

文件名解码

绝大多数可以用 utf8 解码,出错在用 gb18030, 剩下的就难搞了,只能丢掉了。

爬虫架构

bt-crawler-system

  1. infohash 抓取用 maga 改的
  2. metadata 抓取用 迅雷的地址
  3. infohash 传递用 rabbitmqcelery
  4. 数据库用 mongodb, 全文搜索就不好搞, 可以考虑用 elasticsearch.

英文分词大体用空格就可以分开了,而中文分词无法使用该办法。

中文分词最简单的办法就是查字典。它其实就是把一个句子从左往右扫描一遍, 遇到字典里有的词就标识出来,最后取长度最长的那个词,如果都不在字典里, 就分割成单字词。

1
2
3
dictionary = ['中', '中国', '航天', '官员', '应邀', '到', '美国', '与', '太空', '总署', '开会']
text1 = '中国航天官员应邀到美国与太空总署官员开会'
text2 = '中国航天官员应邀到美国与太空总署官员开会去了'
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 look_up_dictionary(text, dictionary):
cut_point = 0
words = []
text_length = len(text)
while cut_point < text_length:
step = 0
i = 0
text_remain_length = len(text[cut_point:])
while i < text_remain_length:
i += 1
word = text[cut_point:cut_point+i]
if word in dictionary:
step = i

# 字典里没有或者就是单字词
if step == 0:
step = 1

words.append(text[cut_point:cut_point+step])
cut_point += step
return words

print('/'.join(look_up_dictionary(text1, dictionary)))
print('/'.join(look_up_dictionary(text2, dictionary)))

输出为

1
2
中国/航天/官员/应邀//美国//太空/总署/官员/开会
中国/航天/官员/应邀//美国//太空/总署/官员/开会/去/

字典里有 , 中国 , 就取长度最长的 中国 ;

字典里没有 去了, 就取单字词

这样就可以解决大半的分词问题。

那么字典去哪里找,这里分享一个网站,http://www.mdbg.net/。

这里有个项目叫 CC-CEDICT, 可以下载到字典。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_dictionary(filename):
dictionary = []
with open(filename) as f:
for line in f:
if line.startswith('#') or line.startswith('%'):
continue
word = line.split()[1]
dictionary.append(word)
return dictionary


dictionary = get_dictionary('cedict_ts.u8')
print(len(dictionary))
print('/'.join(look_up_dictionary('上海大学城书店', dictionary)))

输出为

1
2
114350
上海大学/城/书店

CC-CEDICT 目前大约有 11万 的字典, 里面有繁体,简体, 上面代码只要了简体。

上一篇文章说了怎么抓取期刊里面的歌曲信息,这一篇文章就讲讲怎么去下载歌曲。

随便播放一首歌,用 chromeNetwork 就可以观察到,歌曲的地址。

例如,

1
http://luoo-mp3.kssws.ks-cdn.com/low/luoo/radio805/01.mp3

观察这个地址就可以发现, 805 是期刊的刊号, 01.mp3 就是歌曲的序号构成。 我们就可以根据它构造每一首歌曲的 url 去下载 mp3 格式的文件。

我用 12 个小时 下载了 第1期第805期 的歌曲, 大约有 八千 首歌曲,占用空间 40G 左右。

在下载过程中 第1期第99期 出错了, 是因为下载地址构建错了,

比如,我构建的地址是

1
http://luoo-mp3.kssws.ks-cdn.com/low/luoo/radio099/01.mp3

而正确的地址是

1
http://luoo-mp3.kssws.ks-cdn.com/low/luoo/radio99/01.mp3

前面的 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
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
71
72
73
74
75
76
77
78
79
80
import argparse
import os
import logging
import sys

from urllib.request import urlopen
from concurrent.futures import ThreadPoolExecutor
from pymongo import MongoClient

# http://luoo-mp3.kssws.ks-cdn.com/low/luoo/radio805/01.mp3
# http://luoo-mp3.kssws.ks-cdn.com/low/luoo/radio5/01.mp3
BASE_URL = 'http://luoo-mp3.kssws.ks-cdn.com/low/luoo/radio'

client = MongoClient()
db = client.luoo

logger = logging.getLogger('download')
logger.setLevel(logging.DEBUG)

fh = logging.FileHandler('luoo.log')
fh.setLevel(logging.DEBUG)

ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')

fh.setFormatter(formatter)
ch.setFormatter(formatter)

logger.addHandler(fh)
logger.addHandler(ch)


def download(song, basedir='luoo_download'):
logger.info(('downloading ', song['vol_number'], song['trackname']))
try:
dirpath = os.path.join(
basedir, song['vol_number'] + '_' + song['vol_title'])
if not os.path.exists(dirpath):
os.makedirs(dirpath)
url = BASE_URL + str(int(song['vol_number'])) + '/' + \
song['trackname'].split('.')[0] + '.mp3'
data = urlopen(url).read()
filename = os.path.join(
dirpath, song['trackname'] + '_' + song['artist'] + '.mp3')
if os.path.exists(filename):
raise FileExistsError(filename)
with open(filename, 'wb') as f:
f.write(data)
logger.info(('download complete', song[
'vol_number'], song['trackname']))
except Exception as inst:
logger.error(
('download error', song['vol_number'], song['trackname'], inst))


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--min", type=int, default=0,
help="min number to download")
parser.add_argument("--max", type=int,
help="max number to download")
args = parser.parse_args()

songs = []
if args.max is None:
songs = [song for song in db.music.find(
{'vol_number': {'$gt': str(args.min)}})]
elif args.max > 0:
songs = [song for song in db.music.find(
{'vol_number': {'$gt': str(args.min), '$lt': str(args.max)}})]

with ThreadPoolExecutor(max_workers=4) as executor:
executor.map(download, songs)


if __name__ == '__main__':
main()

Scrapy 是一个 python 爬虫框架。又因为我是个 python3 的死忠, 所以一直听说,却没用过。现在好了,它的测试版已经支持 python3, 终于可以体验下了传说中的东西。

用它写个落网爬虫– luoospider, 可以 Star 支持下.

官网

1
http://scrapy.org/

安装Scrapy

1
pip install scrapy==1.1.0rc3

项目分析

1. 目的:抓取并下载落网的音乐

2. 分析落网的页面结构

1
2
3
4
5
6
7
* 期刊入口: `http://www.luoo.net/music/`
* 每一页的期刊列表在`<div class="vol-list">`
* 下一页在 `<a class="next">`
* 期刊刊号在 `<span class="vol-number">`
* 期刊标题在 `<span class="vol-title>"`
* 每期期刊列表在 `<div class="vol-tracklist">` 里面,有每首歌的详细内容
* 一首歌的url为 `http://luoo-mp3.kssws.ks-cdn.com/low/luoo/radio805/01.mp3`, `radio805` 是第805期期刊,`01.mp3` 是这期期刊的第一首歌曲。

创建项目

1
scrapy startproject luoo

进入luoo目录后显示的文件如下:

1
2
3
4
5
6
7
8
9
├── luoo
│ ├── __init__.py
│ ├── items.py
│ ├── pipelines.py
│ ├── settings.py
│ └── spiders
│ ├── __init__.py
│ └── luoo_spider.py
└── scrapy.cfg

其中 luoo_spider.py 是我们自己要新建的文件.

代码

items.py

1
2
3
4
5
class LuooItem(scrapy.Item):
vol_number = scrapy.Field()
vol_title = scrapy.Field()
trackname = scrapy.Field()
artist = scrapy.Field()

luoo_spider.py

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
class LuooSpider(Spider):
name = 'luoo'
allowed_domains = ['luoo.net']
start_urls = ['http://www.luoo.net/music/']

def __init__(self, last_vol_number=0, *args, **kwargs):
super(LuooSpider, self).__init__(*args, **kwargs)
# 上次抓取的最大期刊期号
self.last_vol_number = last_vol_number
self.stop = False

def parse(self, response):
# 抓取一页里面的每期期刊地址
for href in response.css('div.vol-list>div.item>a::attr("href")'):
url = href.extract()
if url.split('/')[-1] <= self.last_vol_number:
self.stop = True
break
yield Request(url, callback=self.parse_vol_contents)

# 抓取下一页地址
if not self.stop:
for href in response.css('a.next::attr("href")'):
url = href.extract()
yield Request(url, callback=self.parse)

def parse_vol_contents(self, response):
'''抓取一期期刊里面的内容
'''
vol_number = response.css('span.vol-number::text').extract_first()
vol_title = response.css('span.vol-title::text').extract_first()

for sel in response.xpath('//div[contains(@class, "vol-tracklist")]/ul/li'):
item = LuooItem()
item['vol_number'] = vol_number
item['vol_title'] = vol_title
item['trackname'] = sel.css(
'div.track-wrapper>a.trackname::text').extract_first()
item['artist'] = sel.css(
'div.track-wrapper>span.artist::text').extract_first()
yield item

pipelines.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MongoPipeline:
collection_name = 'music'

def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db

@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE', 'luoo')
)

def open_spider(self, sipder):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]

def close_spider(self, spider):
self.client.close()

def process_item(self, item, spider):
self.db[self.collection_name].insert(dict(item))
return item

luoo 爬虫支持 last_vol_number 参数,它指定了上次爬取的最大期刊刊号,可以避免重复 抓取以前抓取过的期刊。

运行爬虫

1
scrapy crawl luoo

或者

1
scrapy crawl luoo -a last_vol-number=800