功能简介
个人兴趣,打算爬个数据库拿来做推荐系统,几经周折终于写出了能稳定运行的D站爬虫。
这个爬虫抓取的内容包括作品名、作者名、上传日期、分类、评分和每个评分者,比较适合于简单的协同推荐算法。
为了效率,采用多线程方式,各线程间利用任务队列同步,结果使用sqlite3模块保存在两个数据库文件中。我的电脑不能24小时运行着它,所以加入了中断恢复的功能。
程序用到的模块如下:
import urllib2
import sqlite3
import re
import string
from bs4 import BeautifulSoup
import time
import Queue
import threading
程序流程
各线程功能
1.扩充个人主页线程Extender
这个线程包含了两个功能:抓取首页作者列表,放入个人主页队列中;以及获得现有队列中每一位作者的好友列表,放回到个人主页队列中。
启动后,它会等待中断恢复线程的报告,若没有待完成任务,就开始抓取首页并进行扩充,直到处理的个人主页达到指定量为止。若有待完成任务,则根据控制信号,直接退出(不扩充)或对中断恢复线程提供的队列进行扩充。
#--------------------------------------------------------------------#
#扩充个人主页列表的线程,输入为已备份主页队列qSaved,输出为已扩充主页队列qExtended和待处理主页队列qProfile
class ExtendProfileThread(threading.Thread):
#初始化
def __init__(self, thread_name, qsaved, qextended, qprofile, repeat=1000):
threading.Thread.__init__(self, name = thread_name)
self.input = qsaved
self.output = qextended
self.recycle = qprofile
self.indexUrl = ''
self.remaincount = repeat
self.isRunning = 0
#读取首页(仅在有首页地址传入时执行)
def readIndex(self):
url_legal_pattern = re.compile('http://.*') #用于检查url合法性的正则表达式模式
strip_tag_pattern = re.compile('</?\w+[^>]*>') #用于去除html标签的正则表达式模式
#检查URL合法性
match = url_legal_pattern.search(indexUrl)
if not match:
print 'Illegal Index URL:', indexUrl
return -1
#读取页面
try:
site = urllib2.urlopen(indexUrl, timeout=10)
content = site.read()
except:
print 'Time out for index:', indexUrl
return -1
#分析HTML结构
soup = BeautifulSoup(content, 'html5lib')
pagetitle = re.sub(strip_tag_pattern, '', soup.html.head.title.__str__())
print '>>Index Page: '+pagetitle+'<<'
#提取title和url信息
articles = soup.findAll('a', attrs = {'href':True, 'title':True, 'class':'thumb'})
for item in articles:
url = item['href']
self.recycle.put(url[:url.find('art/')])
self.remaincount = self.remaincount - 1
site.close()
return 0
#扩充qProfile
def extendProfile(self):
#满足给定扩展次数则线程结束
if self.remaincount <= 0:
print 'Extend repeat finish'
isRunning = 0
return -1
try:
url = self.input.get(block = False)
except Queue.Empty:
return 1 #用于记录qSaved为空重试的次数
self.output.put(url) #将取出的url直接放入qExtended
time.sleep(0.5) #未必需要,防止访问太频繁被服务器打回
retry = 6 #超时重试5次+初始1次
while retry:
try:
page = urllib2.urlopen(url, timeout=10)
content = page.read()
page.close()
except:
if retry > 1:
print 'Time out for watchers:', url
print 'Retry', 7-retry
retry = retry - 1
if not retry:
print 'Cannot connect to url:', url
return 0
continue
retry = 0
print 'Searching for watchers:', url
#分析页面,使用html5lib,因为lxml有额外空格的bug,严重影响分析
soup = BeautifulSoup(content, 'html5lib')
#去掉包含id="groups-list-xxxx"的div字段,因为这是group列表而非watcher列表
groupsdiv = soup.findAll('div', attrs = {'id' : re.compile('groups-list-.*')})
for item in groupsdiv:
item.extract()
#提取watcher列表
watchers1 = soup.findAll('a', attrs = {'class' : 'u', 'href' : True})
watchers2 = soup.findAll('a', attrs = {'target' : '_self', 'href' : True})
watchers = watchers1 + watchers2
for watcher in watchers:
if not watcher['href'] == url:
try:
self.recycle.put(watcher['href'], timeout = 30) #将watcher的url放入待处理队列等待备份
except Queue.Full:
print 'qProfile Full for 30s'
self.remaincount = self.remaincount - 1
return 0
#线程运行
def run(self):
self.isRunning = 1
global threadStatus
threadStatus += 10000
ret = 0
retry = 0
print 'Extender waiting...\n'
while not self.indexUrl:
continue #等待主线程传入启动指令
if not self.indexUrl == '/recover/':
self.readIndex()
while self.isRunning: