Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

新华网爬虫(2022年6月)

1 分析网站结构

新华网网址:新华网_让新闻离你更近 (news.cn)

新华网的首页是带有关键词搜索功能的,我们尝试在搜索栏随意搜索一个关键词

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

可以发现新华网一次最多可以爬取的数据是10000条,且其数据是通过分页显示的

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

2 分析网页具体组成

在搜索后的显示页面按下F12进入开发者页面,切换到NetWork(网络),然后按下Ctrl+R 刷新页面,可以看到网页的各种请求。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

一般情况下服务器返回的数据有HTML和JSON格式的数据:

HTML:一般是选中筛选器中的(DOC)文档,然后点击其请求,最后点击Response(响应)。可以看到服务器的响应输出都是HTML格式的。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

对于这种的爬取可以先定位需要的数据的位置(用左上角的小箭头),然后使用正则表达式 ,BeautifulSoup , xpath 等等把需要的数据解析出来。

本次使用的是下面一种响应的数据格式。

JSON:某些时候,数据并不一定是放在HTML的标签中的,而是通过其他的数据接口,动态的请求加载进去的(Ajax),一般这种数据的请求类型是XHR,而数据的格式是JSON。

可以在筛选器中选择XHR,然后点击其请求,最后点击Response(响应)。可以看到服务器的响应输出是JSON格式(键值对构成)的。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

同时点击预览(Preview),可以看见其一页中的十条数据在results列表中以字典的形式存在,后续就可以在其服务器响应的数据中,在results列表中循环的爬取一页中的每一条数据。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

3 爬虫构建思路

我们回到爬虫的构建中来。点击标头可以发现其请求方法是GET请求,请求的参数可以在URL中构建。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

请求 URL:

http://so.news.cn/getNews?keyword=疫情&curPage=1&sortField=0&searchFields=1&lang=cn

通过分析其URL我们可以发现,其keyword就是我们的爬取的关键词”疫情”,只不过这里被按照UTF-8的格式进行编码了,UTF-8中,一个汉字对应三个字节,当然不同的网站编码不一定相同,还有按照GB2312编码的,一个汉字占用两个字节。

curPage就是我们目前所处的页数,这里就是第一页的意思。其他的参数我们可以不去考虑,这个和我们的爬虫的关系不是很大。

这里的爬虫思路可以分为两个:

第一个就是首先构建每一页的url,然后对每一页的url的response进行解析,并将数据存库(或者存本地)

第二就是首先将每一页的url构建后并存在redis中,然后利用分布式爬虫从redis中取url并进行response的解析与数据存库。

对于新华网我们采用第二种方式,而在人民网的爬取中,我们采用第一种方式。

4 具体实现

各个页面的url的构建和url存redis

在整个Scrapy项目种运行上述代码,就可以将当前关键词或者关键词组的对应的每一页的url存在Redis中,后续就可以进行多机器取url爬取,增加爬取速度,或者实现断点续爬等功能。

#!/usr/bin/env python
encoding: utf-8
import redis
import json
import time
from scrapy import Spider
from scrapy.crawler import CrawlerProcess
from scrapy.http import Request
from scrapy.utils.project import get_project_settings

class XinhuaNewsSearchSpider(Spider):
    name = "xinhua_news_search_spider"
    conn = redis.Redis(host="xx.xx.xx.xx", port=6379, password="xxxx")      # 链接Redis

    xinhua_news_url = "http://www.xinhuanet.com/"

    def __init__(self, keywords=None, *args, **kwargs):
        super(XinhuaNewsSearchSpider, self).__init__(*args, **kwargs)
        self.keywords = keywords

    def start_requests(self):
        keys = self.keywords.split("、")
        for key in keys:
            xinhua_search_url = "http://so.news.cn/getNews?keyword=%s&curPage=1&sortField=0&searchFields=0&lang=cn" % key
            yield Request(xinhua_search_url, callback=self.xinhua_parse)

    def xinhua_parse(self, response):
        data = json.loads(response.text)
        attr = response.url.split("curPage=1")
        pageCount = int(data['content']['pageCount'])       #获得当前的总页数
        for page in range(1, pageCount + 1):                #将所有页码对应的页面的url提取出来并保存
            url = attr[0] + "curPage=" + str(page) + "&sortField=0&searchFields=0&lang=cn"
            self.conn.lpush("xinhua_news_search_spider:start_urls", url)

