解决mikan爬虫在512M的vps上内存溢出的二三事
关于mikan爬虫策略
先简单描述下mikan爬虫的执行过程:
- 扫描首页, 收集到番组的年份和对应的季度
- 获取某个年份某个季度的所有番组
- 获取每个番组下的字幕组, 并获取相应字幕组包含的作品
- 最终按年份输出其对应的所有数据
内存溢出的原因
最初始的方案是把收集的数据按年份key
键存储到一个全局字典里, 每个年份收集完毕后输出一个文件, 输出后手动释放存储年份的key
. 这种方案在单线程下没有问题, 但是用了多线程后内存不到一会就直接飚满爆掉了. 因此本质上应该是内存存储的对象太多, 导致溢出
按年-季碎片化
参考了一些python前辈的建议, 应该在年份文件输出前碎片化切割若干个文件, 最后再通过这些碎片化文件组合起来. 于是我按年份季度优先输出${year}__${season}.json
,
最后通过标识符year
和season
组装数据. 这种方案本来是运行良好的, 直到我更换了爬虫策略, 字幕组作品的数据量一下子就更多了. 据我观察, 这种方案只要爬取完一个年份后,
即使手动释放也只能让内存暂时缓冲一下, 下一个年份的数据一来, 剩余内存就慢慢被蚕食殆尽了
按年-季-星期碎片化
自从换了爬虫策略后, 年-季碎片化的方案一直优化无果, 于是我也只能继续从年-季里继续碎片化了
def find_type_container(self, soup, year, season, season_data):
for key, value in self.type_mapper.items():
attrs = {}
attrs['data-dayofweek'] = key
container = soup.find(attrs=attrs)
season_data[value] = []
if container:
self.find_bangumi_collections(container, season_data[value])
self.output_temp_file(season_data, year, season, value)
一个季度里包含星期一 ~ 星期日 + 剧场版 + ova, 每爬完一个就写一个临时文件(${year}__${season}__${day}.json
), 在所有数据都收集完毕后, 最后一次性输出所有年份的数据
def output(self):
files = pydash.filter_(os.listdir(self.temp_path), lambda el: el.find('.json') > -1)
year_group = pydash.arrays.sorted_uniq(pydash.map_(files, lambda el: el.split('__')[0]))
for year in year_group:
season_arr = []
year_files = pydash.filter_(files, lambda el: el.find(year) > -1)
season_group = pydash.arrays.sorted_uniq(pydash.map_(year_files, lambda el: el.split('__')[1]))
for season in season_group:
day_arr = []
files_read = pydash.filter_(year_files, lambda el: el.find(season) > -1)
for file_read in files_read:
with open(self.temp_path + file_read, 'r') as f:
day = file_read.split('__')[2].replace('.json' , '')
day_arr.append('"' + day + '":' + f.read())
season_arr.append('"' + season + '":{' + ','.join(day_arr) + '}')
with open(spider_path() + 'mikan__' + year + '.json', 'w') as f:
f.write('{' + ','.join(season_arr) + '}')
f.flush()
我在年份文件输出的前是直接按字符串拼接, 应该能节省不少内存
这种方案临时文件挺多, 但是内存控制得非常好, 内存占用只在40% ~ 50%
之间. 总算是能完整跑完了
nodejs的奇怪现象
每次爬虫执行完毕后, 我会用node
来读取年份文件来对比并更新数据源, 过滤出更新的番组和字幕组. 工程目录如下
dist-builder.js
主流程如下
fs.readdir(path.join(__dirname), (err, files) => {
if (err) return writeLog(`Read dir error: ${err.message}`)
files = files.filter(file => file.indexOf('.json') !== -1)
const tasks = files.map((file, i) => {
return new Promise((resolve, reject) => {
readMikan(file)
.then(result => Promise.all([writeBangumi.apply(null, result), writeSub.apply(null, result)]))
.then(result => {
// result[0] -> 更新的番组
// result[1] -> 更新的字幕组
resolve(result)
})
.catch(err => { reject(err) })
})
})
Promise.all(tasks)
.then(result => {
const socket = net.connect({ port: config.socketPort }, () => {
writeLog('connected socket server!')
socket.write(JSON.stringify({
bangumi: _.flattenDeep(result.map(elem => elem[0])),
torrent: _.flattenDeep(result.map(elem => elem[1]))
}))
socket.destroy()
})
})
.catch(err => {
writeLog(err.message)
})
})
爬虫执行完毕会调用dist-builder.js
, 结果如下
有些文件竟然是0 byte!! 但是如果我是让python
直接调用或是命令行执行dist-builder.js
, 是能正常输出的. What the hell????????
我发现node在写入文件的后竟然不会执行回调函数, 但事实上ta是写入了文件了. 由于没有执行回调函数, 我后续的promise铁定也不回执行了. 但是整个过程是没有报错的, 太奇怪了! 当且仅当在这台vps上走爬虫的case才会出现!! 我快要放弃治疗了=. =#########
最后我尝试了用同步的方法来实现上面的那段逻辑
const files = fs.readdirSync(path.join(__dirname)).filter(file => file.indexOf('.json') > -1)
const queue = (function (files) {
const _count = files.length
let _updates = {
bangumi: [],
torrent: []
}
let _index = 0
return function () {
if (_index >= _count) {
const socket = net.connect({ port: config.socketPort }, () => {
writeLog('connected socket server!')
socket.write(JSON.stringify(_updates))
socket.destroy()
})
} else {
readMikan(files[_index])
.then(result => Promise.all([writeBangumi.apply(null, result), writeSub.apply(null, result)]))
.then(result => {
// result[0] -> 更新的番组
// result[1] -> 更新的字幕组
_updates.bangumi = _updates.bangumi.concat(result[0])
_updates.torrent = _updates.torrent.concat(result[1])
queue(++_index)
})
.catch(err => { writeLog(err.message) })
}
}
})(files)
queue()
总算能成功输出了. 那么问题来了, 到底是为什么呢???? 来一个TODO
先记录着
shadowsocks服务器被kill
这个跟本文无关, 只是爬虫服务运行后, ssserver进程可能会被kill掉, 这里mark一下, 日后解决233333