先说说需求:我想做的一个爬虫,它通过爬取“小说推荐吧”里面的所有帖子,从中找到被提到最多的小说,为什么会有这种需求呢,因为我书荒的时候就会去这个贴吧,看哪本小说被推荐的多,我就看哪本了。
要实现这个功能,我碰到的问题主要有以下几个:
如果判断一个词组表示的是一本小说;
采用什么搜索算法;
百度贴吧浏览10页以后的内容需要登录,如何在爬虫里面登录贴吧;
如何判断一个词组表示的是一本小说
对于第一个问题,我是这样解决的,从一个包含很多流行小说的网站上爬取部分小说名字,我是在豆瓣读书上找的,找了几个标签总共2000~3000本小说 的名字,然后将这些小说名字与网页中的文字进行比对,如果有匹配的就表示是一本小说了,虽说无法判断所有小说,但能提取出流行小说就够了。最后匹配了部分 网页发现这种有些问题,比如有小说名叫《战士》或《毒》,这种词出现的太多了,而且大部分时候想表达的意思不是小说名,我最终的处理方案是这些匹配的词后 必须跟特定的符号或词语,比如别人提到“毒写的不错”或“《毒》”,抑或是“毒,战士,活着都可以”,我才认定别人提到的应该是小说名了,当然,这样判断不一定准确,但应该有80%左右是对的。
采用什么搜索算法
多模式匹配,有两种比较流行的算法,AC多模匹配和Wu-Manber算法,前一种似乎不太适合非ascii字符的情形,所有我最终主要采用的是Wu- Manber算法,这种算法的实现思路大概是这样的(因为这不是主要,所以写的比较简单,可以自行百度):比如有 abcdef,ijkhlsd这两个模式串需要在一段文字中进行匹配,我可以设定一个值k=4(该值应该小于最小模式串的长度),和block=2(该值应该小于k值),有两个 表,ShiftTable和HashTable,在ShiftTable中放的是偏移值,对于串abcdef,分别计算ab,bc,cd,的Hash值, 然后在ShiftTable中Hash值对应的地方放上偏移值,比如cd的偏移值是0,bd是1,ab是2,然后HashTable对应偏移为0的地方存 放最开始block大小字符串的哈希值和相应模式串的索引。匹配过程是这样的:字符串从开头中每一个block长度求hash值,找到 ShiftTable中相应地方的偏移值,如果不是0,则将指针前移偏移值个位置,如果是0,则和HashTable中相应地方存放的前缀Hash值进行 比对,如果比对相等,再进行全字匹配。
如何在爬虫里面登录贴吧
根据前人总结的,百度登陆有三个步骤:
Get方式连接https://passport.baidu.com/v2/api/?getapi&class=login&tpl=mn&tangram=false,得到cookie
Get方式连接https://passport.baidu.com/v2/api /?getapi&class=login&tpl=mn&tangram=false,得到的内容中包含一个tocken,解 析出这个tocken的值
Post以下内容:
post_data = {'username':self.usrname,
'password':self.passwd,
'token':self.token,
'charset':'UTF-8',
'callback':'parent.bd12Pass.api.login._postCallback',
'index':'0',
'isPhone':'false',
'mem_pass':'on',
'loginType':'1',
'safeflg':'0',
'staticpage':'https://passport.baidu.com/v2Jump.html',
'tpl':'mn',
'u':'http://www.baidu.com',
'verifycode':'',}
可以通过在最终连接的页面中查找用户名来判断登陆是否成功。
然后是实现过程,我使用的是scrapy爬虫框架,登陆过程和爬取文字代码:
class BdSpider(BaseSpider):
name="baidu"
usrname="395318621@qq.com"
passwd="********"
usrnick="c395318621"
allowed_domains = ['baidu.com']
logined=False
token=''
curpage=0
def __init__(self,keywords,startpage,endpage):
#self.testurl="http://tieba.baidu.com/f?kw=%s&pn=%d"%(keywords,(startpage-1)*50)
UrlCode=urllib.quote(keywords)
self.testurl="http://tieba.baidu.com/f?kw=%s&pn=%d"%(UrlCode,(int(startpage)-1)*50)
print "[user]",self.testurl
self.startpage=int(startpage)
self.endpage=int(endpage)
def start_requests(self):
cookiename='baidu%s.cookie'%(self.usrname)
cj=cookielib.LWPCookieJar()
try:
cj.revert(cookiename)
self.logined=True
print "Has logined before"
except Exception,e:
print e
if self.logined:
return [Request(url=testurl,callback=self.check_page)]
else:
qurl="https://passport.baidu.com/v2/api/?getapi&class=login&tpl=mn&tangram=false"
return [Request(url=qurl,callback=self.get_cookie,dont_filter=True)]
def get_cookie(self,response):
print "int get_cookie"
qurl="https://passport.baidu.com/v2/api/?getapi&class=login&tpl=mn&tangram=false"
return [Request(url=qurl,callback=self.get_tocken,dont_filter=True)]
def get_tocken(self,response):
print "in get_tocken"
login_tokenStr = """bdPass.api.params.login_token='(.*?)';"""
login_tokenObj = re.compile(login_tokenStr,re.DOTALL)
matched_objs=login_tokenObj.findall(response.body)
print response
if matched_objs:
self.token=matched_objs[0]
print "token:",self.token
post_data = {'username':self.usrname,
'password':self.passwd,
'token':self.token,
'charset':'UTF-8',
'callback':'parent.bd12Pass.api.login._postCallback',
'index':'0',
'isPhone':'false',
'mem_pass':'on',
'loginType':'1',
'safeflg':'0',
'staticpage':'https://passport.baidu.com/v2Jump.html',
'tpl':'mn',
'u':self.testurl,
'verifycode':'',}
#path = 'http://passport.baidu.com/?login'
path = 'http://passport.baidu.com/v2/api/?login'
headers = {
"Accept": "image/gif, */*",
"Referer": "https://passport.baidu.com/v2/?login&tpl=mn&u=%s"%(self.testurl),
"Accept-Language": "zh-cn",
"Content-Type": "application/x-www-form-urlencoded",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)",
"Host": "passport.baidu.com",
"Connection": "Keep-Alive",
"Cache-Control": "no-cache"}
return [FormRequest(url=path,formdata=post_data,headers=headers,callback=self.check_page) ]
def check_page(self,response):
print "[user]in check_page"
#self.start_urls.append(self.testurl)
return [Request(url=self.testurl,callback=self.parse,dont_filter=True)]
def checkLogin(self,response):
reUser=re.compile(self.usrnick)
matched=reUser.findall(response.body)
if matched is not None:
print "[user] login baidu ok"
return True
else:
print "[user] login failed"
return False
def parse(self,response):
baseurl="http://tieba.baidu.com"
if self.checkLogin(response):
response_selector=HtmlXPathSelector(response)
next_link=response_selector.select('//div[@id="frs_list_pager"]/a[@class="next"]/@href')
if next_link:
print "[user]next_link:",next_link.extract()[0]
link=baseurl+next_link.extract()[0]
rePage=re.compile(r'pn=(\d+)')
print "[user]link",link
p=rePage.findall(link)
print "rePage",p
if p:
page=int(p[0])/50+1
print "dealing with page:%d"%(page)
#test
if page<=self.endpage and page>=self.startpage:
yield Request(url=link,callback=self.parse)
tie=response_selector.select(u'//div[contains(@class,"threadlist_text threadlist_title j_th_tit notStarList")]/a[contains(@class,"j_th_tit")]/@href')
#print "ite",tie
for item in tie.extract():
print "[user]tie",item
yield Request(url=baseurl+item,callback=self.parse_tie)
else:
print "[user]checklogin failed"
def parse_tie(self,response):
print "[user] in parse_tie"
response_selector=HtmlXPathSelector(response)
yield self.get_it(response)
szPage=response_selector.select(u'//li[@class="l_reply_num"]/span[@class="red"]/text()')
if not szPage:
print "[user] get page num failed"
else:
pages=int(szPage.extract()[1])
#test
if pages>10:
pages=10
for i in range(2,pages+1):
yield Request(url=response.url+"?pn=%d"%(i),callback=self.parse_others)
def parse_others(self,response):
print "[user] in parse_others"
yield self.get_it(response)
def get_it(self,response):
print "[user] in get_item"
item=BdItem()
response_selector=HtmlXPathSelector(response)
title=response_selector.select(u'//div[@id="j_core_title_wrap"]\
/div[contains(@class,"core_title")]/h1/text()').extract()[0]
if len(title)>1:
print "[user] parse tie title failed"
item['turl']=response.url
item['ttitle']=title[0]
"""
dirRe=re.compile(r"/p/(\d+)")
pageRe=re.compile(r"pn=(\d+)")
dirname=dirRe.findall(response.url)[0]
page=pageRe.findall(response.url)
if len(page) is 0:
page=['1']
try:
os.mkdir(dirname)
except Exception as e:
pass
"""
content=response_selector.select(u'//div[contains(@class,"d_post_content_main")]/div[contains(@class,"p_content_nameplate")]/cc/div/text()').extract()
if len(content)==0:
print "[user] get content failed."
item['tresponse']=[]
for text in content:
item['tresponse'].append(text.encode("utf8"))
return item
最后,我采用redis数据库来存储爬取的结果,结果的结构大概是这样:
{
bookname: "xxxx"
[
{url:"http:\xxxx.xxx.xx", times: n},#书本出现url和出现次数
{url:"http:\xxxx.xxx.xx", times: n},
...
]
}
项目地址 https://github.com/chengxiayan/BaiduSpider/tree/master/bd