Beispiel #1
0
class BaiduSpider(BaseSpider):
    def __init__(self) -> None:
        """爬取百度的搜索结果.

        本类的所有成员方法都遵循下列格式:

            {
                'results': <一个列表,表示搜索结果,内部的字典会因为不同的成员方法而改变>,
                'total': <一个正整数,表示搜索结果的最大页数,可能会因为搜索结果页码的变化而变化,因为百度不提供总共的搜索结果页数>
            }

        目前支持百度搜索,百度图片,百度知道,百度视频,百度资讯,百度文库,百度经验和百度百科,并且返回的搜索结果无广告。继承自``BaseSpider``。

        BaiduSpider.`search_web(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度网页搜索

        BaiduSpider.`search_pic(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度图片搜索

        BaiduSpider.`search_zhidao(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度知道搜索

        BaiduSpider.`search_video(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度视频搜索

        BaiduSpider.`search_news(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度资讯搜索

        BaiduSpider.`search_wenku(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度文库搜索

        BaiduSpider.`search_jingyan(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度经验搜索

        BaiduSpider.`search_baike(self: BaiduSpider, query: str) -> dict`: 百度百科搜索
        """
        super().__init__()
        # 爬虫名称(不是请求的,只是用来表识)
        self.spider_name = "BaiduSpider"
        # 设置请求头
        self.headers = {
            "User-Agent":
            "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
            "Referer": "https://www.baidu.com",
            "Accept":
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
            "Accept-Encoding": "gzip, deflate, br",
            "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
        }
        self.parser = Parser()

    def search_web(self, query: str, pn: int = 1, exclude: list = []) -> dict:
        """百度网页搜索.

        - 简单搜索:
            >>> BaiduSpider().search_web('搜索词')
            {
                'results': [
                    {
                        'result': int, 总计搜索结果数,
                        'type': 'total'  # type用来区分不同类别的搜索结果
                    },
                    {
                        'results': [
                            'str, 相关搜索建议',
                            '...',
                            '...',
                            '...',
                            ...
                        ],
                        'type': 'related'
                    },
                    {
                        'process': 'str, 算数过程',
                        'result': 'str, 运算结果',
                        'type': 'calc'
                        # 这类搜索结果仅会在搜索词涉及运算时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'author': 'str, 新闻来源',
                                'time': 'str, 新闻发布时间',
                                'title': 'str, 新闻标题',
                                'url': 'str, 新闻链接',
                                'des': 'str, 新闻简介,大部分情况为None'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'news'
                        # 这类搜索结果仅会在搜索词有相关新闻时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'cover': 'str, 视频封面图片链接',
                                'origin': 'str, 视频来源',
                                'length': 'str, 视频时长',
                                'title': 'str, 视频标题',
                                'url': 'str, 视频链接'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'video'
                        # 这类搜索结果仅会在搜索词有相关视频时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': 'str, 百科封面图片/视频链接',
                            'cover-type': 'str, 百科封面类别,图片是image,视频是video',
                            'des': 'str, 百科简介',
                            'title': 'str, 百科标题',
                            'url': 'str, 百科链接'
                        },
                        'type': 'baike'
                        # 这类搜索结果仅会在搜索词有相关百科时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': 'str, 贴吧封面图片链接',
                            'des': 'str, 贴吧简介',
                            'title': 'str, 贴吧标题',
                            'url': 'str, 贴吧链接',
                            'followers': 'str, 贴吧关注人数(可能有汉字,如:1万)',
                            'hot': [{  # list, 热门帖子
                                'clicks': 'str, 帖子点击总数',
                                'replies': 'str, 帖子回复总数',
                                'title': 'str, 帖子标题',
                                'url': 'str, 帖子链接'
                            }],
                            'total': 'str, 贴吧总帖子数(可能有汉字,如:17万)'
                        },
                        'type': 'tieba'
                        # 这类搜索结果仅会在搜索词有相关贴吧时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'blogs': [{  # list, 博客列表
                                'des': 'str, 博客简介,没有时为`None`',
                                'origin': 'str, 博客来源',
                                'tags': [  # list, 博客标签
                                    'str, 标签文字'
                                ],
                                'title': 'str, 博客标题',
                                'url': 'str, 博客链接'
                            }],
                            'title': 'str, 博客搜索标题',
                            'url': 'str, 博客搜索链接 (https://kaifa.baidu.com)'
                        },
                        'type': 'blog'
                        # 这类搜索结果仅会在搜索词有相关博客时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'title': 'str, 仓库标题',
                            'des': 'str, 仓库简介',
                            'url': 'str, 仓库链接',
                            'star': int, 仓库star数,
                            'fork': int, 仓库fork数,
                            'watch': int, 仓库watch数,
                            'license': 'str, 仓库版权协议',
                            'lang': 'str, 仓库使用的编程语言',
                            'status': 'str, 仓库状态图表链接'
                        },
                        'type': 'gitee'
                        # 这类搜索结果仅会在搜索词有相关代码仓库时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'des': 'str, 搜索结果简介',
                        'origin': 'str, 搜索结果的来源,可能是域名,也可能是名称',
                        'time': 'str, 搜索结果的发布时间',
                        'title': 'str, 搜索结果标题',
                        'type': 'result',  # 正经的搜索结果
                        'url': 'str, 搜索结果链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计的搜索结果页数,可能会因为当前页数的变化而随之变化
            }

        - 带页码:
            >>> BaiduSpider().search_web('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        - 按需解析:
            >>> BaiduSpider().search_web('搜索词', exclude=['要屏蔽的子部件列表'])
            可选值:['news', 'video', 'baike', 'tieba', 'blog', 'gitee', 'related', 'calc'],
            分别表示:资讯,视频,百科,贴吧,博客,Gitee代码仓库,相关搜索,计算。
            当exclude=['all']时,将仅保留基本搜索结果和搜索结果总数。
            如果'all'在exclude列表里,则将忽略列表中的剩余部件,返回exclude=['all']时的结果。

        Args:
            query (str): 要爬取的搜索词.
            pn (int, optional): 爬取的页码. Defaults to 1.
            exclude (list, optional): 要屏蔽的控件. Defaults to [].

        Returns:
            dict: 爬取的返回值和搜索结果
        """
        error = None
        results = {"results": [], "pages": 0}
        # 按需解析
        if "all" in exclude:
            exclude = [
                "news",
                "video",
                "baike",
                "tieba",
                "blog",
                "gitee",
                "calc",
                "related",
            ]
        try:
            text = quote(query, "utf-8")
            url = "https://www.baidu.com/s?wd=%s&pn=%d" % (text, (pn - 1) * 10)
            content = self._get_response(url)
            results = self.parser.parse_web(content, exclude=exclude)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error, "BaiduSpider", "parse-web")
        return {"results": results["results"], "total": results["pages"]}

    def search_pic(self, query: str, pn: int = 1) -> dict:
        """百度图片搜索.

        - 实例:
            >>> BaiduSpider().search_pic('搜索词')
            {
                'results': [
                    {
                        'host': 'str, 图片来源域名',
                        'title': 'str, 图片标题',
                        'url': 'str, 图片链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果总计页码,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_pic('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要爬取的query
            pn (int, optional): 爬取的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果
        """
        error = None
        try:
            url = "http://image.baidu.com/search/flip?tn=baiduimage&word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 20,
            )
            content = self._get_response(url)
            result = self.parser.parse_pic(content)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_zhidao(self, query: str, pn: int = 1) -> dict:
        """百度知道搜索.

        - 普通搜索:
            >>> BaiduSpider().search_zhidao('搜索词')
            {
                'results': [
                    {
                        'count': int, 回答总数,
                        'date': 'str, 发布日期',
                        'des': 'str, 简介',
                        'title': 'str, 标题',
                        'url': 'str, 链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页数,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_zhidao('搜索词', pn=2)  # `pn` !!
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总页码
        """
        error = None
        try:
            url = (
                "https://zhidao.baidu.com/search?lm=0&rn=10&pn=0&fr=search&pn=%d&word=%s"
                % ((pn - 1) * 10, quote(query)))
            source = requests.get(url, headers=self.headers)
            # 转化编码
            source.encoding = "gb2312"
            code = source.text
            result = self.parser.parse_zhidao(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_video(self, query: str, pn: int = 1) -> dict:
        """百度视频搜索.

        - 普通搜索:
            >>> BaiduSpider().search_video('搜索词')
            {
                'results': [
                    {
                        'img': 'str, 视频封面图片链接',
                        'time': 'str, 视频时长',
                        'title': 'str, 视频标题',
                        'url': 'str, 视频链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                'total': int, 搜索结果最大页数,可能因搜索页数改变而改变
            }

        - 带页码:
            >>> BaiduSpider().search_video('搜索词', pn=2)  # <=== `pn`
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果及总页码
        """
        error = None
        try:
            url = (
                "http://v.baidu.com/v?no_al=1&word=%s&pn=%d&ie=utf-8&db=0&s=0&fbl=800"
                % (quote(query), (60 if pn == 2 else (pn - 1) * 20)))
            # 获取源码
            source = requests.get(url, headers=self.headers)
            code = self._minify(source.text)
            result = self.parser.parse_video(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_news(self, query: str, pn: int = 1) -> dict:
        """百度资讯搜索.

        - 获取资讯搜索结果:
            >>> BaiduSpider().search_news('搜索词')
            {
                'results': [
                    {
                        'author': 'str, 资讯来源(作者)',
                        'date': 'str, 资讯发布时间',
                        'des': 'str, 资讯简介',
                        'title': 'str, 资讯标题',
                        'url': 'str, 资讯链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页码,可能会因为当前页数变化而变化
            }

        - 带页码:
            >>> BaiduSpider().search_news('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 搜索query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果与总页码。
        """
        error = None
        try:
            url = "https://www.baidu.com/s?rtt=1&bsst=1&tn=news&word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 10,
            )
            # 源码
            source = requests.get(url, headers=self.headers)
            # 压缩
            code = self._minify(source.text)
            result = self.parser.parse_news(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_wenku(self, query: str, pn: int = 1) -> dict:
        """百度文库搜索.

        - 普通搜索:
            >>> BaiduSpider().search_wenku('搜索词')
            {
                'results': [
                    {
                        'date': 'str, 文章发布日期',
                        'des': 'str, 文章简介',
                        'downloads': int, 文章下载量,
                        'pages': int, 文章页数,
                        'title': 'str, 文章标题',
                        'type': 'str, 文章格式,为全部大写字母',
                        'url': 'str, 文章链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果的页数
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_wenku('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索的页码. Defaults to 1.

        Returns:
            dict: 搜索结果和总计页数
        """
        error = None
        try:
            url = "https://wenku.baidu.com/search?word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 10,
            )
            source = requests.get(url, headers=self.headers)
            source.encoding = "gb2312"
            code = self._minify(source.text)
            result = self.parser.parse_wenku(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_jingyan(self, query: str, pn: int = 1) -> dict:
        """百度经验搜索.

        - 例如:
            >>> BaiduSpider().search_jingyan('关键词')
            {
                'results': [
                    {
                        'title': 'str, 经验标题',
                        'url': 'str, 经验链接',
                        'des': 'str, 经验简介',
                        'date': 'str, 经验发布日期',
                        'category': 'str, 经验分类',
                        'votes': int, 经验的支持票数
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果页数
            }

        - 带页码的:
            >>> BaiduSpider().search_jingyan('搜索词', pn=2) # `pn` 是页码
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的关键词
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总计的页码.
        """
        url = "https://jingyan.baidu.com/search?word=%s&pn=%d&lm=0" % (
            quote(query),
            (pn - 1) * 10,
        )
        # 获取网页源代码
        source = requests.get(url, headers=self.headers)
        # 最小化代码
        code = self._minify(source.text)
        bs = BeautifulSoup(code, "html.parser")
        # 加载搜索结果
        data = bs.find("div", class_="search-list").findAll("dl")
        results = []
        for res in data:
            # 标题
            title = self._format(res.find("dt").find("a").text)
            # 链接
            url = "https://jingyan.baidu.com/" + res.find("dt").find(
                "a")["href"]
            # 简介
            des = self._format(
                res.find("dd").find("div", class_="summary").find(
                    "span", class_="abstract").text)
            # 获取发布日期和分类,位于`<span class="cate"/>`中
            tmp = self._format(
                res.find("dd").find("div", class_="summary").find(
                    "span", class_="cate").text).split("-")
            # 发布日期
            date = self._format(tmp[1])
            # 分类
            category = self._format(tmp[-1]).strip("分类:")
            # 支持票数
            votes = int(
                self._format(
                    res.find("dt").find("span",
                                        class_="succ-times").text).strip("得票"))
            # 生成结果
            result = {
                "title": title,
                "url": url,
                "des": des,
                "date": date,
                "category": category,
                "votes": votes,
            }
            results.append(result)  # 加入结果到集合中
        # 获取分页
        pages_ = bs.find("div", id="pg").findAll("a")[-1]
        # 既不是最后一页也没有超出最后一页
        if "尾页" in pages_.text:
            # 获取尾页并加一
            total = int(
                int(pages_["href"].split("&")[-1].strip("pn=")) / 10) + 1
        # 是最后一页或者是超过了最后一页
        else:
            # 重新获取分页
            pages_ = bs.find("div", id="pg").findAll("a")[1]
            # 获取尾页并加一
            total = int(
                int(pages_["href"].split("&")[-1].strip("pn=")) / 10) + 1
        return {"results": results, "total": total}

    def search_baike(self, query: str) -> dict:
        """百度百科搜索.

        - 使用方法:
            >>> BaiduSpider().search_baike('搜索词')
            {
                'results': {
                    [
                        'title': 'str, 百科标题',
                        'des': 'str, 百科简介',
                        'date': 'str, 百科最后更新时间',
                        'url': 'str, 百科链接'
                    ],
                    [ ... ],
                    [ ... ],
                    [ ... ]
                },
                'total': int, 搜索结果总数
            }

        Args:
            query (str): 要搜索的关键词

        Returns:
            dict: 搜索结果和总页数
        """
        # 获取源码
        source = requests.get(
            "https://baike.baidu.com/search?word=%s" % quote(query),
            headers=self.headers,
        )
        code = self._minify(source.text)
        # 创建BeautifulSoup对象
        soup = (BeautifulSoup(code, "html.parser").find(
            "div", class_="body-wrapper").find("div", class_="searchResult"))
        # 获取百科总数
        total = int(
            soup.find(
                "div",
                class_="result-count").text.strip("百度百科为您找到相关词条约").strip("个"))
        # 获取所有结果
        container = soup.findAll("dd")
        results = []
        for res in container:
            # 链接
            url = "https://baike.baidu.com" + self._format(
                res.find("a", class_="result-title")["href"])
            # 标题
            title = self._format(res.find("a", class_="result-title").text)
            # 简介
            des = self._format(res.find("p", class_="result-summary").text)
            # 更新日期
            date = self._format(res.find("span", class_="result-date").text)
            # 生成结果
            results.append({
                "title": title,
                "des": des,
                "date": date,
                "url": url
            })
        return {"results": results, "total": total}
Beispiel #2
0
class BaiduSpider(BaseSpider):
    def __init__(self) -> None:
        """爬取百度的搜索结果.

        本类的所有成员方法都遵循下列格式:

            {
                'results': <一个列表,表示搜索结果,内部的字典会因为不同的成员方法而改变>,
                'total': <一个正整数,表示搜索结果的最大页数,可能会因为搜索结果页码的变化而变化,因为百度不提供总共的搜索结果页数>
            }

        目前支持百度搜索,百度图片,百度知道,百度视频,百度资讯,百度文库,百度经验和百度百科,并且返回的搜索结果无广告。继承自``BaseSpider``。

        BaiduSpider.`search_web(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度网页搜索

        BaiduSpider.`search_pic(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度图片搜索

        BaiduSpider.`search_zhidao(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度知道搜索

        BaiduSpider.`search_video(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度视频搜索

        BaiduSpider.`search_news(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度资讯搜索

        BaiduSpider.`search_wenku(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度文库搜索

        BaiduSpider.`search_jingyan(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度经验搜索

        BaiduSpider.`search_baike(self: BaiduSpider, query: str) -> dict`: 百度百科搜索
        """
        super().__init__()
        # 爬虫名称(不是请求的,只是用来表识)
        self.spider_name = "BaiduSpider"
        # 设置请求头
        self.headers = {
            "User-Agent":
            "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
            "Referer": "https://www.baidu.com",
            "Accept":
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
            "Accept-Encoding": "gzip, deflate, br",
            "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
        }
        self.parser = Parser()
        self.EMPTY = {"results": [], "pages": 0}

    def search_web(self,
                   query: str,
                   pn: int = 1,
                   exclude: list = [],
                   time: tuple = None) -> dict:
        """百度网页搜索.

        - 简单搜索:
            >>> BaiduSpider().search_web('搜索词')
            {
                'results': [
                    {
                        'result': int, 总计搜索结果数,
                        'type': 'total'  # type用来区分不同类别的搜索结果
                    },
                    {
                        'results': [
                            'str, 相关搜索建议',
                            '...',
                            '...',
                            '...',
                            ...
                        ],
                        'type': 'related'
                    },
                    {
                        'process': 'str, 算数过程',
                        'result': 'str, 运算结果',
                        'type': 'calc'
                        # 这类搜索结果仅会在搜索词涉及运算时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'author': 'str, 新闻来源',
                                'time': 'str, 新闻发布时间',
                                'title': 'str, 新闻标题',
                                'url': 'str, 新闻链接',
                                'des': 'str, 新闻简介,大部分情况为None'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'news'
                        # 这类搜索结果仅会在搜索词有相关新闻时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'cover': 'str, 视频封面图片链接',
                                'origin': 'str, 视频来源',
                                'length': 'str, 视频时长',
                                'title': 'str, 视频标题',
                                'url': 'str, 视频链接'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'video'
                        # 这类搜索结果仅会在搜索词有相关视频时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': 'str, 百科封面图片/视频链接',
                            'cover-type': 'str, 百科封面类别,图片是image,视频是video',
                            'des': 'str, 百科简介',
                            'title': 'str, 百科标题',
                            'url': 'str, 百科链接'
                        },
                        'type': 'baike'
                        # 这类搜索结果仅会在搜索词有相关百科时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': 'str, 贴吧封面图片链接',
                            'des': 'str, 贴吧简介',
                            'title': 'str, 贴吧标题',
                            'url': 'str, 贴吧链接',
                            'followers': 'str, 贴吧关注人数(可能有汉字,如:1万)',
                            'hot': [{  # list, 热门帖子
                                'clicks': 'str, 帖子点击总数',
                                'replies': 'str, 帖子回复总数',
                                'title': 'str, 帖子标题',
                                'url': 'str, 帖子链接'
                            }],
                            'total': 'str, 贴吧总帖子数(可能有汉字,如:17万)'
                        },
                        'type': 'tieba'
                        # 这类搜索结果仅会在搜索词有相关贴吧时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'blogs': [{  # list, 博客列表
                                'des': 'str, 博客简介,没有时为`None`',
                                'origin': 'str, 博客来源',
                                'tags': [  # list, 博客标签
                                    'str, 标签文字'
                                ],
                                'title': 'str, 博客标题',
                                'url': 'str, 博客链接'
                            }],
                            'title': 'str, 博客搜索标题',
                            'url': 'str, 博客搜索链接 (https://kaifa.baidu.com)'
                        },
                        'type': 'blog'
                        # 这类搜索结果仅会在搜索词有相关博客时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'title': 'str, 仓库标题',
                            'des': 'str, 仓库简介',
                            'url': 'str, 仓库链接',
                            'star': int, 仓库star数,
                            'fork': int, 仓库fork数,
                            'watch': int, 仓库watch数,
                            'license': 'str, 仓库版权协议',
                            'lang': 'str, 仓库使用的编程语言',
                            'status': 'str, 仓库状态图表链接'
                        },
                        'type': 'gitee'
                        # 这类搜索结果仅会在搜索词有相关代码仓库时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'des': 'str, 搜索结果简介',
                        'origin': 'str, 搜索结果的来源,可能是域名,也可能是名称',
                        'time': 'str, 搜索结果的发布时间',
                        'title': 'str, 搜索结果标题',
                        'type': 'result',  # 正经的搜索结果
                        'url': 'str, 搜索结果链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计的搜索结果页数,可能会因为当前页数的变化而随之变化
            }

        - 带页码:
            >>> BaiduSpider().search_web('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        - 按需解析:
            >>> BaiduSpider().search_web('搜索词', exclude=['要屏蔽的子部件列表'])
            可选值:['news', 'video', 'baike', 'tieba', 'blog', 'gitee', 'related', 'calc'],
            分别表示:资讯,视频,百科,贴吧,博客,Gitee代码仓库,相关搜索,计算。
            当exclude=['all']时,将仅保留基本搜索结果和搜索结果总数。
            如果'all'在exclude列表里,则将忽略列表中的剩余部件,返回exclude=['all']时的结果。

        - 按时间筛选:
            >>> BaiduSpider().search_web('搜索词', time=(开始时间, 结束时间))
            其中,开始时间和结束时间均为datetime.datetime类型,或者是使用time.time()函数生成的时间戳。
            time参数也可以是以下任意一个字符串:['day', 'week', 'month', 'year']。它们分别表示:一天内、
            一周内、一月内、一年内。
            如果参数非法,BaiduSpider会忽略此次筛选。

        Args:
            query (str): 要爬取的搜索词.
            pn (int, optional): 爬取的页码. Defaults to 1.
            exclude (list, optional): 要屏蔽的控件. Defaults to [].

        Returns:
            dict: 爬取的返回值和搜索结果
        """
        error = None
        results = {"results": [], "pages": 0}
        # 按需解析
        if "all" in exclude:
            exclude = [
                "news",
                "video",
                "baike",
                "tieba",
                "blog",
                "gitee",
                "calc",
                "related",
            ]
        # 按时间筛选
        if type(time) == str:
            to = datetime.datetime.now()
            from_ = datetime.datetime(to.year, to.month, to.day, to.hour,
                                      to.minute, to.second, to.microsecond)
            if time == "day":
                from_ += datetime.timedelta(days=-1)
            elif time == "week":
                from_ += datetime.timedelta(days=-7)
            elif time == "month":
                from_ += datetime.timedelta(days=-31)
            elif time == "year":
                from_ += datetime.timedelta(days=-365)
        elif type(time) == tuple or type(time) == list:
            to = time[0]
            from_ = time[1]
        else:
            to = from_ = None
        if type(to) == datetime.datetime and type(from_) == datetime.datetime:
            FORMAT = "%Y-%m-%d %H:%M:%S"
            to = int(
                time_lib.mktime(time_lib.strptime(to.strftime(FORMAT),
                                                  FORMAT)))
            from_ = int(
                time_lib.mktime(
                    time_lib.strptime(from_.strftime(FORMAT), FORMAT)))
        try:
            text = quote(query, "utf-8")
            url = "https://www.baidu.com/s?wd=%s&pn=%d" % (text, (pn - 1) * 10)
            if to is not None and from_ is not None:
                url += "&gpc=" + quote(f"stf={from_},{to}|stftype=1")
            content = self._get_response(url)
            results = self.parser.parse_web(content, exclude=exclude)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error, "BaiduSpider", "parse-web")
        return {"results": results["results"], "total": results["pages"]}

    def search_pic(self, query: str, pn: int = 1) -> dict:
        """百度图片搜索.

        - 实例:
            >>> BaiduSpider().search_pic('搜索词')
            {
                'results': [
                    {
                        'host': 'str, 图片来源域名',
                        'title': 'str, 图片标题',
                        'url': 'str, 图片链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果总计页码,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_pic('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要爬取的query
            pn (int, optional): 爬取的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果
        """
        error = None
        try:
            url = "http://image.baidu.com/search/flip?tn=baiduimage&word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 20,
            )
            content = self._get_response(url)
            result = self.parser.parse_pic(content)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_zhidao(self, query: str, pn: int = 1) -> dict:
        """百度知道搜索.

        - 普通搜索:
            >>> BaiduSpider().search_zhidao('搜索词')
            {
                'results': [
                    {
                        'count': int, 回答总数,
                        'date': 'str, 发布日期',
                        'des': 'str, 简介',
                        'title': 'str, 标题',
                        'url': 'str, 链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页数,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_zhidao('搜索词', pn=2)  # `pn` !!
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总页码
        """
        error = None
        try:
            url = (
                "https://zhidao.baidu.com/search?lm=0&rn=10&pn=0&fr=search&pn=%d&word=%s"
                % ((pn - 1) * 10, quote(query)))
            source = requests.get(url, headers=self.headers)
            # 转化编码
            source.encoding = "gb2312"
            code = source.text
            result = self.parser.parse_zhidao(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_video(self, query: str, pn: int = 1) -> dict:
        """百度视频搜索.

        - 普通搜索:
            >>> BaiduSpider().search_video('搜索词')
            {
                'results': [
                    {
                        'img': 'str, 视频封面图片链接',
                        'time': 'str, 视频时长',
                        'title': 'str, 视频标题',
                        'url': 'str, 视频链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                'total': int, 搜索结果最大页数,可能因搜索页数改变而改变
            }

        - 带页码:
            >>> BaiduSpider().search_video('搜索词', pn=2)  # <=== `pn`
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果及总页码
        """
        error = None
        try:
            url = (
                "http://v.baidu.com/v?no_al=1&word=%s&pn=%d&ie=utf-8&db=0&s=0&fbl=800"
                % (quote(query), (60 if pn == 2 else (pn - 1) * 20)))
            # 获取源码
            source = requests.get(url, headers=self.headers)
            code = self._minify(source.text)
            result = self.parser.parse_video(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_news(self, query: str, pn: int = 1) -> dict:
        """百度资讯搜索.

        - 获取资讯搜索结果:
            >>> BaiduSpider().search_news('搜索词')
            {
                'results': [
                    {
                        'author': 'str, 资讯来源(作者)',
                        'date': 'str, 资讯发布时间',
                        'des': 'str, 资讯简介',
                        'title': 'str, 资讯标题',
                        'url': 'str, 资讯链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页码,可能会因为当前页数变化而变化
            }

        - 带页码:
            >>> BaiduSpider().search_news('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 搜索query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果与总页码。
        """
        error = None
        try:
            url = "https://www.baidu.com/s?rtt=1&bsst=1&tn=news&word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 10,
            )
            # 源码
            source = requests.get(url, headers=self.headers)
            # 压缩
            code = self._minify(source.text)
            result = self.parser.parse_news(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_wenku(self, query: str, pn: int = 1) -> dict:
        """百度文库搜索.

        - 普通搜索:
            >>> BaiduSpider().search_wenku('搜索词')
            {
                'results': [
                    {
                        'date': 'str, 文章发布日期',
                        'des': 'str, 文章简介',
                        'downloads': int, 文章下载量,
                        'pages': int, 文章页数,
                        'title': 'str, 文章标题',
                        'type': 'str, 文章格式,为全部大写字母',
                        'url': 'str, 文章链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果的页数
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_wenku('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索的页码. Defaults to 1.

        Returns:
            dict: 搜索结果和总计页数
        """
        error = None
        try:
            url = "https://wenku.baidu.com/search?word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 10,
            )
            source = requests.get(url, headers=self.headers)
            source.encoding = "gb2312"
            code = self._minify(source.text)
            result = self.parser.parse_wenku(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_jingyan(self, query: str, pn: int = 1) -> dict:
        """百度经验搜索.

        - 例如:
            >>> BaiduSpider().search_jingyan('关键词')
            {
                'results': [
                    {
                        'title': 'str, 经验标题',
                        'url': 'str, 经验链接',
                        'des': 'str, 经验简介',
                        'date': 'str, 经验发布日期',
                        'category': 'str, 经验分类',
                        'votes': int, 经验的支持票数
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果页数
            }

        - 带页码的:
            >>> BaiduSpider().search_jingyan('搜索词', pn=2) # `pn` 是页码
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的关键词
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总计的页码.
        """
        error = None
        try:
            url = "https://jingyan.baidu.com/search?word=%s&pn=%d&lm=0" % (
                quote(query),
                (pn - 1) * 10,
            )
            source = requests.get(url, headers=self.headers)
            code = self._minify(source.text)
            result = self.parser.parse_jingyan(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_baike(self, query: str) -> dict:
        """百度百科搜索.

        - 使用方法:
            >>> BaiduSpider().search_baike('搜索词')
            {
                'results': {
                    [
                        'title': 'str, 百科标题',
                        'des': 'str, 百科简介',
                        'date': 'str, 百科最后更新时间',
                        'url': 'str, 百科链接'
                    ],
                    [ ... ],
                    [ ... ],
                    [ ... ]
                },
                'total': int, 搜索结果总数
            }

        Args:
            query (str): 要搜索的关键词

        Returns:
            dict: 搜索结果和总页数
        """
        error = None
        try:
            url = "https://baike.baidu.com/search?word=%s" % quote(query)
            source = requests.get(url, headers=self.headers)
            code = self._minify(source.text)
            result = self.parser.parse_baike(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}
Beispiel #3
0
class BaiduSpider(BaseSpider):
    def __init__(self) -> None:
        """爬取百度的搜索结果

        本类的所有成员方法都遵循下列格式:

            {
                'results': <一个列表,表示搜索结果,内部的字典会因为不同的成员方法而改变>,
                'total': <一个正整数,表示搜索结果的最大页数,可能会因为搜索结果页码的变化而变化,因为百度不提供总共的搜索结果页数>
            }

        目前支持百度搜索,百度图片,百度知道,百度视频,百度资讯,百度文库,百度经验和百度百科,并且返回的搜索结果无广告。继承自``BaseSpider``。

        BaiduSpider.`search_web(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度网页搜索

        BaiduSpider.`search_pic(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度图片搜索

        BaiduSpider.`search_zhidao(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度知道搜索

        BaiduSpider.`search_video(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度视频搜索

        BaiduSpider.`search_news(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度资讯搜索

        BaiduSpider.`search_wenku(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度文库搜索

        BaiduSpider.`search_jingyan(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度经验搜索

        BaiduSpider.`search_baike(self: BaiduSpider, query: str) -> dict`: 百度百科搜索
        """
        super().__init__()
        # 爬虫名称(不是请求的,只是用来表识)
        self.spider_name = 'BaiduSpider'
        # 设置请求头
        self.headers = {
            'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36',
            'Referer': 'https://www.baidu.com',
            'Accept':
            'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
            'Accept-Encoding': 'gzip, deflate, br',
            'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7'
        }
        self.parser = Parser()

    def search_web(self, query: str, pn: int = 1) -> dict:
        """百度网页搜索

        - 简单搜索:
            >>> BaiduSpider().search_web('搜索词')
            {
                'results': [
                    {
                        'result': int, 总计搜索结果数,
                        'type': 'total'  # type用来区分不同类别的搜索结果
                    },
                    {
                        'results': [
                            'str, 相关搜索建议',
                            '...',
                            '...',
                            '...',
                            ...
                        ],
                        'type': 'related'
                    },
                    {
                        'process': 'str, 算数过程',
                        'result': 'str, 运算结果',
                        'type': 'calc'
                        # 这类搜索结果仅会在搜索词涉及运算时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'author': 'str, 新闻来源',
                                'time': 'str, 新闻发布时间',
                                'title': 'str, 新闻标题',
                                'url': 'str, 新闻链接',
                                'des': 'str, 新闻简介,大部分情况为None'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'news'
                        # 这类搜索结果仅会在搜索词有相关新闻时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'cover': 'str, 视频封面图片链接',
                                'origin': 'str, 视频来源',
                                'length': 'str, 视频时长',
                                'title': 'str, 视频标题',
                                'url': 'str, 视频链接'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'video'
                        # 这类搜索结果仅会在搜索词有相关视频时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                                'cover': 'str, 百科封面图片/视频链接',
                                'cover-type': 'str, 百科封面类别,图片是image,视频是video',
                                'des': 'str, 百科简介',
                                'title': 'str, 百科标题',
                                'url': 'str, 百科链接'
                        },
                        'type': 'baike'
                        # 这类搜索结果仅会在搜索词有相关百科时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                                'cover': 'str, 贴吧封面图片链接',
                                'des': 'str, 贴吧简介',
                                'title': 'str, 贴吧标题',
                                'url': 'str, 贴吧链接',
                                'followers': 'str, 贴吧关注人数(可能有汉字,如:1万)',
                                'hot': [{  # list, 热门帖子
                                    'clicks': 'str, 帖子点击总数',
                                    'replies': 'str, 帖子回复总数',
                                    'title': 'str, 帖子标题',
                                    'url': 'str, 帖子链接'
                                }],
                                'total': 'str, 贴吧总帖子数(可能有汉字,如:17万)'
                        },
                        'type': 'tieba'
                        # 这类搜索结果仅会在搜索词有相关贴吧时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'des': 'str, 搜索结果简介',
                        'origin': 'str, 搜索结果的来源,可能是域名,也可能是名称',
                        'time': 'str, 搜索结果的发布时间',
                        'title': 'str, 搜索结果标题',
                        'type': 'result',  # 正经的搜索结果
                        'url': 'str, 搜索结果链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计的搜索结果页数,可能会因为当前页数的变化而随之变化
            }

        - 带页码:
            >>> BaiduSpider().search_web('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要爬取的query
            pn (int, optional): 爬取的页码. Defaults to 1.

        Returns:
            dict: 爬取的返回值和搜索结果
        """
        error = None
        try:
            text = quote(query, 'utf-8')
            url = 'https://www.baidu.com/s?wd=%s&pn=%d' % (text, (pn - 1) * 10)
            content = self._get_response(url)
            results = self.parser.parse_web(content)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error)
        return {'results': results['results'], 'total': results['pages']}

    def search_pic(self, query: str, pn: int = 1) -> dict:
        """百度图片搜索

        - 实例:
            >>> BaiduSpider().search_pic('搜索词')
            {
                'results': [
                    {
                        'host': 'str, 图片来源域名',
                        'title': 'str, 图片标题',
                        'url': 'str, 图片链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果总计页码,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_pic('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要爬取的query
            pn (int, optional): 爬取的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果
        """
        error = None
        try:
            url = 'http://image.baidu.com/search/flip?tn=baiduimage&word=%s&pn=%d' % (
                quote(query), (pn - 1) * 20)
            content = self._get_response(url)
            result = self.parser.parse_pic(content)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error)
        return {'results': result['results'], 'total': result['pages']}

    def search_zhidao(self, query: str, pn: int = 1) -> dict:
        """百度知道搜索

        - 普通搜索:
            >>> BaiduSpider().search_zhidao('搜索词')
            {
                'results': [
                    {
                        'count': int, 回答总数,
                        'date': 'str, 发布日期',
                        'des': 'str, 简介',
                        'title': 'str, 标题',
                        'url': 'str, 链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页数,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_zhidao('搜索词', pn=2)  # `pn` !!
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总页码
        """
        error = None
        try:
            url = 'https://zhidao.baidu.com/search?lm=0&rn=10&pn=0&fr=search&pn=%d&word=%s' % (
                (pn - 1) * 10, quote(query))
            source = requests.get(url, headers=self.headers)
            # 转化编码
            source.encoding = 'gb2312'
            code = source.text
            result = self.parser.parse_zhidao(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {'results': result['results'], 'total': result['pages']}

    def search_video(self, query: str, pn: int = 1) -> dict:
        """百度视频搜索

        - 普通搜索:
            >>> BaiduSpider().search_video('搜索词')
            {
                'results': [
                    {
                        'img': 'str, 视频封面图片链接',
                        'time': 'str, 视频时长',
                        'title': 'str, 视频标题',
                        'url': 'str, 视频链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                'total': int, 搜索结果最大页数,可能因搜索页数改变而改变
            }

        - 带页码:
            >>> BaiduSpider().search_video('搜索词', pn=2)  # <=== `pn`
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果及总页码
        """
        error = None
        try:
            url = 'http://v.baidu.com/v?no_al=1&word=%s&pn=%d&ie=utf-8&db=0&s=0&fbl=800' % (
                quote(query), (60 if pn == 2 else (pn - 1) * 20))
            # 获取源码
            source = requests.get(url, headers=self.headers)
            code = self._minify(source.text)
            result = self.parser.parse_video(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {'results': result['results'], 'total': result['pages']}

    def search_news(self, query: str, pn: int = 1) -> dict:
        """百度资讯搜索

        - 获取资讯搜索结果:
            >>> BaiduSpider().search_news('搜索词')
            {
                'results': [
                    {
                        'author': 'str, 资讯来源(作者)',
                        'date': 'str, 资讯发布时间',
                        'des': 'str, 资讯简介',
                        'title': 'str, 资讯标题',
                        'url': 'str, 资讯链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页码,可能会因为当前页数变化而变化
            }

        - 带页码:
            >>> BaiduSpider().search_news('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 搜索query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果与总页码。
        """
        error = None
        try:
            url = 'https://www.baidu.com/s?rtt=1&bsst=1&tn=news&word=%s&pn=%d' % (
                quote(query), (pn - 1) * 10)
            # 源码
            source = requests.get(url, headers=self.headers)
            # 压缩
            code = self._minify(source.text)
            result = self.parser.parse_news(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {'results': result['results'], 'total': result['pages']}

    def search_wenku(self, query: str, pn: int = 1) -> dict:
        """百度文库搜索

        - 普通搜索:
            >>> BaiduSpider().search_wenku('搜索词')
            {
                'results': [
                    {
                        'date': 'str, 文章发布日期',
                        'des': 'str, 文章简介',
                        'downloads': int, 文章下载量,
                        'pages': int, 文章页数,
                        'title': 'str, 文章标题',
                        'type': 'str, 文章格式,为全部大写字母',
                        'url': 'str, 文章链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果的页数
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_wenku('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索的页码. Defaults to 1.

        Returns:
            dict: 搜索结果和总计页数
        """
        error = None
        try:
            url = 'https://wenku.baidu.com/search?word=%s&pn=%d' % (
                quote(query), (pn - 1) * 10)
            source = requests.get(url, headers=self.headers)
            source.encoding = 'gb2312'
            code = self._minify(source.text)
            result = self.parser.parse_wenku(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {'results': result['results'], 'total': result['pages']}

    def search_jingyan(self, query: str, pn: int = 1) -> dict:
        """百度经验搜索

        - 例如:
            >>> BaiduSpider().search_jingyan('关键词')
            {
                'results': [
                    {
                        'title': 'str, 经验标题',
                        'url': 'str, 经验链接',
                        'des': 'str, 经验简介',
                        'date': 'str, 经验发布日期',
                        'category': 'str, 经验分类',
                        'votes': int, 经验的支持票数
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果页数
            }

        - 带页码的:
            >>> BaiduSpider().search_jingyan('搜索词', pn=2) # `pn` 是页码
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的关键词
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总计的页码.
        """
        url = 'https://jingyan.baidu.com/search?word=%s&pn=%d&lm=0' % (
            quote(query), (pn - 1) * 10)
        # 获取网页源代码
        source = requests.get(url, headers=self.headers)
        # 最小化代码
        code = self._minify(source.text)
        bs = BeautifulSoup(code, 'html.parser')
        # 加载搜索结果
        data = bs.find('div', class_='search-list').findAll('dl')
        results = []
        for res in data:
            # 标题
            title = self._format(res.find('dt').find('a').text)
            # 链接
            url = 'https://jingyan.baidu.com/' + \
                res.find('dt').find('a')['href']
            # 简介
            des = self._format(
                res.find('dd').find('div', class_='summary').find(
                    'span', class_='abstract').text)
            # 获取发布日期和分类,位于`<span class="cate"/>`中
            tmp = self._format(
                res.find('dd').find('div', class_='summary').find(
                    'span', class_='cate').text).split('-')
            # 发布日期
            date = self._format(tmp[1])
            # 分类
            category = self._format(tmp[-1]).strip('分类:')
            # 支持票数
            votes = int(
                self._format(
                    res.find('dt').find('span',
                                        class_='succ-times').text).strip('得票'))
            # 生成结果
            result = {
                'title': title,
                'url': url,
                'des': des,
                'date': date,
                'category': category,
                'votes': votes
            }
            results.append(result)  # 加入结果到集合中
        # 获取分页
        pages_ = bs.find('div', id='pg').findAll('a')[-1]
        # 既不是最后一页也没有超出最后一页
        if '尾页' in pages_.text:
            # 获取尾页并加一
            total = int(
                int(pages_['href'].split('&')[-1].strip('pn=')) / 10) + 1
        # 是最后一页或者是超过了最后一页
        else:
            # 重新获取分页
            pages_ = bs.find('div', id='pg').findAll('a')[1]
            # 获取尾页并加一
            total = int(
                int(pages_['href'].split('&')[-1].strip('pn=')) / 10) + 1
        return {'results': results, 'total': total}

    def search_baike(self, query: str) -> dict:
        """百度百科搜索

        - 使用方法:
            >>> BaiduSpider().search_baike('搜索词')
            {
                'results': {
                    [
                        'title': 'str, 百科标题',
                        'des': 'str, 百科简介',
                        'date': 'str, 百科最后更新时间',
                        'url': 'str, 百科链接'
                    ],
                    [ ... ],
                    [ ... ],
                    [ ... ]
                },
                'total': int, 搜索结果总数
            }

        Args:
            query (str): 要搜索的关键词

        Returns:
            dict: 搜索结果和总页数
        """
        # 获取源码
        source = requests.get('https://baike.baidu.com/search?word=%s' %
                              quote(query),
                              headers=self.headers)
        code = self._minify(source.text)
        # 创建BeautifulSoup对象
        soup = BeautifulSoup(code, 'html.parser').find(
            'div', class_='body-wrapper').find('div', class_='searchResult')
        # 获取百科总数
        total = int(
            soup.find(
                'div',
                class_='result-count').text.strip('百度百科为您找到相关词条约').strip('个'))
        # 获取所有结果
        container = soup.findAll('dd')
        results = []
        for res in container:
            # 链接
            url = 'https://baike.baidu.com' + \
                self._format(res.find('a', class_='result-title')['href'])
            # 标题
            title = self._format(res.find('a', class_='result-title').text)
            # 简介
            des = self._format(res.find('p', class_='result-summary').text)
            # 更新日期
            date = self._format(res.find('span', class_='result-date').text)
            # 生成结果
            results.append({
                'title': title,
                'des': des,
                'date': date,
                'url': url
            })
        return {'results': results, 'total': total}
class BaiduSpider(BaseSpider):
    """爬取百度的搜索结果.

    本类的所有成员方法都遵循下列格式:

        {
            'results': <一个列表,表示搜索结果,内部的字典会因为不同的成员方法而改变>,
            'total': <一个正整数,表示搜索结果的最大页数,可能会因为搜索结果页码的变化而变化,因为百度不提供总共的搜索结果页数>
        }

    目前支持百度搜索,百度图片,百度知道,百度视频,百度资讯,百度文库,百度经验和百度百科,并且返回的搜索结果无广告。继承自``BaseSpider``。

    - BaiduSpider.`#!python search_web(
        self: BaiduSpider,
        query: str,
        pn: int = 1,
        exclude: list = [],
        time: Union[tuple, str, None] = None,
        proxies: dict = None
    ) -> WebResult`: 百度网页搜索

    - BaiduSpider.`#!python search_pic(
        self: BaiduSpider,
        query: str,
        pn: int = 1,
        proxies: dict = None
    ) -> PicResult`: 百度图片搜索

    - BaiduSpider.`#!python search_zhidao(
        self: BaiduSpider,
        query: str,
        pn: int = 1,
        time: Union[str, None] = None,
        proxies: dict = None
    ) -> ZhidaoResult`: 百度知道搜索

    - BaiduSpider.`#!python search_video(
        self: BaiduSpider,
        query: str,
        pn: int = 1,
        proxies: dict = None
    ) -> VideoResult`: 百度视频搜索

    - BaiduSpider.`#!python search_news(
        self: BaiduSpider,
        query: str,
        pn: int = 1,
        sort_by: str = "focus",
        show: str = "all",
        proxies: dict = None
    ) -> NewsResult`: 百度资讯搜索

    - BaiduSpider.`#!python search_wenku(
        self: BaiduSpider,
        query: str,
        pn: int = 1,
        scope: str = "all",
        format: str = "all",
        time: str = "all",
        page_range: Union[Tuple[int], str] = "all",
        sort_by: str = "relation",
        proxies: dict = None
    ) -> WenkuResult`: 百度文库搜索

    - BaiduSpider.`#!python search_jingyan(
        self: BaiduSpider,
        query: str,
        pn: int = 1,
        scope: str = "all",
        proxies: dict = None
    ) -> JingyanResult`: 百度经验搜索

    - BaiduSpider.`#!python search_baike(
        self: BaiduSpider,
        query: str,
        proxies: dict = None
    )`: 百度百科搜索
    """
    def __init__(self, cookie: str = None) -> None:
        """初始化BaiduSpider.

        - 设置Cookie:
            ```python
            spider = BaiduSpider(cookie="你的cookie")
            ```
            Cookie可以被用于增强爬虫的真实性,尽可能使百度减少封禁IP的最大限制。

            如果你想获取你的Cookie,请打开<https://www.baidu.com/s?wd=placeholder&pn=0>,并
            按F12打开开发者工具,然后在开发者工具最上方的选项栏中选择“网络”(Network)这一选项,点击
            出现的列表中最上方的以`s?wd=placeholder`开头的选项,在出现的详情中找到`Request Headers`
            一项,然后在它的下方找到`Cookie`,并复制Cookie这一选项内(不包括`Cookie: `)后面的所有内容,
            并将它粘贴在你需要的位置。

            请勿传入非法的Cookie。

        Args:
            cookie (Union[str, None], optional): 浏览器抓包得到的cookie. Defaults to None.
        """
        super().__init__()
        # 爬虫名称(不是请求的,只是用来表识)
        self.spider_name = "BaiduSpider"
        # 解析Cookie
        if cookie is not None:
            if cookie.find("__yjs_duid") == -1:
                cookie += "; __yjs_duid=1_" + str(
                    hashlib.md5().hexdigest()) + "; "
            else:
                _ = cookie.split("__yjs_duid=")
                __ = _[1].split(";", 1)[-1]
                ___ = hashlib.md5()
                cookie = _[0] + "__yjs_duid=1_" + str(
                    ___.hexdigest()) + "; " + __
        # 设置请求头
        self.headers = {
            "User-Agent":
            "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
            "Referer":
            "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=2&ch=&tn=baiduhome_pg&bar=&wd=123&oq=123&rsv_pq=896f886f000184f4&rsv_t=fdd2CqgBgjaepxfhicpCfrqeWVSXu9DOQY5WyyWqQYmsKOC%2Fl286S248elzxl%2BJhOKe2&rqlang=cn",
            "Accept":
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
            "Accept-Encoding": "gzip, deflate, br",
            "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
            "Sec-Fetch-Mode": "navigate",
            "Cookie": cookie,
        }
        self.parser = Parser()
        self.EMPTY = {"results": [], "pages": 0}
        self.RESULTS_PER_PAGE = {
            "web": 10,
            "pic": 20,
            "zhidao": 10,
            "news": 10,
            "jingyan": 10,
        }

    def search_web(
        self,
        query: str,
        pn: int = 1,
        exclude: list = [],
        time: Union[tuple, str, None] = None,
        proxies: dict = None,
    ) -> WebResult:
        """百度网页搜索。

        - 简单搜索:
            ```python
            BaiduSpider().search_web('搜索词')
            ```
            `plain`返回值:
            ```python
            {
                'results': [
                    {
                        'result': int,  # 总计搜索结果数,
                        'type': 'total'  # type用来区分不同类别的搜索结果
                    },
                    {
                        'results': [
                            str,  # 相关搜索建议
                            '...',
                            '...',
                            '...',
                            ...
                        ],
                        'type': 'related'
                    },
                    {
                        'process': str,  # 算数过程
                        'result': str,  # 运算结果
                        'type': 'calc'
                        # 这类搜索结果仅会在搜索词涉及运算时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'author': str,  # 新闻来源
                                'time': str,  # 新闻发布时间
                                'title': str,  # 新闻标题
                                'url': str,  # 新闻链接
                                'des': str,  # 新闻简介,大部分情况为None
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'news'
                        # 这类搜索结果仅会在搜索词有相关新闻时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'cover': str,  # 视频封面图片链接
                                'origin': str,  # 视频来源
                                'length': str,  # 视频时长
                                'title': str,  # 视频标题
                                'url': str,  # 视频链接
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'video'
                        # 这类搜索结果仅会在搜索词有相关视频时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': str,  # 百科封面图片/视频链接
                            'cover-type': str,  # 百科封面类别,图片是image,视频是video
                            'des': str,  # 百科简介
                            'title': str,  # 百科标题
                            'url': str,  # 百科链接
                        },
                        'type': 'baike'
                        # 这类搜索结果仅会在搜索词有相关百科时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': str,  # 贴吧封面图片链接
                            'des': str,  # 贴吧简介
                            'title': str,  # 贴吧标题
                            'url': str,  # 贴吧链接
                            'followers': str,  # 贴吧关注人数(可能有汉字,如:1万)
                            'hot': [{  # list, 热门帖子
                                'clicks': str,  # 帖子点击总数
                                'replies': str,  # 帖子回复总数
                                'title': str,  # 帖子标题
                                'url': str,  # 帖子链接
                            }],
                            'total': str,  # 贴吧总帖子数(可能有汉字,如:17万)'
                        },
                        'type': 'tieba'
                        # 这类搜索结果仅会在搜索词有相关贴吧时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'blogs': [{  # list, 博客列表
                                'des': str,  # 博客简介,没有时为`None`
                                'origin': str,  # 博客来源
                                'tags': [  # list, 博客标签
                                    str,  # 标签文字
                                ],
                                'title': str,  # 博客标题
                                'url': str,  # 博客链接
                            }],
                            'title': str,  # 博客搜索标题
                            'url': str,  # 博客搜索链接 (https://kaifa.baidu.com)
                        },
                        'type': 'blog'
                        # 这类搜索结果仅会在搜索词有相关博客时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'title': str,  # 仓库标题
                            'des': str,  # 仓库简介
                            'url': str,  # 仓库链接
                            'star': int,  # 仓库star数
                            'fork': int,  # 仓库fork数
                            'watch': int,  # 仓库watch数
                            'license': str,  # 仓库版权协议
                            'lang': str,  # 仓库使用的编程语言
                            'status': str,  # 仓库状态图表链接
                        },
                        'type': 'gitee'
                        # 这类搜索结果仅会在搜索词有相关代码仓库时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            {
                                'songs': [{  # list, 歌曲信息
                                    'album': {  # dict, 歌曲专辑
                                        'name': str,  # 专辑名称
                                        'url': str,  # 专辑链接
                                    },
                                    'singer': [{  # list, 歌手信息
                                        'name': str,  # 歌手名称
                                        'url': str,  # 歌手链接
                                    }],
                                    'song': {  # dict, 歌曲信息
                                        'copyright': bool,  # 歌曲是否有版权
                                        'duration': datetime.time,  # 歌曲时长
                                        'is_original': bool,  # 歌曲是否为原唱
                                        'labels': List[str],  # 歌曲标签
                                        'name': str,  # 歌曲名称
                                        'other_sites: List[str],  # 歌曲在其他网站发布的链接
                                        'poster': str,  # 歌曲海报图片链接
                                        'pub_company': str,  # 歌曲发行公司名称
                                        'pub_date': datetome.datetime,  # 歌曲发行时间
                                        'site': str,  # 歌曲发布网站名称(拼音)
                                        'url': str  # 歌曲链接
                                    }
                                }],
                                'title': str,  # 音乐标题
                                'url': str  # 音乐链接
                            }
                        },
                        'type': 'music'
                        # 这类搜索结果仅会在搜索词有相关音乐时出现,不一定每个搜索结果都会出现的
                    }
                    {
                        'des': str,  # 搜索结果简介
                        'origin': str,  # 搜索结果的来源,可能是域名,也可能是名称
                        'time': str,  # 搜索结果的发布时间
                        'title': str,  # 搜索结果标题
                        'type': 'result,  # 正经的搜索结果
                        'url': str  # 搜索结果链接
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'pages': int  # 总计的搜索结果页数,可能会因为当前页数的变化而随之变化
            }
            ```

        - 带页码:
            ```python
            BaiduSpider().search_web('搜索词', pn=2)
            ```

        - 按需解析:
            ```python
            BaiduSpider().search_web('搜索词', exclude=['要屏蔽的子部件列表'])
            ```
            可选值:`['news', 'video', 'baike', 'tieba', 'blog', 'gitee', 'related', 'calc', 'music']`,
            分别表示:资讯,视频,百科,贴吧,博客,Gitee代码仓库,相关搜索,计算。
            当`exclude=['all']`时,将仅保留基本搜索结果和搜索结果总数。
            如果`all`在`exclude`列表里,则将忽略列表中的剩余部件,返回`exclude=['all']`时的结果。

        - 按时间筛选:
            ```python
            BaiduSpider().search_web('搜索词', time=(开始时间, 结束时间))
            ```
            其中,开始时间和结束时间均为datetime.datetime类型,或者是使用time.time()函数生成的时间戳。
            time参数也可以是以下任意一个字符串:`['day', 'week', 'month', 'year']`。它们分别表示:一天内、
            一周内、一月内、一年内。当`time`为`None`时,BaiduSpider将展示全部结果,忽略筛选。
            如果参数非法,BaiduSpider会忽略此次筛选。

        - 设置代理:
            ```python
            BaiduSpider().search_web('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 要爬取的搜索词.
            pn (int, optional): 爬取的页码. Defaults to 1.
            exclude (list, optional): 要屏蔽的控件. Defaults to [].
            time (Union[tuple, str, None]): 按时间筛选参数. Defaults to None.
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            WebResult: 爬取的返回值和搜索结果
        """
        error = None
        result = self.EMPTY
        # 按需解析
        if "all" in exclude:
            exclude = [
                "news",
                "video",
                "baike",
                "tieba",
                "blog",
                "gitee",
                "calc",
                "related",
                "music",
            ]
        # 按时间筛选
        if type(time) == str:
            to = datetime.datetime.now()
            from_ = datetime.datetime(to.year, to.month, to.day, to.hour,
                                      to.minute, to.second, to.microsecond)
            if time == "day":
                from_ += datetime.timedelta(days=-1)
            elif time == "week":
                from_ += datetime.timedelta(days=-7)
            elif time == "month":
                from_ += datetime.timedelta(days=-31)
            elif time == "year":
                from_ += datetime.timedelta(days=-365)
        elif type(time) == tuple or type(time) == list:
            from_ = time[0]
            to = time[1]
        else:
            to = from_ = None
        if type(to) == datetime.datetime and type(from_) == datetime.datetime:
            FORMAT = "%Y-%m-%d %H:%M:%S"
            to = int(
                time_lib.mktime(time_lib.strptime(to.strftime(FORMAT),
                                                  FORMAT)))
            from_ = int(
                time_lib.mktime(
                    time_lib.strptime(from_.strftime(FORMAT), FORMAT)))
        try:
            text = quote(query, "utf-8")
            url = "https://www.baidu.com/s?wd=%s&pn=%d&inputT=%d" % (
                text,
                (pn - 1) * 10,
                random.randint(500, 4000),
            )
            if to is not None and from_ is not None:
                url += "&gpc=" + quote(f"stf={from_},{to}|stftype=2")
            # 解析Cookie
            cookie = self.headers["Cookie"]
            if cookie is not None:
                if cookie.find("__yjs_duid") == -1:
                    pass
                else:
                    _ = cookie.split("__yjs_duid=")
                    __ = _[1].split(";", 1)[-1]
                    ___ = hashlib.md5()
                    cookie = _[0] + "__yjs_duid=1_" + str(___.hexdigest()) + __
            self.headers["Cookie"] = cookie
            content = self._get_response(url, proxies)
            # print(content)
            results = self.parser.parse_web(content, exclude=exclude)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error, "BaiduSpider", "parse-web")
        pages = self._calc_pages(results["total"],
                                 self.RESULTS_PER_PAGE["web"])
        return WebResult._build_instance(plain=results["results"],
                                         pages=pages,
                                         total=results["total"])

    def search_pic(self,
                   query: str,
                   pn: int = 1,
                   proxies: dict = None) -> PicResult:
        """百度图片搜索。

        - 实例:
            ```python
            BaiduSpider().search_pic('搜索词')
            ```
            `plain`返回值:
            ```python
            {
                'results': [
                    {
                        'host': str,  # 图片来源域名
                        'title': str,  # 图片标题
                        'url': str,  # 图片链接
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'pages': int  # 搜索结果总计页码,可能会变化
            }
            ```

        - 带页码的搜索:
            ```python
            BaiduSpider().search_pic('搜索词', pn=2)
            ```

        - 设置代理:
            ```python
            BaiduSpider().search_pic('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 要爬取的query
            pn (int, optional): 爬取的页码. Defaults to 1.
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            PicResult: 爬取的搜索结果
        """
        error = None
        result = self.EMPTY
        try:
            url = "http://image.baidu.com/search/flip?tn=baiduimage&word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 20,
            )
            content = self._get_response(url, proxies)
            result = self.parser.parse_pic(content)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            self._handle_error(error)
        pages = self._calc_pages(result["total"], self.RESULTS_PER_PAGE["pic"])
        return PicResult._build_instance(plain=result["results"],
                                         pages=pages,
                                         total=result["total"])

    def search_zhidao(
        self,
        query: str,
        pn: int = 1,
        time: Union[str, None] = None,
        proxies: dict = None,
    ) -> ZhidaoResult:
        """百度知道搜索。

        - 普通搜索:
            ```python
            BaiduSpider().search_zhidao('搜索词')
            ```
            `plain`返回值:
            ```python
            {
                'results': [
                    {
                        'count': int,  # 回答总数
                        'date': str,  # 回答发布日期
                        'question': str,  # 问题简介
                        'answer': str,  # 回答简介
                        'agree': int,  # 回答赞同数
                        'answerer': str,  # 回答者
                        'title': str,  # 问题标题
                        'url': str  # 问题链接
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'pages': int  # 搜索结果最大页数,可能会变化
            }
            ```

        - 带页码的搜索:
            ```python
            BaiduSpider().search_zhidao('搜索词', pn=2)
            ```

        - 按时间筛选:
            ```python
            BaiduSpider().search_zhidao('搜索词', time='时间范围')
            ```
            其中,time参数可以是以下任意一个字符串:['week', 'month', 'year']。它们分别表示:一周内、一月内、
            一年内。当`time`为`None`时,BaiduSpider将展示全部结果,忽略筛选。
            如果参数非法,BaiduSpider会忽略此次筛选。

        - 设置代理:
            ```python
            BaiduSpider().search_zhidao('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.
            time (Union[str, None], optional): 时间筛选参数. Defaults to None.
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            dict: 搜索结果以及总页码
        """
        error = None
        result = self.EMPTY
        _ = [None, None, "week", "month", "year"]
        if _.index(time) > 0:
            time = _.index(time)
        else:
            time = 0
        try:
            url = (
                "https://zhidao.baidu.com/search?lm=0&rn=10&fr=search&pn=%d&word=%s&date=%d"
                % ((pn - 1) * 10, quote(query), time))
            code = self._get_response(url, proxies, "gb2312")
            # 转化编码
            # source.encoding = "gb2312"
            # code = source.text
            result = self.parser.parse_zhidao(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        pages = self._calc_pages(result["total"],
                                 self.RESULTS_PER_PAGE["zhidao"])
        return ZhidaoResult._build_instance(result["results"], pages,
                                            result["total"])

    def search_video(self,
                     query: str,
                     pn: int = 1,
                     proxies: dict = None) -> VideoResult:
        """百度视频搜索。

        - 普通搜索:
            ```python
            BaiduSpider().search_video('搜索词')
            ```
            `plain`返回值:
            ```python
            {
                'results': [
                    {
                        'img': str,  # 视频封面图片链接
                        'time': str,  # 视频时长
                        'title': str,  # 视频标题
                        'url': str  # 视频链接
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                'pages': int  # 搜索结果最大页数,可能因搜索页数改变而改变
            }
            ```

        - 带页码:
            ```python
            BaiduSpider().search_video('搜索词', pn=2)
            ```

        - 设置代理:
            ```python
            BaiduSpider().search_video('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            VideoResult: 爬取的搜索结果
        """
        error = None
        result = self.EMPTY
        try:
            url = (
                "https://www.baidu.com/sf/vsearch?pd=video&tn=vsearch&wd=%s&pn=%d&async=1"
                % (quote(query), (pn - 1) * 10))
            # 获取源码
            code = self._get_response(url, proxies)
            result = self.parser.parse_video(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return VideoResult._build_instance(result["results"])

    def search_news(
        self,
        query: str,
        pn: int = 1,
        sort_by: str = "focus",
        show: str = "all",
        proxies: dict = None,
    ) -> NewsResult:
        """百度资讯搜索。

        - 获取资讯搜索结果:
            ```python
            BaiduSpider().search_news('搜索词')
            ```
            `plain`返回值:
            ```python
            {
                'results': [
                    {
                        'author': str,  # 资讯来源(作者)
                        'date': str,  # 资讯发布时间
                        'des': str,  # 资讯简介
                        'title': str,  # 资讯标题
                        'url': str  # 资讯链接
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'pages': int  # 搜索结果最大页码,可能会因为当前页数变化而变化
            }
            ```

        - 带页码:
            ```python
            BaiduSpider().search_news('搜索词', pn=2)
            ```

        - 排序方式:
            目前支持两种排序方式:按焦点排序(默认)和按时间排序。可以通过设置`sort_by`参数来设置排序方式。`sort_by`
            参数只支持两种值:`focus`(按焦点排序,默认值)和`time`(按时间排序)。样例:
            ```python
            BaiduSpider().search_news('搜索词', sort_by='time')  # 按时间排序
            ```

        - 筛选:
            你可以通过设置`show`参数设置要筛选显示的资讯来源。目前支持三种来源:`all`(全部显示,默认)、`media`(媒体)
            和`baijiahao`(百家号)。样例:
            ```python
            BaiduSpider().search_news('搜索词', show='media')  # 仅显示来自媒体的新闻结果
            ```

        - 设置代理:
            ```python
            BaiduSpider().search_news('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 搜索query
            pn (int, optional): 搜索结果的页码. Defaults to 1.
            sort_by (str, optional): 搜索结果排序方式. Defaults to "focus".
            show (str, optional): 搜索结果筛选方式. Defaults to "all".
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            NewsResult: 爬取的搜索结果与总页码。
        """
        error = None
        result = self.EMPTY
        try:
            if sort_by == "time":
                sort_by = 4
            else:
                sort_by = 1
            if show == "media":
                show = 1
            elif show == "baijiahao":
                show = 2
            else:
                show = 0
            url = (
                "https://www.baidu.com/s?tn=news&wd=%s&pn=%d&rtt=%d&medium=%d&cl=2"
                % (quote(query), (pn - 1) * 10, sort_by, show))
            # 源码
            code = self._get_response(url, proxies)
            result = self.parser.parse_news(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        pages = self._calc_pages(result["total"],
                                 self.RESULTS_PER_PAGE["news"])
        return NewsResult._build_instance(result["results"], pages,
                                          result["total"])

    def search_wenku(
        self,
        query: str,
        pn: int = 1,
        scope: str = "all",
        format: str = "all",
        time: str = "all",
        page_range: Union[Tuple[int], str] = "all",
        sort_by: str = "relation",
        proxies: dict = None,
    ) -> WenkuResult:
        """百度文库搜索。

        请注意,目前百度文库搜索若报错,则可能需要先手动打开百度文库搜索
        (`https://wenku.baidu.com/search?word=placeholder&lm=0&od=0&fr=top_home&ie=utf-8`)
        通过安全验证后才能正常搜索。

        - 普通搜索:
            ```python
            BaiduSpider().search_wenku('搜索词')
            ```
            `plain`返回值:
            ```python
            {
                'results': [
                    {
                        'pub_date': str,  # 文档发布日期
                        'des': str,  # 文档简介
                        'downloads': int,  # 文档下载量
                        'pages': int,  # 文档页数
                        'title': str,  # 文档标题
                        'type': str,  # 文档格式,为全部大写字母
                        'url': str,  # 文档链接
                        'quality': float,  # 文档质量分
                        'uploader': {  # dict, 文档上传者信息
                            'name': str,  # 文档上传者用户名
                            'url': str  # 文档上传者链接
                        },
                        'is_vip': bool  # 该文档是否需要VIP权限下载
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'pages': int  # 总计搜索结果的页数
            }
            ```

        - 带页码的搜索:
            ```python
            BaiduSpider().search_wenku('搜索词', pn=2)
            ```

        - 筛选范围:
            BaiduSpider支持五种筛选范围:全部、VIP专享、VIP免费、免费、精品。你可以使用`scope`参数来定义此次搜索
            范围。`scope`参数的取值可以是下列任一一项:['all', 'vip-only', 'vip-free', 'free', 'high-quality']。
            他们的含义与上文所述一致。样例:
            ```python
            BaiduSpider().search_wenku('搜索词', scope='free')  # 仅显示免费的文档
            ```
            若`scope`参数非法,BaiduSpider将忽略此次筛选。

        - 格式筛选:
            BaiduSpider支持六种格式筛选。你可以通过设置`format`参数来设置需要筛选的格式。`format`参数取值可以是:
            `all`(全部)、`doc`(DOC文档)、`ppt`(幻灯片文档)、`txt`(纯文本文档)、`pdf`(PDF文档)、`xls`
            (Excel表格文档)。例如:
            ```python
            BaiduSpider().search_wenku('搜索词', format='ppt')  # 仅显示幻灯片文档
            ```
            若`format`参数非法,BaiduSpider将忽略此次筛选。

        - 按时间筛选:
            BaiduSpider提供百度文科搜索的按时间筛选。`time`参数接受的合法传参如下:['all', 'this-year', 'last-year',
            'previous-years']。他们分别表示全部、今年、去年和前年及以前。示例:
            ```python
            BaiduSpider().search_wenku('搜索词', time='last-year')  # 仅显示去年上传的文档
            ```
            若`time`参数非法,BaiduSpider将忽略此次筛选。

        - 按页数筛选:
            BaiduSpider提供使用页数筛选文档,参数为`page_range`。`page_range`可选值为:['all', Tuple[start: int, end: int]]。
            分别表示全部和Tuple[开始页码(`int`), 结束页码(`int`)]。样例:
            ```python
            BaiduSpider().search_wenku('搜索词', page_range=(0, 10))  # 仅显示页数为0 - 10页的文档
            ```
            若`page_range`参数非法,BaiduSpider将忽略此次筛选。

        - 搜索结果排序:
            BaiduSpider提供由百度文库内置的搜索结果排序。你可以通过设置`sort_by`参数来设置排序方式,默认为按相关性排序。`sort_by`可选值为:
            ['relation', 'time', 'downloads', 'score'],分别表示按相关性、按时间、按下载量和按评分排序。样例:
            ```python
            BaiduSpider().search_wenku('搜索词', sort_by='downloads')  # 按下载量排序
            ```
            若`sort_by`参数非法,BaiduSpider将忽略此次筛选。

        - 设置代理:
            ```python
            BaiduSpider().search_wenku('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索的页码. Defaults to 1.
            scope (str, optional): 按范围筛选. Defaults to "all".
            format (str, optional): 按格式筛选. Defaults to "all".
            time (str, optional): 按时间筛选. Defaults to "all".
            page_range (Union[str, Tuple[int]]): 按页数筛选. Defaults to "all".
            sort_by (str): 排序方式. Defaults to "relation".
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            WenkuResult: 搜索结果和总计页数
        """
        error = None
        result = self.EMPTY
        # 范围筛选
        _ = ["all", "vip-only", "vip", "free", "high-quality"]
        if _.index(scope) > 0:
            scope = _.index(scope)
        else:
            scope = 0
        # 格式筛选
        _ = ["all", "doc", "pdf", "ppt", "xls", "txt"]
        if _.index(format) > 0:
            format = _.index(format)
        else:
            format = 0
        # 按时间筛选
        _ = ["all", "this-year", "last-year", "previous-years"]
        if _.index(time) > 0:
            time = _.index(time)
        else:
            time = 0
        # 按页数筛选
        if (type(page_range) is tuple and len(page_range) == 2
                and type(page_range[0]) is int and type(page_range[1]) is int):
            pass
        else:
            page_range = -1
        # 排序方式
        _ = ["relation", "time", "downloads", "score"]
        if _.index(sort_by) > 0:
            sort_by = _.index(sort_by)
        else:
            sort_by = 0
        try:
            url = (
                "https://wenku.baidu.com/gsearch/search/pcsearch?word=%s&pn=%d&fr=top_home&fd=%d&lm=%d&pt=%d&od=%d"
                % (quote(query), (pn - 1) * 10, scope, format, time, sort_by))
            if page_range != -1:
                url += "&pb=%d&pe=%d" % (page_range[0], page_range[1])
            else:
                url += "&pg=0"
            code = self._get_response(url, proxies)
            result = self.parser.parse_wenku(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return WenkuResult._build_instance(result["results"], result["pages"])

    def search_jingyan(self,
                       query: str,
                       pn: int = 1,
                       scope: str = "all",
                       proxies: dict = None) -> JingyanResult:
        """百度经验搜索。

        - 例如:
            ```python
            BaiduSpider().search_jingyan('关键词')
            ```
            `plain`返回值:
            ```python
            {
                'results': [
                    {
                        'title': str,  # 经验标题
                        'url': str,  # 经验链接
                        'des': str,  # 经验简介
                        'date': str,  # 经验发布日期
                        'category': List[str],  # 经验分类
                        'votes': int,  # 经验的支持票数
                        'publisher': {  # dict, 经验发布者信息
                            'name': str,  # 经验发布者用户名
                            'url': str  # 经验发布者链接
                        },
                        'is_original': bool,  # 经验是否为原创
                        'is_outstanding': bool  # 经验是否为优秀经验
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'pages': int  # 总计搜索结果页数
            }
            ```

        - 带页码的:
            ```python
            BaiduSpider().search_jingyan('搜索词', pn=2)
            ```

        - 筛选经验:
            你可以通过设置`scope`参数来设定筛选范围。`scope`参数的可选值为:["all", "outstanding", "praise", "original"]。
            它们分别表示:全部经验、优秀经验、最受好评和原创经验。例子:
            ```python
            BaiduSpider().search_jingyan('搜索词', scope="outstanding")  # 仅显示优秀经验
            ```

        - 设置代理:
            ```python
            BaiduSpider().search_jingyan('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 要搜索的关键词
            pn (int, optional): 搜索结果的页码. Defaults to 1.
            scope (str, optional): 筛选范围. Defaults to "all".
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            JingyanResult: 搜索结果以及总计的页码.
        """
        error = None
        result = self.EMPTY
        _ = ["all", "outstanding", "praise", "original"]
        if _.index(scope) > 0:
            scope = _.index(scope)
        else:
            scope = 0
        try:
            url = "https://jingyan.baidu.com/search?word=%s&pn=%d&lm=%d" % (
                quote(query),
                (pn - 1) * 10,
                scope,
            )
            code = self._get_response(url, proxies)
            result = self.parser.parse_jingyan(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        pages = self._calc_pages(result["total"],
                                 self.RESULTS_PER_PAGE["jingyan"])
        return JingyanResult._build_instance(result["results"], pages,
                                             result["total"])

    def search_baike(self, query: str, proxies: dict = None) -> BaikeResult:
        """百度百科搜索。

        - 使用方法:
            ```python
            BaiduSpider().search_baike('搜索词')
            ```
            `plain`返回值:
            ```python
            {
                'results': {
                    [
                        'title': str,  # 百科标题
                        'des': str,  # 百科简介
                        'date': str,  # 百科最后更新时间
                        'url': str  # 百科链接
                    ],
                    [ ... ],
                    [ ... ],
                    [ ... ]
                },
                'total': int  # 搜索结果总数
            }
            ```

        - 设置代理:
            ```python
            BaiduSpider().search_baike('搜索词', proxies={
                "http": "http://xxx.xxx.xxx:xxxx",  # HTTP代理
                "https": "https://xxx.xxx.xxx:xxxx"  # HTTPS代理
            })
            ```
            详细配置请参考[requests文档](https://docs.python-requests.org/zh_CN/latest/user/advanced.html?highlight=proxies#proxies)。

        Args:
            query (str): 要搜索的关键词.
            proxies (Union[dict, None]): 代理配置. Defaults to None.

        Returns:
            dict: 搜索结果和总页数
        """
        error = None
        result = self.EMPTY
        try:
            url = "https://baike.baidu.com/search?word=%s" % quote(query)
            code = self._get_response(url, proxies)
            result = self.parser.parse_baike(code)
            result = result if result is not None else self.EMPTY
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return BaikeResult._build_instance(result["results"], result["total"])
Beispiel #5
0
class BaiduSpider(BaseSpider):
    def __init__(self) -> None:
        """爬取百度的搜索结果.

        本类的所有成员方法都遵循下列格式:

            {
                'results': <一个列表,表示搜索结果,内部的字典会因为不同的成员方法而改变>,
                'total': <一个正整数,表示搜索结果的最大页数,可能会因为搜索结果页码的变化而变化,因为百度不提供总共的搜索结果页数>
            }

        目前支持百度搜索,百度图片,百度知道,百度视频,百度资讯,百度文库,百度经验和百度百科,并且返回的搜索结果无广告。继承自``BaseSpider``。

        BaiduSpider.`search_web(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度网页搜索

        BaiduSpider.`search_pic(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度图片搜索

        BaiduSpider.`search_zhidao(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度知道搜索

        BaiduSpider.`search_video(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度视频搜索

        BaiduSpider.`search_news(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度资讯搜索

        BaiduSpider.`search_wenku(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度文库搜索

        BaiduSpider.`search_jingyan(self: BaiduSpider, query: str, pn: int = 1) -> dict`: 百度经验搜索

        BaiduSpider.`search_baike(self: BaiduSpider, query: str) -> dict`: 百度百科搜索
        """
        super().__init__()
        # 爬虫名称(不是请求的,只是用来标识)
        self.spider_name = "BaiduSpider"
        # 设置请求头
        self.headers = {
            "User-Agent":
            "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
            "Referer": "https://www.baidu.com",
            "Accept":
            "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
            "Accept-Encoding": "gzip, deflate, br",
            "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
        }
        self.parser = Parser()

    def search_web(self, query: str, pn: int = 1, exclude: list = []) -> dict:
        """百度网页搜索.

        - 简单搜索:
            >>> BaiduSpider().search_web('搜索词')
            {
                'results': [
                    {
                        'result': int, 总计搜索结果数,
                        'type': 'total'  # type用来区分不同类别的搜索结果
                    },
                    {
                        'results': [
                            'str, 相关搜索建议',
                            '...',
                            '...',
                            '...',
                            ...
                        ],
                        'type': 'related'
                    },
                    {
                        'process': 'str, 算数过程',
                        'result': 'str, 运算结果',
                        'type': 'calc'
                        # 这类搜索结果仅会在搜索词涉及运算时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'author': 'str, 新闻来源',
                                'time': 'str, 新闻发布时间',
                                'title': 'str, 新闻标题',
                                'url': 'str, 新闻链接',
                                'des': 'str, 新闻简介,大部分情况为None'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'news'
                        # 这类搜索结果仅会在搜索词有相关新闻时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'results': [
                            {
                                'cover': 'str, 视频封面图片链接',
                                'origin': 'str, 视频来源',
                                'length': 'str, 视频时长',
                                'title': 'str, 视频标题',
                                'url': 'str, 视频链接'
                            },
                            { ... },
                            { ... },
                            { ... },
                            ...
                        ],
                        'type': 'video'
                        # 这类搜索结果仅会在搜索词有相关视频时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': 'str, 百科封面图片/视频链接',
                            'cover-type': 'str, 百科封面类别,图片是image,视频是video',
                            'des': 'str, 百科简介',
                            'title': 'str, 百科标题',
                            'url': 'str, 百科链接'
                        },
                        'type': 'baike'
                        # 这类搜索结果仅会在搜索词有相关百科时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'cover': 'str, 贴吧封面图片链接',
                            'des': 'str, 贴吧简介',
                            'title': 'str, 贴吧标题',
                            'url': 'str, 贴吧链接',
                            'followers': 'str, 贴吧关注人数(可能有汉字,如:1万)',
                            'hot': [{  # list, 热门帖子
                                'clicks': 'str, 帖子点击总数',
                                'replies': 'str, 帖子回复总数',
                                'title': 'str, 帖子标题',
                                'url': 'str, 帖子链接'
                            }],
                            'total': 'str, 贴吧总帖子数(可能有汉字,如:17万)'
                        },
                        'type': 'tieba'
                        # 这类搜索结果仅会在搜索词有相关贴吧时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'blogs': [{  # list, 博客列表
                                'des': 'str, 博客简介,没有时为`None`',
                                'origin': 'str, 博客来源',
                                'tags': [  # list, 博客标签
                                    'str, 标签文字'
                                ],
                                'title': 'str, 博客标题',
                                'url': 'str, 博客链接'
                            }],
                            'title': 'str, 博客搜索标题',
                            'url': 'str, 博客搜索链接 (https://kaifa.baidu.com)'
                        },
                        'type': 'blog'
                        # 这类搜索结果仅会在搜索词有相关博客时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'result': {
                            'title': 'str, 仓库标题',
                            'des': 'str, 仓库简介',
                            'url': 'str, 仓库链接',
                            'star': int, 仓库star数,
                            'fork': int, 仓库fork数,
                            'watch': int, 仓库watch数,
                            'license': 'str, 仓库版权协议',
                            'lang': 'str, 仓库使用的编程语言',
                            'status': 'str, 仓库状态图表链接'
                        },
                        'type': 'gitee'
                        # 这类搜索结果仅会在搜索词有相关代码仓库时出现,不一定每个搜索结果都会出现的
                    },
                    {
                        'des': 'str, 搜索结果简介',
                        'origin': 'str, 搜索结果的来源,可能是域名,也可能是名称',
                        'time': 'str, 搜索结果的发布时间',
                        'title': 'str, 搜索结果标题',
                        'type': 'result',  # 正经的搜索结果
                        'url': 'str, 搜索结果链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计的搜索结果页数,可能会因为当前页数的变化而随之变化
            }

        - 带页码:
            >>> BaiduSpider().search_web('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        - 按需解析:
            >>> BaiduSpider().search_web('搜索词', exclude=['要屏蔽的子部件列表'])
            可选值:['news', 'video', 'baike', 'tieba', 'blog', 'gitee', 'related', 'calc'],
            分别表示:资讯,视频,百科,贴吧,博客,Gitee代码仓库,相关搜索,计算。
            当exclude=['all']时,将仅保留基本搜索结果和搜索结果总数。
            如果'all'在exclude列表里,则将忽略列表中的剩余部件,返回exclude=['all']时的结果。

        Args:
            query (str): 要爬取的搜索词.
            pn (int, optional): 爬取的页码. Defaults to 1.
            exclude (list, optional): 要屏蔽的控件. Defaults to [].

        Returns:
            dict: 爬取的返回值和搜索结果
        """
        error = None
        results = {"results": [], "pages": 0}
        # 按需解析
        if "all" in exclude:
            exclude = [
                "news",
                "video",
                "baike",
                "tieba",
                "blog",
                "gitee",
                "calc",
                "related",
            ]
        try:
            text = quote(query, "utf-8")
            url = "https://www.baidu.com/s?wd=%s&pn=%d" % (text, (pn - 1) * 10)
            content = self._get_response(url)
            results = self.parser.parse_web(content, exclude=exclude)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error, "BaiduSpider", "parse-web")
        return {"results": results["results"], "total": results["pages"]}

    def search_ads(self, query: str, pn: int = 1) -> dict:
        error = None
        results = {"results": [], "pages": 0}
        try:
            text = quote(query, "utf-8")
            url = "https://www.baidu.com/s?wd=%s&pn=%d" % (text, (pn - 1) * 10)
            content = self._get_response(url)
            results = self.parser.parse_advertising(content)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error, "BaiduSpider", "parse-advertising")
        return {"results": results["results"], "total": results["pages"]}

    def flat(self, result: dict) -> list:
        """
        "扁平化"搜索结果
        结果样例:
        [['python吧 - 百度贴吧',                                      # 标题
          'http://tieba.baidu.com/f?kw=python&fr=ala0&loc=rec',     # 网址
          'python学习交流基地。',                                      # 描述
          'tieba'],                                                 # 类型
         ['python安装相关博客',                       # 标题
          'http://www.baidu.com/link?url=aJncmLXsD5iqIe47eQfESoydtwFay5podum390RFWEmiOcyntS4tM9Fo18_eDWOPoELF0NwLIKes-TusYYfWZnraAPRXZRJROaToaA05ifu',
          None,                                     # 没有描述
          'blog'],                                  # 类型
         ['Python - Gitee',
          'http://www.baidu.com/link?url=ssnksDzg7Cuh_uiT3hCLgiVYgx7A6tzYb1qGKjyQMjvZer4Y1AX1TGt1eAilYvYqphv8lhb31v_Lo7Q2oL4HrTkf_IXDrr9PtkmgfI9TFTX43KCgmuAEqE-X73K4CvZH',
          'Python 算法集',
          'gitee'],
         ......
         ['Python3.8.2中文版 32/64位 最新版 加速吧',
          'python3.8.2版是一款非常专业的通用型计算机程序设计语言安装包。目前大版本已经来到了3.8.2版本,同时随着版本的不断更新和语言新功能的添加,越来越多被用于独立的...',
          'http://www.baidu.com/link?url=s8pY0zaKIm7PY581uTkpEC2t_oHWV-5-5ta7V6QD4YrssPdlvqDvF1cQJykl0r2Jja3xyMRIQL6nDJI33NnTnq',
          'result']]
        Args:
            result: BaiduSpider().search_web() 的返回结果

        Returns:
            list: 扁平化的结果

        """
        flat_result = []
        for i in result['results']:
            if i['type'] == 'news':
                for j in i['results']:
                    flat_result.append(
                        (j['title'], j['url'], j['des'], 'news'))
            if i['type'] == 'baike':
                flat_result.append([
                    i['result']['title'], i['result']['url'],
                    i['result']['des'], 'baike'
                ])
            if i['type'] == 'tieba':
                flat_result.append([
                    i['result']['title'], i['result']['url'],
                    i['result']['des'], 'tieba'
                ])
            if i['type'] == 'blog':
                flat_result.append(
                    [i['result']['title'], i['result']['url'], None, 'blog'])
            if i['type'] == 'gitee':
                flat_result.append([
                    i['result']['title'], i['result']['url'],
                    i['result']['des'], 'gitee'
                ])
            if i['type'] == 'result':
                flat_result.append([i['title'], i['des'], i['url'], 'result'])
        return flat_result

    def convert_links(self, result: list):  # pragma: no cover
        """
        解析百度搜索结果中的链接,这个过程是多线程的,最多需要1.5秒。
        目前不可用
        """
        convert = {}

        def get_link_url(link):
            if 'www.baidu.com/link?url=' not in link:
                convert[link] = link
                return
            try:
                r = requests.get(link, timeout=1).text
                convert[link] = re.findall(r"URL='(http\S+)'", r)[0]
            except (requests.exceptions.ConnectTimeout,
                    requests.exceptions.ReadTimeout):
                convert[link] = link

        # from concurrent.futures import ThreadPoolExecutor
        # pool = ThreadPoolExecutor(10)
        for i in result:
            # pool.submit(get_link_url, link=i[1])
            print(i[1])
            if i[3] != 'blog':
                get_link_url(i[1])
        # pool.shutdown(wait=True)

        for i in result:
            i[1] = convert[i[1]]

        return result

    def search_pic(self, query: str, pn: int = 1) -> dict:
        """百度图片搜索.

        - 实例:
            >>> BaiduSpider().search_pic('搜索词')
            {
                'results': [
                    {
                        'host': 'str, 图片来源域名',
                        'title': 'str, 图片标题',
                        'url': 'str, 图片链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果总计页码,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_pic('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要爬取的query
            pn (int, optional): 爬取的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果
        """
        error = None
        try:
            url = "http://image.baidu.com/search/flip?tn=baiduimage&word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 20,
            )
            content = self._get_response(url)
            result = self.parser.parse_pic(content)
        except Exception as err:
            error = err
        finally:
            self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_zhidao(self, query: str, pn: int = 1) -> dict:
        """百度知道搜索.

        - 普通搜索:
            >>> BaiduSpider().search_zhidao('搜索词')
            {
                'results': [
                    {
                        'count': int, 回答总数,
                        'date': 'str, 发布日期',
                        'des': 'str, 简介',
                        'title': 'str, 标题',
                        'url': 'str, 链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页数,可能会变化
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_zhidao('搜索词', pn=2)  # `pn` !!
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总页码
        """
        error = None
        try:
            url = (
                "https://zhidao.baidu.com/search?lm=0&rn=10&pn=0&fr=search&pn=%d&word=%s"
                % ((pn - 1) * 10, quote(query)))
            source = requests.get(url, headers=self.headers)
            # 转化编码
            source.encoding = "gb2312"
            code = source.text
            result = self.parser.parse_zhidao(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_video(self, query: str, pn: int = 1) -> dict:
        """百度视频搜索.

        - 普通搜索:
            >>> BaiduSpider().search_video('搜索词')
            {
                'results': [
                    {
                        'img': 'str, 视频封面图片链接',
                        'time': 'str, 视频时长',
                        'title': 'str, 视频标题',
                        'url': 'str, 视频链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                'total': int, 搜索结果最大页数,可能因搜索页数改变而改变
            }

        - 带页码:
            >>> BaiduSpider().search_video('搜索词', pn=2)  # <=== `pn`
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果及总页码
        """
        error = None
        try:
            url = (
                "http://v.baidu.com/v?no_al=1&word=%s&pn=%d&ie=utf-8&db=0&s=0&fbl=800"
                % (quote(query), (60 if pn == 2 else (pn - 1) * 20)))
            # 获取源码
            source = requests.get(url, headers=self.headers)
            code = self._minify(source.text)
            result = self.parser.parse_video(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_news(self, query: str, pn: int = 1) -> dict:
        """百度资讯搜索.

        - 获取资讯搜索结果:
            >>> BaiduSpider().search_news('搜索词')
            {
                'results': [
                    {
                        'author': 'str, 资讯来源(作者)',
                        'date': 'str, 资讯发布时间',
                        'des': 'str, 资讯简介',
                        'title': 'str, 资讯标题',
                        'url': 'str, 资讯链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 搜索结果最大页码,可能会因为当前页数变化而变化
            }

        - 带页码:
            >>> BaiduSpider().search_news('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 搜索query
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 爬取的搜索结果与总页码。
        """
        error = None
        try:
            url = "https://www.baidu.com/s?rtt=1&bsst=1&tn=news&word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 10,
            )
            # 源码
            source = requests.get(url, headers=self.headers)
            # 压缩
            code = self._minify(source.text)
            result = self.parser.parse_news(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_wenku(self, query: str, pn: int = 1) -> dict:
        """百度文库搜索.

        - 普通搜索:
            >>> BaiduSpider().search_wenku('搜索词')
            {
                'results': [
                    {
                        'date': 'str, 文章发布日期',
                        'des': 'str, 文章简介',
                        'downloads': int, 文章下载量,
                        'pages': int, 文章页数,
                        'title': 'str, 文章标题',
                        'type': 'str, 文章格式,为全部大写字母',
                        'url': 'str, 文章链接'
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果的页数
            }

        - 带页码的搜索:
            >>> BaiduSpider().search_wenku('搜索词', pn=2)
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的query
            pn (int, optional): 搜索的页码. Defaults to 1.

        Returns:
            dict: 搜索结果和总计页数
        """
        error = None
        try:
            url = "https://wenku.baidu.com/search?word=%s&pn=%d" % (
                quote(query),
                (pn - 1) * 10,
            )
            source = requests.get(url, headers=self.headers)
            source.encoding = "gb2312"
            code = self._minify(source.text)
            result = self.parser.parse_wenku(code)
        except Exception as err:
            error = err
        finally:
            if error:
                self._handle_error(error)
        return {"results": result["results"], "total": result["pages"]}

    def search_jingyan(self, query: str, pn: int = 1) -> dict:
        """百度经验搜索.

        - 例如:
            >>> BaiduSpider().search_jingyan('关键词')
            {
                'results': [
                    {
                        'title': 'str, 经验标题',
                        'url': 'str, 经验链接',
                        'des': 'str, 经验简介',
                        'date': 'str, 经验发布日期',
                        'category': 'str, 经验分类',
                        'votes': int, 经验的支持票数
                    },
                    { ... },
                    { ... },
                    { ... },
                    ...
                ],
                'total': int, 总计搜索结果页数
            }

        - 带页码的:
            >>> BaiduSpider().search_jingyan('搜索词', pn=2) # `pn` 是页码
            {
                'results': [ ... ],
                'total': ...
            }

        Args:
            query (str): 要搜索的关键词
            pn (int, optional): 搜索结果的页码. Defaults to 1.

        Returns:
            dict: 搜索结果以及总计的页码.
        """
        url = "https://jingyan.baidu.com/search?word=%s&pn=%d&lm=0" % (
            quote(query),
            (pn - 1) * 10,
        )
        # 获取网页源代码
        source = requests.get(url, headers=self.headers)
        # 最小化代码
        code = self._minify(source.text)
        bs = BeautifulSoup(code, "html.parser")
        # 加载搜索结果
        data = bs.find("div", class_="search-list").findAll("dl")
        results = []
        for res in data:
            # 标题
            title = self._format(res.find("dt").find("a").text)
            # 链接
            url = "https://jingyan.baidu.com/" + res.find("dt").find(
                "a")["href"]
            # 简介
            des = self._format(
                res.find("dd").find("div", class_="summary").find(
                    "span", class_="abstract").text)
            # 获取发布日期和分类,位于`<span class="cate"/>`中
            tmp = self._format(
                res.find("dd").find("div", class_="summary").find(
                    "span", class_="cate").text).split("-")
            # 发布日期
            date = self._format(tmp[1])
            # 分类
            category = self._format(tmp[-1]).strip("分类:")
            # 支持票数
            votes = int(
                self._format(
                    res.find("dt").find("span",
                                        class_="succ-times").text).strip("得票"))
            # 生成结果
            result = {
                "title": title,
                "url": url,
                "des": des,
                "date": date,
                "category": category,
                "votes": votes,
            }
            results.append(result)  # 加入结果到集合中
        # 获取分页
        pages_ = bs.find("div", id="pg").findAll("a")[-1]
        # 既不是最后一页也没有超出最后一页
        if "尾页" in pages_.text:
            # 获取尾页并加一
            total = int(
                int(pages_["href"].split("&")[-1].strip("pn=")) / 10) + 1
        # 是最后一页或者是超过了最后一页
        else:
            # 重新获取分页
            pages_ = bs.find("div", id="pg").findAll("a")[1]
            # 获取尾页并加一
            total = int(
                int(pages_["href"].split("&")[-1].strip("pn=")) / 10) + 1
        return {"results": results, "total": total}

    def search_baike(self, query: str) -> dict:
        """百度百科搜索.

        - 使用方法:
            >>> BaiduSpider().search_baike('搜索词')
            {
                'results': {
                    [
                        'title': 'str, 百科标题',
                        'des': 'str, 百科简介',
                        'date': 'str, 百科最后更新时间',
                        'url': 'str, 百科链接'
                    ],
                    [ ... ],
                    [ ... ],
                    [ ... ]
                },
                'total': int, 搜索结果总数
            }

        Args:
            query (str): 要搜索的关键词

        Returns:
            dict: 搜索结果和总页数
        """
        # 获取源码
        source = requests.get(
            "https://baike.baidu.com/search?word=%s" % quote(query),
            headers=self.headers,
        )
        code = self._minify(source.text)
        # 创建BeautifulSoup对象
        soup = (BeautifulSoup(code, "html.parser").find(
            "div", class_="body-wrapper").find("div", class_="searchResult"))
        # 获取百科总数
        total = int(
            soup.find(
                "div",
                class_="result-count").text.strip("百度百科为您找到相关词条约").strip("个"))
        # 获取所有结果
        container = soup.findAll("dd")
        results = []
        for res in container:
            # 链接
            url = "https://baike.baidu.com" + self._format(
                res.find("a", class_="result-title")["href"])
            # 标题
            title = self._format(res.find("a", class_="result-title").text)
            # 简介
            des = self._format(res.find("p", class_="result-summary").text)
            # 更新日期
            date = self._format(res.find("span", class_="result-date").text)
            # 生成结果
            results.append({
                "title": title,
                "des": des,
                "date": date,
                "url": url
            })
        return {"results": results, "total": total}