从0开始的爬虫实践项目 (1):豆瓣用关键词搜索书籍

141 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

诸神缄默不语-个人CSDN博文目录

本文写于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&amp;query=%E4%B8%AD%E4%B8%96%E7%BA%AA&amp;cat_id=1001&amp;type=search&amp;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&amp;query=%E4%B8%AD%E4%B8%96%E7%BA%AA&amp;cat_id=1001&amp;type=search&amp;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&amp;query=%E4%B8%AD%E4%B8%96%E7%BA%AA&amp;cat_id=5015&amp;type=search&amp;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&amp;query=%E4%B8%AD%E4%B8%96%E7%BA%AA&amp;cat_id=5015&amp;type=search&amp;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,对数据的处理、对各书籍下一步信息爬取什么的就留给下一次吧!