一个可以爬取小说的小爬虫 - 来自业余编程人的第一篇编程分享

内容提要

最近闲来无事,网上找了本小说,翻来覆去的终于找到一个还不错的小说,然而所下载的小说质量实在不讨喜,错误重复随处可见,网站广告也夹杂其中,遂产生了自己爬小说的念头。还好小说的网站都比较简单,基本没有什么反爬措施。期间遇到一个神奇的网站,小说内容是用JS格式化加载的。后来想了一个办法,遇到加载未完成,重新请求即可。
废话少说,我们来看代码。代码使用Python写的。

麻雀虽小,五脏俱全

整个代码分为三个模块,一、主程序模块 mainSpider.py 二、网页下载模块 三、网页解析模块

整个程序所涉及的库如下:
  1. logging – 用于打日志,比Print好用
  2. re – 用于匹配网址域名
  3. os – 用于路径操作
  4. requests – 用于网络请求,比自带的urllib/urllib2 好用很多
  5. bs4 / BeautifulSoup – html解析库,简单易用
  6. lxml – 个人经验,lxml 用作 bs4的解析参数要比 html.parser 好太多了,有时候用默认的html.parser 很难解析出想要的内容
  7. time – 主要使用sleep方法 – requests请求出错时,暂停程序
  8. random – 随机获取 请求头参数,实际上爬小说貌似用不到轮换请求头参数,但是使用了也不为错。
    以上除Python标准库 logging,re,os,time,random 其他库都可以通过 pip install + 库名称 自动下载安装。

为了学习logging模块的用法,特意都打了日志。说句实在话,从门外汉,一窍不通,到自学python的现在,debug全部都是借助print来完成的。从今以后,logging模块开始用起来啦。logging模块比print 爽太多了。哈哈哈。。。
另外,因为程序实在是太小了,所以没有加注释,不过也相信大家都能看得明白。

第一个模块 mainSpider.py
import logging
import re
import os
from htmlDownloaderforNovel import HtmlDownloader
from htmlParserforNovel import NovelParser


logger = logging.getLogger('MyNovelLogger')

logger.setLevel(logging.DEBUG)

formatter = logging.Formatter(fmt='%(asctime)s - %(filename)s - %(name)s - %(levelname)s - %(lineno)d - %(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S')

stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(fmt=formatter)
logger.addHandler(stream_handler)