if __name__ == "__main__":
     keys = '防疫、疫情、新冠'

    process = CrawlerProcess(get_project_settings())
    process.crawl('xinhua_news_search_spider', keywords=keys)
    process.start()

    print("xinhua-search执行完毕")

页面数据的爬取

等待上一步工作完成,就可以运行下面的代码,就可以将每一页的数据源源不断的爬取到你设置的位置

这里的爬取到的数据存入了数据库,需要在Scrapy的项目中设置好数据库的相关参数。

import re
import json
import redis
from scrapy import Spider
from scrapy.crawler import CrawlerProcess
from scrapy.selector import Selector
from scrapy.http import Request
from scrapy.utils.project import get_project_settings
from scrapy_redis.spiders import RedisSpider
from newsSpider.items import NewInformationItem_xinhua
from datetime import datetime

""""如果一个关键词相关的新闻总数超过10000,则只能查询前10000条数据。目前最多只能查询以时间为顺序的前10000条数据"""
class Xinhua_News_Info_Spider(RedisSpider):
    name = "xinhua_news_info_spider"
    # 相对于scrapy设置的start_urls,在scrapy_redis中只需要设置redis_key就可以了,
    # 爬虫会自动去redis的相应的key中取到url,然后包装成Request对象,保存在redis的待爬取队列(request       # queue)中。
    redis_key = "xinhua_news_search_spider:start_urls"

    def __init__(self, keywords=None, *args, **kwargs):
        super(Xinhua_News_Info_Spider, self).__init__(*args, **kwargs)

    """ 解析响应 """
    def parse(self, response):
        text = "".join([response.text.strip().rsplit("}", 1)[0], "}"])
        data = json.loads(text)
        for item in data['content']['results']:
            new_information = NewInformationItem_xinhua()       # 每一个item都是一条新闻的信息
            timeArray = str(item['pubtime']).split(' ')[0]      # 取出新闻发布时间的年月日
            new_information['title'] = re.sub("<font color="red">|</font>|&#xA0;|&quo|&", "", item['title'])                                              # &#xA0;&#x8FD9;&#x4E9B;&#x662F;HTML&#x8F6C;&#x4E49;&#x5B57;&#x7B26;
            new_information['url'] = item['url']
            new_information['author'] = item['sitename']
            new_information['create_time'] = item['pubtime']
            new_information['origin'] = "&#x65B0;&#x534E;&#x7F51;"
            dt = datetime.now()
            new_information['crawl_time'] = dt.strftime('%Y-%m-%d %H:%M:%S')
            if item['keyword'] is None:
                new_information['keyword'] = " "
            else:
                new_information['keyword'] = re.sub("<font color="red">|</font>", "", item['keyword'])
            yield Request(new_information['url'], callback=self.parse_content, dont_filter=True, meta={'new_item': new_information})

    def parse_content(self, response):
        new_item = response.meta['new_item']
        selector = Selector(response)
        """&#x6BCF;&#x4E2A;&#x7F51;&#x9875;&#x7684;&#x5185;&#x5BB9;&#x6709;&#x4E0D;&#x540C;&#x7684;id&#x6216;class"""
        """&#x6BCF;&#x4E2A;&#x7F51;&#x9875;&#x7684;&#x7ED3;&#x6784;&#x4E0D;&#x76F8;&#x540C;&#xFF0C;&#x65E0;&#x6CD5;&#x5C06;&#x6240;&#x6709;&#x7684;&#x7F51;&#x9875;&#x722C;&#x53D6;&#x5230;"""
        article = selector.xpath('//p//text()').extract()
        new_item['article'] = '\n'.join(article) if article else ''
        if article is None or str(article).strip() == "":         #str(article).strip():&#x662F;&#x628A;article&#x7684;&#x5934;&#x548C;&#x5C3E;&#x7684;&#x7A7A;&#x683C;&#xFF0C;&#x4EE5;&#x53CA;&#x4F4D;&#x4E8E;&#x5934;&#x5C3E;&#x7684;\n \t&#x4E4B;&#x7C7B;&#x7ED9;&#x5220;&#x6389;&#x3002;
            print(response.url)
        else:
            yield new_item

