开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
本文写于2023.2.5,由于爬虫技术受前端开发方式变化影响很大,因此时效性很高,后来的读者可能需要根据最新的情况对代码进行修改。
任务背景:找找“中世纪”主题适于读的书
任务分解:用豆瓣网站搜索“中世纪”,并扒下其“书籍”分类中的各项条目(书籍)信息
(原则上由于这个真的很简单,所以感觉用八爪鱼之类的就能用……但是我没下。我就想整个简单点的sei)
网址:
https://www.douban.com/search?cat=1001&q=%E4%B8%AD%E4%B8%96%E7%BA%AA
(q后面就是“中世纪”)
豆瓣是比较好扒的网站,难点就是其翻页不是直接翻页,而需要动态获取数据:
1. 先试试直接扒能扒下来啥东西
url='https://www.douban.com/search?cat=1001&q=%E4%B8%AD%E4%B8%96%E7%BA%AA'
content=requests.get(url)
print(content.content)
输出:b''
就啥都扒不下来呗。
2. 加上User-Agent直接扒试试
获取UA的方法:右键检查-Network-刷新网页-点击第一个元素,复制其User-Agent
出于隐私问题,以下示例都用User_Agent字符串对象来代替原始的UA
url='https://www.douban.com/search?cat=1001&q=%E4%B8%AD%E4%B8%96%E7%BA%AA'
headers={'User-Agent':User_Agent}
content=requests.get(url,headers=headers)
html_text=content.content
扒出东西来了,但是打印出来会发现是乱码(也不是传统意义上的乱码,反正就是需要解码的意思,我忘了那玩意叫啥,你能理解我的意思就行),而且格式很混乱,需要解码。
3. 解码网页文本
其实可以直接用re的,下次有时间写个用纯re的版本吧,在这里我用的是BeautifulSoup(Beautiful Soup 4.4.0 文档 — Beautiful Soup 4.2.0 中文 文档)
用BS解析HTML代码,然后先用文本文档看一下整理后的格式(我用的是VSCode的Jupyter Notebook,所以直接输出的话格式会乱(因为会自动换行),用网页版Jupyer Notebook似乎没有这个问题):
html_text2=html_text.decode('utf-8')
soup=BeautifulSoup(html_text2, 'html.parser')
with open('blank.txt','w') as f:
f.write(soup.prettify())
可以比较容易地发现每个书目的格式都类似这样,由<div class="result-list">
包裹:
<div class="result">
<div class="pic">
<a class="nbg" href="https://www.douban.com/link2/?url=https%3A%2F%2Fbook.douban.com%2Fsubject%2F35546520%2F&query=%E4%B8%AD%E4%B8%96%E7%BA%AA&cat_id=1001&type=search&pos=0" onclick="moreurl(this,{i: '0', query: '%E4%B8%AD%E4%B8%96%E7%BA%AA', from: 'dou_search_book', sid: 35546520, qcat: '1001'})" target="_blank" title="欧洲中世纪史(第11版)">
<img src="https://img1.doubanio.com/view/subject/s/public/s33971630.jpg"/>
</a>
</div>
<div class="content">
<div class="title">
<h3>
<span>
[书籍]
</span>
<a href="https://www.douban.com/link2/?url=https%3A%2F%2Fbook.douban.com%2Fsubject%2F35546520%2F&query=%E4%B8%AD%E4%B8%96%E7%BA%AA&cat_id=1001&type=search&pos=0" onclick="moreurl(this,{i: '0', query: '%E4%B8%AD%E4%B8%96%E7%BA%AA', from: 'dou_search_book', sid: 35546520, qcat: '1001'})" target="_blank">
欧洲中世纪史(第11版)
</a>
</h3>
<div class="rating-info">
<span class="allstar45">
</span>
<span class="rating_nums">
9.2
</span>
<span>
(310人评价)
</span>
<span class="subject-cast">
朱迪斯·M.本内特 / 林盛 / 上海社会科学院出版社 / 2021
</span>
</div>
</div>
<p>
"“中世纪是欧洲历史上一个灾难性的时代。”这种过时而不可信的说法广为流传,时至今日,提及中世纪,人们脑海中浮现的依旧是“黑暗”“落后”这样悲惨的印象。但实际上...
</p>
</div>
</div>
可以从中提取到每个书目的封面链接、sid(href
没啥用,每本书跳转后的链接都是https://book.douban.com/subject/sid/
的格式)、书籍标题、评分、评分人数、作者出版社出版年代(这是一整个字符串)(简介不全,所以我就不在这里采集了):
for result in soup.find_all('div'):
if 'class' in result.attrs and 'result-list' in result['class']:
for book in result.children:
#有的是bs4.element.NavigableString(空字符串),有的是bs4.element.Tag(书)
if isinstance(book,bs4.element.Tag):
book_factor=[]
book_information=book.find('a',class_='nbg')
picture_link=book_information.img['src']
sid=re.findall(r'sid: (.*?),',book_information['onclick'])[0]
title=book_information['title']
rate=book.find('span',class_='rating_nums')
rating=rate.text
rating_num=rate.next_sibling.next_sibling.text
subject=book.find('span',class_='subject-cast').text
print(picture_link,sid,title,rating,rating_num,subject)
但这个代码是有问题的,会报错,因为最后一个bs4.element.Tag
类型的book对象是这个:
<div class="result-list-ft">
<a class="j a_search_more" data-cat="1001" data-q="中世纪" data-start="20" href="#more">显示更多</a>
</div>
这显然是个动态网页问题,进入下一节解决。
先把之前的数据存储为pd.DataFrame格式,然后存储到CSV文件中:
book_list = []
for result in soup.find_all('div'):
if 'class' in result.attrs and 'result-list' in result['class']:
for book in result.children:
#有的是bs4.element.NavigableString(空字符串),有的是bs4.element.Tag(书)
if isinstance(book,bs4.element.Tag):
book_factor=[]
book_information=book.find('a',class_='nbg')
book_factor.append(book_information.img['src'])
book_factor.append(re.findall(r'sid: (.*?),',book_information['onclick'])[0])
book_factor.append(book_information['title'])
rate=book.find('span',class_='rating_nums')
book_factor.append(rate.text)
book_factor.append(rate.next_sibling.next_sibling.text)
book_factor.append(book.find('span',class_='subject-cast').text)
book_list.append(book_factor)
#(上面的代码会报错,但在Jupyter Notebook里已经存储好的数据会保存在内存中,所以我就不写debug语句,而是直接往下写了)
book_df=pd.DataFrame(book_list,columns=['picture_link','sid','title','rating','rating_num','subject'])
book_df.to_csv('book.csv',index=False)
4. 解决“显示更多”动态网页问题
首先在浏览器的开发者工具Network部分点击clear,然后点一遍显示更多,可以发现这个除了第一条之外,全都是JPG……
豆瓣开发者还是挺实诚的(诚恳)
Request URL是https://www.douban.com/j/search?q=%E4%B8%AD%E4%B8%96%E7%BA%AA&start=20&cat=1001
,那个%E4%B8%AD%E4%B8%96%E7%BA%AA
是中文“中世纪”的URL编码形式,cat
就是书籍,start
是开始项。
Request Headers里面的内容加到之前的headers对象里,我加了Host
Referer
Cookie
这几项之后就可以了。(这几个都是不用改的,所以下文代码里就不会出现怎么加的内容了)
直接用这个扒数据:
url='https://www.douban.com/j/search?q=%E4%B8%AD%E4%B8%96%E7%BA%AA&start=20&cat=1001'
content2=requests.get(url,headers=headers).content
显然这是个JSON格式的数据,其中items
键是20项元素的列表,每一项值就是HTML代码,然后别的就跟上一节中的差不多了,处理后存储到CSV文件中:
(这里还出现了一个由于评论人数不足,所以没有评分的异常处理)
soup2=json.loads(content2)
book_list=[]
for book in soup2['items']:
book_factor=[]
soup3=BeautifulSoup(book,'html.parser')
book_information=soup3.find('a',class_='nbg')
book_factor.append(book_information.img['src'])
book_factor.append(re.findall(r'sid: (.*?),',book_information['onclick'])[0])
book_factor.append(book_information['title'])
rate=soup3.find('span',class_='rating_nums')
try:
book_factor.append(rate.text)
book_factor.append(rate.next_sibling.next_sibling.text)
except AttributeError:
book_factor.append('(评价人数不足)')
book_factor.append('(评价人数不足)')
book_factor.append(soup3.find('span',class_='subject-cast').text)
book_list.append(book_factor)
book_df=pd.DataFrame(book_list,columns=['picture_link','sid','title','rating','rating_num','subject'])
book_df.to_csv('book.csv',index=False,header=False,mode='a')
然后我们随机一下,每隔随机的1-11秒就往下翻一页,一直翻到没法翻(也就是要么翻到最大限制,要么被ban。被ban当然就直接停了。最大限制的话,我看了一下,默认的最大限制是2000条,差不多了,够多了):
index=20
while True:
time.sleep(1+random.random()*10)
index+=20
url='https://www.douban.com/j/search?q=%E4%B8%AD%E4%B8%96%E7%BA%AA&start='+str(index)+'&cat=1001'
content2=requests.get(url,headers=headers).content
soup2=json.loads(content2)
book_list=[]
for book in soup2['items']:
book_factor=[]
soup3=BeautifulSoup(book,'html.parser')
book_information=soup3.find('a',class_='nbg')
book_factor.append(book_information.img['src'])
book_factor.append(re.findall(r'sid: (.*?),',book_information['onclick'])[0])
book_factor.append(book_information['title'])
rate=soup3.find('span',class_='rating_nums')
try:
book_factor.append(rate.text)
book_factor.append(rate.next_sibling.next_sibling.text)
except AttributeError:
book_factor.append('(评价人数不足)')
book_factor.append('(评价人数不足)')
book_factor.append(soup3.find('span',class_='subject-cast').text)
book_list.append(book_factor)
book_df=pd.DataFrame(book_list,columns=['picture_link','sid','title','rating','rating_num','subject'])
book_df.to_csv('book.csv',index=False,header=False,mode='a')
print('翻完第'+str(index)+'条书目')
然后发现了一个新情况,是这个:
<div class="result">
<div class="pic">
<a class="nbg" href="https://www.douban.com/link2/?url=http%3A%2F%2Fread.douban.com%2Fcolumn%2F4250658%2F%3Fdcs%3Ddouban-search&query=%E4%B8%AD%E4%B8%96%E7%BA%AA&cat_id=5015&type=search&pos=576" onclick="moreurl(this,{i: '576', query: '%E4%B8%AD%E4%B8%96%E7%BA%AA', from: 'dou_search_column', sid: 4250658, qcat: '1001'})" target="_blank" title="透明的世界">
<img alt="透明的世界" height="71.56px" src="https://pic.arkread.com/cover/column/f/4250658.1653639830.jpg!cover_default.jpg" width="48px"/>
</a>
</div>
<div class="content">
<div class="title">
<h3>
<span>[自出版]</span>
<a href="https://www.douban.com/link2/?url=http%3A%2F%2Fread.douban.com%2Fcolumn%2F4250658%2F%3Fdcs%3Ddouban-search&query=%E4%B8%AD%E4%B8%96%E7%BA%AA&cat_id=5015&type=search&pos=576" onclick="moreurl(this,{i: '576', query: '%E4%B8%AD%E4%B8%96%E7%BA%AA', from: 'dou_search_column', sid: 4250658, qcat: '1001'})" target="_blank">
透明的世界
</a>
<span class="ic-mark ic-read-mark">有电子版</span>
</h3>
<span>玄幻·奇幻连载 / 洛逸轩 / 已更新 10 篇</span>
</div>
<p>精壮的国王无端猝死,
年轻的剑士初露峥嵘,
谁的哀雪洒满洛城?
剑之都上暗流涌动,
大魔导师为何忧心忡忡?
女巫的狂舞预言了英雄的结局。</p>
</div>
</div>
报的bug是:
AttributeError Traceback (most recent call last)
<ipython-input-151-8442c0394b9b> in <module>
23 book_factor.append('(评价人数不足)')
24
---> 25 book_factor.append(soup3.find('span',class_='subject-cast').text)
26 book_list.append(book_factor)
27
AttributeError: 'NoneType' object has no attribute 'text'
无语,所以从这里继续往下写:
index=540
while True:
time.sleep(1+random.random()*10)
index+=20
url='https://www.douban.com/j/search?q=%E4%B8%AD%E4%B8%96%E7%BA%AA&start='+str(index)+'&cat=1001'
content2=requests.get(url,headers=headers).content
soup2=json.loads(content2)
book_list=[]
for book in soup2['items']:
book_factor=[]
soup3=BeautifulSoup(book,'html.parser')
book_information=soup3.find('a',class_='nbg')
book_factor.append(book_information.img['src'])
book_factor.append(re.findall(r'sid: (.*?),',book_information['onclick'])[0])
book_factor.append(book_information['title'])
rate=soup3.find('span',class_='rating_nums')
try:
book_factor.append(rate.text)
book_factor.append(rate.next_sibling.next_sibling.text)
except AttributeError:
book_factor.append('(评价人数不足)')
book_factor.append('(评价人数不足)')
try:
book_factor.append(soup3.find('span',class_='subject-cast').text)
except AttributeError:
book_factor.append('(无作者出版社出版年份信息)')
book_list.append(book_factor)
book_df=pd.DataFrame(book_list,columns=['picture_link','sid','title','rating','rating_num','subject'])
book_df.to_csv('book.csv',index=False,header=False,mode='a')
print('翻完第'+str(index)+'条书目')
翻到2000多条,然后搜了一下中世纪关键词,发现差不多到600来条之后的就跟中世纪没啥关系了……也不知道是因为反爬还是因为,就是只有这么多了……所以就直接打断了。
OK,对数据的处理、对各书籍下一步信息爬取什么的就留给下一次吧!