file_handler = logging.FileHandler(filename='novel_log.log', encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(fmt=formatter)
logger.addHandler(file_handler)

url_to_site = {
    'http://www.jianlaixiaoshuo.com/': 'JianLai',
    'https://www.xbiquge6.com/': 'BiQuGe6',
    'https://www.xbiqugexsw.com/': 'BiQuGeX', # 此网站的如果遇到网速较慢,则小说正文转码也会比较慢,经常遇到正在加载的状况,如无必要,不要从此网站下载小说
    'https://www.biqugg.com/': 'BiQuGG',
    'http://www.quanben.co/': 'QuanBenC',
    'https://www.biquge.info/': 'BiQuGeI'
}

def main_spider(start_url, novel_name=''):
    while True:
        novel_dir = ''
        downloader = HtmlDownloader()

        html = downloader.download_html(url=start_url)
        # 'https://www.xbiquge6.com/76_76449/139368.html'
        logger.debug('正在解析当前网站内容')
        try:
            domain, novel_dir = re.findall('(.*.com/)(.*?\/\d+\/)\d+.html', start_url, re.DOTALL)[0]
            logger.info(novel_dir)
        except IndexError:
            domain_tuple = re.findall('(.*.com/).*.html|(.*.co/).*.html|(.*.info/).*.html', start_url, re.DOTALL)[0]
            # domain = domain_tuple[0] if domain_tuple[0] else domain_tuple[1]
            domain = list(filter(lambda d: True if d else False, domain_tuple))[0]
        logger.debug('当前网站域名解析成功: %s', domain)

        logger.debug('开始解析当前页面,当前页面链接 %s', start_url)
        parser = NovelParser(site=url_to_site.get(domain))

        title, content, next_page = parser.parse_html(html, novel_dir=novel_dir)
        logger.info('当前章节 %s 解析完成', title)

        logger.info('下页页面链接 %s', next_page)

        novel_folder = os.path.join(os.path.dirname(__file__), 'MyNovel')
        if not os.path.exists(novel_folder):
            logger.debug('Novel_folder 不存在,准备创建')
            os.makedirs(novel_folder)
            logger.debug('Novel_folder 创建成功')

        novel_path = os.path.join(novel_folder, novel_name+'.txt')
        logger.info('正在写入本页内容 %s', start_url)
        with open(novel_path, 'a+', encoding='utf-8') as f:
            f.write(title + '\n')
            f.write(content + '\n\n')
        logger.info('本页内容写入完毕')
        if next_page:
            logger.debug('发现新的页面,开始采集新页面内容')
            # return main_spider(next_page, novel_name=novel_name)
            start_url = next_page
        else:
            logger.info('已到达最后一页,整本采集完成|文件地址 %s', novel_path)
            break


if __name__ == '__main__':
    url = 'https://www.biquge.info/1_1760/844140.html'
    name = '武炼巅峰'

    main_spider(url, name)

主函数程序接收两个参数:

  1. 小说第一章的url,随着程序运行,会自动查找下章/页链接,最开始用的是回归调用main_spider 函数下载全本小说,然而发现会栈溢出,导致python崩溃,所以改为while True break 结构了
  2. 小说的名字 name - 下载的txt文件也是以此名字命名
第二个模块 网页下载模块 htmlDownloaderforNovel.py
import requests
import time
import random
import logging

logger = logging.getLogger('MyNovelLogger.htmlDownloader')


class HtmlDownloader:
    def __init__(self):
        logger.debug('正在获取user_agent')
        self.user_agent = self.get_random_agent()
        logger.debug('获取user_agent成功: %s', self.user_agent)

    def download_html(self, url, sleep=5, retry=3, timeout=10, cookies=None, proxy=None):
        if url:
            try:
                session = requests.Session()
                req = session.get(url, headers={'User-Agent': self.user_agent}, timeout=timeout,
                                  cookies=cookies, proxies=proxy)
                logger.info('打开URL成功 - %s', url)
                if '正在加载' in req.text:
                    # for biqugex
                    logger.warning('网站加载未完成')
                    logger.info('开始休息 %d 秒钟', sleep)
                    time.sleep(sleep)
                    logger.info('休息结束,重新开始打开URL: %s', url)
                    return self.download_html(url)

                if req.status_code == 200:
                    req.encoding = req.apparent_encoding
                    logger.info('获取网页内容成功,正在返回网页')
                    return req.text
                elif req.status_code in (404, 502, 503):
                    logger.warning('网站访问失败,错误内容: Error Catch - Error Code: %d', req.status_code)
                    logger.info('开始休息 %d 秒钟', sleep)
                    time.sleep(sleep)
                    logger.info('休息结束,重新开始打开URL: %s', url)
                    return self.download_html(url)
                else:
                    logger.error('网站访问失败,错误内容: Final Error Catch, Error Code: %d', req.status_code)
                    logger.error(req.text)
                    logger.info('开始休息 %d 秒钟', sleep)
                    time.sleep(sleep)
                    logger.info('休息结束,重新开始打开URL: %s', url)
                    return self.download_html(url)

            except (requests.ConnectTimeout, requests.ReadTimeout, requests.ConnectionError):
                logger.exception('Error Occurred')
                logger.debug('正在判断剩余重试次数是否大于0')
                if retry > 0:
                    logger.debug('剩余重试次数大于0')
                    retry -= 1
                    logger.info('开始休息 %d 秒钟', sleep)
                    time.sleep(sleep)
                    logger.info('休息结束,重新开始打开URL: %s', url)
                    return self.download_html(url, retry=retry)
            except Exception:
                logger.exception('Final Error Occurred')
                logger.info('开始休息 %d 秒钟', sleep)
                time.sleep(sleep)
                logger.info('休息结束,重新开始打开URL: %s', url)
                return self.download_html(url)

这就是一个简单的 html下载器了,为了让爬虫更顺畅,采用了一些简单的错误重试手段。

核心代码块,解析器 htmlParserforNovel.py
import logging
from bs4 import BeautifulSoup


logger = logging.getLogger('MyNovelLogger.htmlParserforNovel')

class NovelParser:
    def __init__(self, site):
        self.site = site
        logger.info('当前小说网站为 %s', site)

    def parse_html(self, html, novel_dir=''):
        if self.site == 'JianLai':
            return self._parse_jianlai(html)
        elif self.site == 'BiQuGe6':
            return self._parse_biquge6(html)
        elif self.site == 'BiQuGeX':
            return self._parse_biqugex(html)
        elif self.site == 'BiQuGG':
            return self._parse_biqugg(html, novel_dir=novel_dir)
        elif self.site == 'BiQuGeI':
            return self._parse_biqugei(html)
        elif self.site == 'QuanBenC':
            return self._parse_quanbenc(html)
        else:
            logger.warning('当前网站不支持,请更换网站')
            raise Exception('当前网站不支持,请更换网站')

    def _parse_biqugei(self, html):
        logger.info('正在解析当前页面')
        soup = BeautifulSoup(html, 'lxml')
        title = soup.find('h1').getText().strip()
        content = soup.select_one('#content').getText().replace('    ','\n').strip()
        next_page = soup.select_one('.bottem1').find_all('a')[-3].get('href')

        if not next_page.endswith('.html'):
            next_page = None

        return title, content, next_page

    def _parse_quanbenc(self, html):
        logger.info('正在解析当前页面')
        soup = BeautifulSoup(html, 'lxml')
        title = soup.find('h1').getText().strip()
        content = soup.select_one('.novel_content').getText().replace('    ', '').strip()
        next_page = soup.select_one('.novel_bottom_wap').find_all('a')[-1].get('href')

        if 'index.html' in next_page:
            next_page = None
        else:
            next_page = 'http://www.quanben.co' + next_page
        return title, content, next_page

    def _parse_jianlai(self, html):
        logger.info('正在解析当前页面')
        soup = BeautifulSoup(html, 'lxml')
        title = soup.find('h1').getText().strip()
        content = soup.select_one('#BookText').getText().strip()
        next_page_con = soup.find('div', class_="link xb")
        try:
            next_page = 'http://www.jianlaixiaoshuo.com' + next_page_con.find('a', rel="next").get('href')
        except Exception:
            next_page = None
        logger.info('当前页面解析成功')
        return title, content, next_page

    def _parse_biquge6(self, html):
        logger.info('正在解析当前页面')

        soup = BeautifulSoup(html, 'lxml')
        title = soup.find('h1').getText().strip()

        content = soup.select_one('#content').getText().replace('    ', '\n').strip().rstrip(';')

        next_page = soup.select_one('.bottem1').find_all('a')[-2].get('href')
        if not next_page.endswith('html'):
            next_page = None
        else:
            next_page = 'https://www.xbiquge6.com' + next_page
        logger.info('当前页面解析成功')

        return title, content, next_page

    def _parse_biqugg(self, html, novel_dir):
        logger.info('正在解析当前页面')

        soup = BeautifulSoup(html, 'lxml')
        title = soup.find('h1').getText().strip()

        content = soup.select_one('#content').getText().replace('    ', '\n').strip().rstrip(';')

        next_page = soup.select_one('.bottem1').find_all('a')[-2].get('href')
        if '无下章' in html:
            next_page = None
        else:
            next_page = 'https://www.biqugg.com/' + novel_dir + next_page
        logger.info('当前页面解析成功')

        return title, content, next_page

    def _parse_biqugex(self, html):
        logger.info('正在解析当前页面')

        soup = BeautifulSoup(html, 'lxml')
        # logger.debug('正在获取本页标题。。。')

        title = soup.select_one('h1').getText()
        # logger.debug('本页标题获取完毕:%s', title)
        re_text = '转码页面功能异常,本站不支持转码阅读,点击页面底部[查看原网页]可正常浏览,或通过浏览器访问本页地址: http://www.xbiqugexsw.com/'
        # logger.info('正在解析并处理内容')
        content = soup.select_one('#content').getText().replace(re_text, '\n').replace('  ', '').strip()
        # logger.debug('内容解析且处理完毕')
        # logger.debug('正在获取next page 链接')
        next_page = soup.select_one('.bottem1').find_all('a')[-1].get('href')

        if not next_page.startswith('javascript'):
            next_page = 'https://www.xbiqugexsw.com' + next_page
            # logger.debug('下页链接获取完毕:%s', next_page)
        else:
            next_page = None
            # logger.debug('当前为最后一页, 程序结束')
        logger.info('当前页面解析成功')
        return title, content, next_page
        

不得不说,这块代码写得不是很满意,啰哩啰嗦的东西太多,不够优雅,后续有时间有精力的话再做优化吧。

总结

这是本人自开通CSDN账号以来的第一篇爬虫博文。还不是特别熟悉Markdown语法,所以写的比较糙,还请多多包涵。
本人开通CSDN的初衷是为了写一些自己亚马逊卖家工作相关的一些爬虫和日常工作工具类的代码。但是总觉得以前写的代码太烂(粗略算来,从当初为了改进工作效率开始自学Python,居然已经过去3年了,期间也是偶有断续),加之近期忙于本职工作也没有太多时间重新优化以前的代码。于是就以这个小项目开启本人的编程博文分享历程。
目前为止可以从六个网站采集小说,链接如下。
本文代码可以自行扩充,只需要更改添加 url_to_site 字典以及解析器里面的相关内容即可,当然也需要注意函数主程序中的对于网址域名所采用的正则表达式规则
http://www.jianlaixiaoshuo.com/
https://www.xbiquge6.com/
https://www.xbiqugexsw.com/
https://www.biqugg.com/
http://www.quanben.co/
https://www.biquge.info/

如果亚马逊卖家相关的朋友看到了此篇博文,且有对亚马逊卖家相关的工具开发之类的兴趣的朋友,可以私信交流。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值