if __name__ == "__main__":
    conn = redis.Redis(host="192.168.1.103", port=6379, password="root")
    print("url&#x6570;&#x91CF;:" + str(conn.llen('xinhua_news_search_spider:start_urls')))

    process = CrawlerProcess(get_project_settings())
    process.crawl('xinhua_news_info_spider', keywords=keys)
    process.start()

人民网爬虫(2022年6月)

1 分析网站结构

人民网网址:人民网_网上的人民日报 (people.com.cn)

同样的,人民网的首页也是带有关键词搜索功能的,我们搜索栏随意搜索一个关键词

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

可以发现其显示的数据的形式也是分页显示的,同时我们发现其显示搜索到的数据是660522条,这个不是我们可以爬取的实际的数据大小,在人民网我们实际可以爬到的最大的数据量还是10000条,这个后续再说明。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

2 分析网页具体组成

这个在新华网的爬取部分已经简单介绍过了,就不再次说明了。同时我们本次爬取的数据格式还是按照JSON来获取。

3 爬虫构建思路

我们按下F12进入开发者页面,过滤器选中XHR,并点击search请求,再点击Header(标头),可以发现与新华网不同的是,其是POST请求,其url中并不携带参数,而是通过一个单独的参数列表进行参数的传递。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

这里的爬虫思路也可以分为两个:

第一个就是首先构建每一页的request(基础的url加上请求的参数),然后对每一页的request的response进行解析,并将数据存库(或者存本地)

第二就是首先将每一页的每一页的request构建后并存在redis中,然后利用分布式爬虫从redis中取request并进行response的解析与数据存库。与新华网爬虫不同的是,redis中只能直接存url,而post请求需要有请求的url和其携带的请求参数,所以要直接间request存入redis 是行不通的,解决办法可以通过重写scrapy-redis中的make_request_from_data方法,让其可以重新将我们的url和请求参数包装成request,并向服务器请求。具体的后续再出文章说明吧。

这里我们采用第一种方法来构建人民网爬虫

4 具体实现

POST请求中参数有两个重要的部分:body, headers

body里面承载的是我们的请求参数,在payload(负载)中可以清楚的看见。其中key就是我们的关键词,page就是我们当前的页面。其他的参数我们可以保持不变,对于爬虫影响不大

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

headers可以在headers栏中找到,我们需要的有Accept:希望服务器返回什么样的数据类型;Content-Type:告诉服务器给其传的数据的类型;Referer:告诉服务器该网页是从哪个页面链接过来的。不设置请求可能会被拦截从而获取不到数据。User-Agent:(这个在Scrapy中的setting中设置过了,在代码中不需要再设置一次):将爬虫伪装成浏览器进行请求,防止服务器识别出爬虫从而拦截。

Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

注意: 对于人民网 :1000页以后的数据无法获取,1000页以后的数据都是第一页数据的内容

#!/usr/bin/env python
encoding: utf-8
import re
import random
import json
import time
import scrapy
from scrapy import Spider
from scrapy.crawler import CrawlerProcess
from scrapy.http import Request
from scrapy.utils.project import get_project_settings
from newsSpider.items import NewInformationItem_people
from datetime import datetime
from bs4 import BeautifulSoup   # &#x89E3;&#x6790;HTML&#x6587;&#x672C;

