本篇介绍爬取豆瓣日记的策略分析、源码剖析、知识点汇总
(先放个封面图)
一代偶像 张国荣
</br>
本来想用Scrapy来爬的,结果连续被ban。
设置动态UA、加Cookies、用vpn也无济于事,辗转一天多,累觉不爱。
反爬机制不要太强啊,给豆瓣小组点个赞,跪服!!
</br>
不过,最后还是用一般方法的解决了
说来也奇怪,大概因为Scrapy是异步多线程,所以容易被发现吧。
</br>
一、目标
爬取豆瓣所有关于张国荣的日记
1、获取每一篇标题、作者、链接、点赞数、发布时间,将数据存入excel
2、获取所有日记内容,存入txt
3、将所有文章汇总,jieba分词,做成词云图
</br>
二、过程
1、分析网页及源码
豆瓣首页搜索框中输入 张国荣 回车页面跳转 以第一篇日记为例,点开 查看源码,分析得到URL 顺带分析下其它得到具体日记的URL,接下来就是翻页
下拉看到显示更多 第2页 start=20 第3页start=40
发现规律了么,每一次刷新,URL=https://www.douban.com/j/search?q=张国荣&start=n&cat=1015, 其中n=0,20,40...那我们完全可以直接这样构造,每一次获取20项内容(为一个list),不用一个一个来了,手动试了下共有n最大为2000
</br>
2、具体步骤及问题解决
a、构造URL,解析json格式
代码这样,注意range函数:
for i in range(0,2001,20):
response=self.get_source(url='https://www.douban.com/j/search?q=张国荣+&start='+str(i)+'&cat=1015')
main=json.loads(response.text)['items']
mains.append(main)
</br>
b、解析每一项获得id,由id构造每一篇链接
直接在源码上看的话,眼花,我是for循环打印到IDE中看的
用到BeautifulSoup和正则:
links=[]
#get_main()是上面解析数据的函数
#得到包含20项内容的大list
mains=self.get_main()
for main in mains:
#每一页有20项
for j in range(0,20):
#先找到所有h3标签
soup=BeautifulSoup(main[j],'lxml').find('h3')
#在其中匹配id
pattern=re.compile(r'<a href=.*?sid:(.*?)>(.*?)</a>', re.S)
items=re.findall(pattern, str(soup))
for item in items:
id=item[0][0:-3].strip()
link='https://www.douban.com/note/'+str(id)+'/'
#所有链接放入list中
links.append(link)
time.sleep(0.2)
</br>
看样子是没错,然而,运行时却出问题。
List index out of range(抱歉忘记截图了)
搞了好久,终于发现了,比如start=120时:
也就是说,并不是“每一页”都有20项,有的少于20项。我以为120这里是个特殊,后面发现很多别的页面也有这样的状况。不能愉快地用range函数for循环了,好气呀。
还好本宝宝机智,想到itertools迭代器,可是超出索引范围会报IndexError。
想了下,加个try...except...,解决!
#记得导入itertools模块
import itertools
links=[]
mains=self.get_main()
for main in mains:
try:
for j in itertools.count(0):
xxxxxxxx(和上面那一堆一样)
except IndexError:
continue
</br>
c、提取每一页中标题、作者、日期、内容、点赞数等信息
具体项目放入名为data的list中,多个data放入名为container的大list中
程序跑起来没有问题,可是打开点赞数那一栏,却是空的。跑去分析源码,发现原来没那么简单。如何入手解决,想了很久。
突然想到之前貌似看到过什么:
发现了么,原来豆瓣分pc端和移动端,移动端是m.douban.com形式(m 即为move),试了一下,这两个页面还是有些不一样的。
既然pc端获取不到点赞数,那么何不试试移动端。于是出现这样:
果然有差别,看到红框框里这个data-needlogin应该觉悟,这是需要登录才能看到点赞数呀(或者喜欢),于是模拟登录。试了才知道,post方法根本就行不通。
没辙了么,别忘了我们还有个简单的,用Cookies模拟登录也可以呀,再加个Session保持会话。
#这里用的首页的cookies
cookies='bid=6xgbcS6Pqds; ll="118318"; viewed="3112503"; gr_user_id=660f3409-0b65-4195-9d2f-a3c71573b40f; ct=y; ps=y; _ga=GA1.2.325764598.1467804810; _vwo_uuid_v2=112D0E7472DB37089F4E96B7F4E5913D|faf50f21ff006f877c92e097c4f2819c; ap=1; push_noty_num=0; push_doumail_num=0; _pk_ref.100001.8cb4=%5B%22%22%2C%22%22%2C1491576344%2C%22http%3A%2F%2Fwww.so.com%2Flink%3Furl%3Dhttp%253A%252F%252Fwww.douban.com%252F%26q%3D%25E8%25B1%2586%25E7%2593%25A3%26ts%3D1491459621%26t%3Df67ffeb4cd66c531150a172c69796e0%26src%3Dhaosou%22%5D; __utmt=1; _pk_id.100001.8cb4=41799262efd0b923.1467804804.35.1491576361.1491567557.; _pk_ses.100001.8cb4=*; __utma=30149280.325764598.1467804810.1491566154.1491576346.34; __utmb=30149280.3.10.1491576346; __utmc=30149280; __utmz=30149280.1491469694.24.15.utmcsr=baidu|utmccn=(organic)|utmcmd=organic; __utmv=30149280.12683; dbcl2="126831173:APSgA3NPab8"'
headers={'User_Agent':'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36','Cookie':cookies}
s=requests.Session()
response=s.get(url,headers=headers)
requests.adapters.DEFAULT_RETRIES=5
然后就是这样,加Cookies模拟登录后,用pc端的URL也可以(后面都抓的pc端)。
到了提取具体信息的环节。还需要要注意的是,“喜欢”那一栏可能没有数据。
而且分析源码可以知道,有人点赞的有<span class="fav-num".?>(.?)</span>这一项,点赞为0的是没有这一项的。
陷阱真多-_-|| 分情况讨论匹配正则。
links=self.get_link()
container=[]
n=1
for link in links:
html=self.get_source(link).text
data=[]
#得先判断有无点赞
patternNum=re.compile(r'class="fav-num"',re.S)
Num=re.search(patternNum,html)
if Num:
pattern=re.compile(r'<h1>(.*?)</h1>.*?<a href=.*?class="note-author">(.*?)</a>.*?<span class="pub-date">(.*?)</span>.*?<div class="note" id="link-report">(.*?)</div>.*?<span class="fav-num".*?>(.*?)</span>',re.S)
items=re.findall(pattern,html)
for item in items:
data.append(item[0])
data.append(link)
data.append(item[1])
data.append(item[2])
data.append(item[4])
data.append(self.tool.replace(item[3]))
else:
pattern=re.compile(r'<h1>(.*?)</h1>.*?<a href=.*?class="note-author">(.*?)</a>.*?<span class="pub-date">(.*?)</span>.*?<div class="note" id="link-report">(.*?)</div>',re.S)
items=re.findall(pattern, html)
for item in items:
data.append(item[0])
data.append(link)
data.append(item[1])
data.append(item[2])
#无点赞时用0代替
data.append('0')
data.append(self.tool.replace(item[3]))
container.append(data)
time.sleep(0.5)
n+=1
细心的盆友可能会叫住了,等等,这个self.tool.replace()怎么回事?
应该想到,豆瓣日记除了文字,还有部分可能是含图片甚至音频等
所以定义了一个Tool类,清洗多余的div、img、td等标签
class Tool():
def replace(self,x):
x=re.sub(re.compile('<br>|</br>||<p>|</p>|<td>|</td>|<tr>|</tr>|</a>|<table>|</table>'), "", x)
x=re.sub(re.compile('<div.*?>|<img.*?>|<a.*?>|<td.*?>'), "", x)
return x.strip()
class Spider():
def __init__(self):
self.tool=Tool()
</br>
d、保存数据入excel和txt
目前获得标题、作者、链接、发布时间、点赞数、文章内容共6项。
我们将前5项录入excel,最后一项存入txt。
可以先将文章录入txt,接着list中去除文章一项, 最后录入excel。用到remove方法。
一定警惕list=list.remove(i)这种写法!
为什么呢,看下面:
list=list.remove(i)其实是默认返回None,然后。。。就是个大坑啊,基础不牢地动山摇论小白学习的心酸血泪史≧﹏≦
e、结巴分词及词云图
这回共2000多篇文章,字数太多不可能再用语料库在线统计词频,所以采用统计权重的方法,再乘以10000放大。
这里参考了博客:http://www.jianshu.com/p/6a285dfa3d87
取了权重最大的前150个词(后面作词云时又相应去掉了一些)
import jieba.analyse
f=open(r'F:\Desktop\DouBan.txt','r')
content=f.read()
try:
jieba.analyse.set_stop_words(r'F:\Desktop\TingYong.txt')
tags=jieba.analyse.extract_tags(content,topK=150, withWeight=True)
for item in tags:
print item[0]+'\t'+str(int(item[1]*10000))
finally:
f.close()
TingYong.txt是停用词表,可以在网上下载到,还可以自己修改添加。
词云就差不多和之前一样,可见文章 http://www.jianshu.com/p/462d32450b5f
</br>
三、代码
完整版代码我放github上了:https://github.com/LUCY78765580/Python-web-scraping/tree/master/DouBan
(如果您觉得有用,star我也可以的哟~)
豆瓣反爬比较厉害,半夜抓取可能比较好。
</br>
四、总结
最后总结一下本文关键
1、源码分析:首先分析网站及URL结构;发现豆瓣不仅有pc端还有移动端,而且两者页面不太一样;分析出不登录的话是获取不到点赞数的,同时有无点赞页面代码是不一样的。
2、抓包获取URL,解析json格式数据
3、Cookies模拟登录,Session保持会话
4、数据提取与清洗:用到正则re、BeautifulSoup及自定义的Tool类
5、用jieba.analyse编制程序,结巴分词、词频统计,词云制作
6、基础知识,range(start,end,n)、itertools.count(i)、remove等方法
陷阱与知识点的大杂烩
用来作为入门阶段的复习总结倒是不错的。本篇就是这样啦~
网友评论