class PeopleNewsSpider(Spider):
    name = "people_news_spider"
    people_news_url = "http://www.people.com.cn/"
    start_urls = 'http://search.people.cn/search-platform/front/search'

    def __init__(self, keywords=None, *args, **kwargs):
        super(PeopleNewsSpider, self).__init__(*args, **kwargs)
        self.keywords = keywords
        self.key = ' '

    def start_requests(self):
        keys = self.keywords.split("&#x3001;")
        for self.key in keys:
            time_ns = int(round(time.time() * 1000))
            headers={"Accept": "application/json, text/plain, */*",
                    "Content-Type": "application/json",
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53",
                    "Referer": "http://search.people.cn/s/?keyword="+self.key+"&st=0&_="+str(time_ns)
                    }
            data = {"endTime": "0",
                    "hasContent": "True",
                    "hasTitle": "True",
                    "isFuzzy": "True",
                    "key": self.key,
                    "limit": "10",
                    "page": "1",
                    "sortType": "2",
                    "startTime": "0",
                    "type": "0"}
            temp=json.dumps(data)
            yield scrapy.Request(self.start_urls,method='POST',body=temp,headers=headers,callback=self.people_parse, meta={'time_ns': time_ns})

    def people_parse(self, response):
        data_news = json.loads(response.text)
        print("&#x603B;&#x5171;&#x9875;&#x6570; :", data_news['data']['pages'])
        print("&#x5F53;&#x524D;&#x9875; :", data_news['data']['current'])
        time_ns = response.meta['time_ns']
        key_value = self.key

        page_flag = 1
        while True:
            if page_flag < data_news['data']['pages']:
                page_flag = page_flag + 1
                # 1000&#x9875;&#x4EE5;&#x540E;&#x7684;&#x6570;&#x636E;&#x65E0;&#x6CD5;&#x83B7;&#x53D6;&#xFF0C;1000&#x9875;&#x4EE5;&#x540E;&#x7684;&#x6570;&#x636E;&#x90FD;&#x662F;&#x7B2C;&#x4E00;&#x9875;&#x6570;&#x636E;&#x7684;&#x5185;&#x5BB9;
                if page_flag > 1000:
                    break
                headers={"Accept": "application/json, text/plain, */*",
                        "Content-Type": "application/json",
                        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53",
                        "Referer": "http://search.people.cn/s/?keyword="+key_value+"&st=0&_="+str(time_ns)
                        }
                data = {"endTime": "0",
                        "hasContent": "True",
                        "hasTitle": "True",
                        "isFuzzy": "True",
                        "key": key_value,
                        "limit": "10",
                        "page": page_flag,
                        "sortType": "2",
                        "startTime": "0",
                        "type": "0"}
                temp=json.dumps(data)

                # &#x5728;&#x722C;&#x53D6;&#x591A;&#x5173;&#x952E;&#x8BCD;&#x65F6;&#x9700;&#x8981;&#x5EF6;&#x65F6;&#x4E0B;&#xFF0C;&#x4E0D;&#x5EF6;&#x65F6; &#x4F1A;&#x51FA;&#x73B0;&#x5F88;&#x591A;&#x91CD;&#x590D;&#x6570;&#x636E;&#xFF0C;&#x8FD9;&#x662F;&#x56E0;&#x4E3A;&#x6BCF;&#x4E00;&#x9875;&#x7684;&#x8BF7;&#x6C42;&#x7684;&#x901F;&#x5EA6;&#x8FC7;&#x5FEB;&#xFF0C;&#x800C;item&#x6301;&#x4E45;&#x5316;&#x7684;&#x901F;&#x5EA6;&#x8F83;&#x6162;&#xFF0C;&#x9020;&#x6210;&#x6570;&#x636E;&#x7684;&#x8986;&#x76D6;
                time.sleep(0.1)
                # &#x6BCF;&#x722C;1000&#x6761;&#x6570;&#x636E;&#xFF0C;&#x968F;&#x673A;&#x505C;&#x51E0;&#x79D2;&#xFF08;&#x6F14;&#x793A;&#x7528;&#xFF09;
                if page_flag % 100 == 0:
                    time.sleep(random.randint(1,10))
                yield scrapy.Request(self.start_urls,method='POST',body=temp,headers=headers,callback=self.people_parse_content, meta={'key_value': key_value})
            else:
                break

    def people_parse_content(self, response):
        key_value = response.meta['key_value']
        data_news = json.loads(response.text)
        records = data_news['data']['records']
        for item in records:
            new_information = NewInformationItem_people()   # &#x6BCF;&#x4E00;&#x4E2A;item&#x90FD;&#x662F;&#x4E00;&#x6761;&#x65B0;&#x95FB;&#x7684;&#x4FE1;&#x606F;
            new_information['title'] = re.sub("<em>|</em>|&#xA0;|&quo|&", "", item['title'])                                              # &#xA0;&#x8FD9;&#x4E9B;&#x662F;HTML&#x8F6C;&#x4E49;&#x5B57;&#x7B26;
            new_information['url'] = item['url']
            new_information['author'] = item['author']
            new_information['create_time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(item["displayTime"]/1000))
            new_information['origin'] = "&#x4EBA;&#x6C11;&#x7F51;"
            new_information['point'] = key_value
            new_information['keyword'] = re.sub("<em>|</em>|&#xA0;|&quo|&", "", item['keyword'])
            dt = datetime.now()
            new_information['crawl_time'] = dt.strftime('%Y-%m-%d %H:%M:%S')
            new_information['article'] = BeautifulSoup(item["content"], "html.parser").text
            new_information['category'] = re.sub("#|&#xA0;|&quo|&", "", item["belongsName"])
            yield new_information                           # &#x63D0;&#x4EA4;&#x5230;item&#x901A;&#x9053;&#x8FDB;&#x884C;&#x6301;&#x4E45;&#x5316;

if __name__ == "__main__":
    keywords = '&#x9632;&#x75AB;&#x3001;&#x75AB;&#x60C5;&#x3001;&#x65B0;&#x51A0;'

    process = CrawlerProcess(get_project_settings())
    process.crawl('people_news_spider', keywords=keywords)
    process.start()

    print("peoplenews-search&#x6267;&#x884C;&#x5B8C;&#x6BD5;")

提醒

1 每个网站的搜索的url或者html标签都有可能改变,在进行爬虫设置的时候,一定要根据各位当时的url进行正确的设置

2 F12进入开发者模式,由于各个浏览器,浏览器的版本各不相同,可能与本文说明的有些许差异,各位可以根据实际情况实际操作

3 各个网站都是存在一定的反爬措施的(新华网和人民网的反爬措施均不强),比如:

爬取频率限制

长时间高频率地爬取数据会被服务器就视为爬虫,对其 IP 进行访问限制,因为正常人访问无法做到这么高强度的访问(比如一秒访问十次网站)。如果要避免可以降低爬取的频率

4 两个完整的爬虫可以见如下,其中数据库相关信息需要填写成各自对应的:

jack-nie-23/Scrapy-Spider: 新华网和人民网的简单关键词Scrapy爬虫 (github.com)

Original: https://www.cnblogs.com/jack-nie-23/p/16486878.html
Author: jacknie23
Title: Scrapy关键词 爬虫的简单实现(以新华网和人民网为例)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/582386/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • /dev/random 和 /dev/urandom 的原理

    /dev/random 和 /dev/urandom 是 Linux 上的字符设备文件,它们是随机数生成器,为系统提供随机数 随机数的重要性 随机数在计算中很重要。 TCP/IP …

    Linux 2023年6月13日
    075
  • Linux 0.11源码阅读笔记-文件IO流程

    文件IO流程 用户进程read、write在高速缓冲块上读写数据,高速缓冲块和块设备交换数据。 何时将磁盘块数据读取到缓冲块? [En] when will the disk bl…

    Linux 2023年5月27日
    071
  • ulimit: open files: cannot modify limit: Operation not permitted

    统管理员刚给授权了一台Linux 服务器访问权限,我在JumpServer 登录的时候,遇到下面错误: Last login: Wed Nov 10 13:29:30 2021 f…

    Linux 2023年5月27日
    0109
  • Java基础之接口篇

    Overload和Override的区别?重载Overload:表示同⼀个类中可以有多个名称相同的⽅法,但这些⽅法的参数列表各不相同,参 数个数或类型不同 重写Override:表…

    Linux 2023年6月7日
    081
  • NO.6 HTML+CSS 笔记

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Linux 2023年6月7日
    085
  • 【论文笔记】(2015,JSMA)The Limitations of Deep Learning in Adversarial Settings

    本文是早期的对抗文章,发表于 EuroS&P 2016会议,最主要的工作是:提出了一个生成对抗样本的算法– JSMA(Jacobian Saliency Map…

    Linux 2023年6月7日
    085
  • docker Redis 安裝路徑

    /usr/local/etc posted @2022-01-14 17:35 刘大飞 阅读(20 ) 评论() 编辑 Original: https://www.cnblogs….

    Linux 2023年5月28日
    093
  • Linux netstat:查看网络状态

    netstat 主要用于网络监控,在进程管理方面也很重要。它的输出分为两大部分,分别是网络和系统自己的进程相关性部分。 netstat [-atunlp] -a 列出目前系统上所有…

    Linux 2023年6月13日
    088
  • 小白上手Linux系统安装Tomcat教程

    1.准备阶段: 要有JDK环境,在安装好JDK后再配置tomcat,JDK安装详情在我博客中可以看到。 3.导入 进入到Xshell输入在自己的文件中(cd /home/lzh)好…

    Linux 2023年6月13日
    093
  • 尤娜故事-迷雾-springboot扮酷小技巧

    前情回顾 从前,有一个简单的通道系统叫尤娜…… 尤娜系统的第一次飞行中换引擎的架构垂直拆分改造 四种常用的微服务架构拆分方式 尤娜,我去面试了 正文 我回到…

    Linux 2023年6月14日
    070
  • [云计算]腾讯云从业者认证-思维导图

    第一章 云计算基础介绍 第二章 腾讯云服务器产品介绍 第三章 腾讯云网络产品介绍 第四章 腾讯云CDN加速产品介绍 第五章 腾讯云存储产品介绍 第六章 腾讯云数据库产品介绍 第七章…

    Linux 2023年6月13日
    099
  • Xshell 设置右键粘贴功能

    参考链接:百度经验 活在当下, 从零 出发; posted @2018-04-27 09:38 半天的半天 阅读(266 ) 评论() 编辑 Original: https://w…

    Linux 2023年5月28日
    084
  • 正则表达式

    1.正则表达式分类 正则表达式:REGEXP,REGular EXPression。正则表达式分为两类: Basic REGEXP(基本正则表达式) Extended REGEXP…

    Linux 2023年6月6日
    084
  • Linux编译安装、压缩打包与定时任务服务

    一、编译安装 即使用源代码编译安装的方式,编译打包软件。特点: 可以自定制软件; 可以按需构建软件; 编译安装案例 1、下载源代码包(这里以Nginx软件包源代码为例) wget …

    Linux 2023年5月27日
    080
  • RabbitMQ超详细安装教程(Linux)

    镜像下载、域名解析、时间同步请点击阿里云开源镜像站 1、简介 官网:https://www.rabbitmq.com/ RabbitMQ是一个开源的遵循AMQP协议实现的基于Erl…

    Linux 2023年5月27日
    0160
  • U盘如何安装centos7系统?U盘安装centos7详细安装图解教程

    一般来说,无论是Windows还是linux的IOS系统镜像,我们都可以使用UltraIOS(软碟通)这款软件制作U盘启动工具,不过考虑到不少小白依然不会如何操作,所以今天考虑写一…

    Linux 2023年6月8日
    088
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球