目录

一、环境准备

二、实战

1、项目介绍

2、创建项目

新建项目

定义Item(items) 

mysql建表

3、selenium模拟用户点击网页并加载更多(middlewares)

4、管道文件(Pipelines)

mysql存储

CSV存储

5、配置文件(settings)

6、爬虫文件(cls_finance) 

7、运行测试


一、环境准备

1、python 

python3以上,你可以直接下个anaconda,参考之前我写的博客

Anaconda安装+scrapy部署及初步认识-CSDN博客

2、mysql

3、scrapy

参考:Anaconda安装+scrapy部署及初步认识-CSDN博客

4、selenium+chromdriver

因为我的浏览器是chrom,所以用的chromdiver

参考:scrapy爬虫实战:爬取财经网站新闻数据(动态渲染页面)---详细图文解说-CSDN博客

二、实战

1、项目介绍

本项目要爬取的数据是财联社的头条新闻数据,模拟用户点击加载更多按钮,获取完整的网页源码,并把数据存储在mysql数据库。

财联社是主流的财经新闻媒体,专注于中国证券市场动态的分析、报道。

财联社深度:重大政策事件及时分析解读_供给侧改革

然后按F12分析发现本网页呈现的逻辑:列表数据是通过Ajax加载的。

数据通过 AJAX 加载 是指网页的内容(如列表、表格、动态更新的信息)不是直接写在初始 HTML 代码中,而是通过 JavaScript 异步请求(AJAX) 从服务器获取数据,再动态插入到页面中的技术。

要想爬取全部头条数据得使用selenium模拟人工点击到加载完成才能获取。

2、创建项目

新建项目

 首先新建一个Scrapy项目,名字叫做financeSpider,创建命令如下(在base环境中执行):

(看过上个博客已经创建过项目的不需要再创建)

scrapy startproject financeSpider

接下来进入项目,然后新建一个Spider,名称cls_finance,命令如下:

cd ./financeSpider
scrapy genspider cls_finance https://www.cls.cn/

定义Item(items) 

 定义需要爬取的字段,在items.py里面定义一个FinancespiderItem,代码如下(比上个博客的数据结构多了个news_source字段,记录文章来源的)

class FinancespiderItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    source=scrapy.Field()
    source_url= scrapy.Field()
    title = scrapy.Field()
    link = scrapy.Field()
    content = scrapy.Field()
    news_source = scrapy.Field()
    update_time = scrapy.Field()

mysql建表

建表并给url列设为唯一,ddl命令如下

CREATE TABLE if not exists finance_news (
    id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,  -- ✅ 自增ID(无符号整数)
    url VARCHAR(255) NOT NULL UNIQUE COMMENT '新闻url',            -- ✅ 唯一URL(
    title VARCHAR(255) COMMENT '新闻标题',
    source VARCHAR(255) COMMENT '新闻来源网站',
    source_url VARCHAR(255) COMMENT '来源url',
    content TEXT COMMENT '新闻内容',
    news_source VARCHAR(255) COMMENT '新闻来源',
    update_time VARCHAR(255) COMMENT '新闻更新时间',
    dt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '数据写入时间',  -- ✅ 时间戳(自动记录时间)

    -- 可选:添加普通索引(根据查询需求)
    INDEX idx_dt (dt)  -- 如果经常按时间查询可加索引

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
# 加唯一索引
ALTER TABLE finance_news
ADD UNIQUE INDEX idx_unique_url (url);

3、selenium模拟用户点击网页并加载更多(middlewares)

在middlewares文件中,新建selenium下载中间件(middlewares),完整代码如下

class SeleniumMiddleware:


    def process_request(self, request, spider):

        # 检查是否需要使用Selenium,默认关闭
        if not request.meta.get('use_selenium', False):
            return None  # 继续正常处理

        # 配置浏览器参数
        chrome_options = Options()
        # chrome_options.add_argument("--ignore-certificate-errors")  # 忽略 SSL 证书错误
        # chrome_options.add_argument('--ignore-ssl-errors')
        chrome_options.add_argument('--headless')  # 无头模式
        driver = webdriver.Chrome(options=chrome_options)

        try:
             driver.get(request.url)
             click_attempts = 0  # 点击尝试计数器
             max_clicks = 10  # 最大点击次数(防死循环)
             while click_attempts < max_clicks:

                try:

                    # 等待“加载更多”按钮出现
                    wait = WebDriverWait(driver, 10)
                    load_more_button = wait.until(EC.presence_of_element_located(
                        (By.XPATH, "//div[contains(@class, 'list-more-button') and 
                     contains(text(), '加载更多')]")))
                    # 点击“加载更多”按钮
                    # 做个有点击按钮的判断,没有就退出循环
                    if load_more_button:
                        load_more_button.click()
                    else:
                        print ("没有找到加载更多按钮")
                        break

                    # 强制等待动态内容加载(根据网络情况调整)
                    time.sleep(3)
                    click_attempts += 1

                except (NoSuchElementException, TimeoutException):
                    print("已经加载到最后")
                    break

            # 获取完整渲染后的页面源码
             body = driver.page_source
             return HtmlResponse(url=driver.current_url, body=body, encoding='utf-8')
        finally:
            driver.quit()

代码重点解析:

  • 模拟点击加载更多按钮,因为要点击多次设置了while循环,以防发生死循环所以做了个最大值限制

  • selenium耗内存,下载效率慢,所以在request设置了开启和关闭标签,

  • 一定要记得及时关闭浏览器,释放内存

 

4、管道文件(Pipelines)

mysql存储

把数据写入mysql数据库(同上一篇博客)

import pymysql
from itemadapter import ItemAdapter
from scrapy.exceptions import DropItem

class MySQLPipeline:
    def __init__(self, host, port, user, password, db, charset):
        self.host = host
        self.port = port
        self.user = user
        self.password = password
        self.db = db
        self.charset = charset
        self.connection = None
        self.cursor = None

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            host=crawler.settings.get('MYSQL_HOST'),
            port=crawler.settings.get('MYSQL_PORT'),
            user=crawler.settings.get('MYSQL_USER'),
            password=crawler.settings.get('MYSQL_PASSWORD'),
            db=crawler.settings.get('MYSQL_DATABASE'),
            charset=crawler.settings.get('MYSQL_CHARSET')
        )

    def open_spider(self, spider):
        """连接数据库"""
        self.connection = pymysql.connect(
            host=self.host,
            port=self.port,
            user=self.user,
            password=self.password,
            db=self.db,
            charset=self.charset,
            cursorclass=pymysql.cursors.DictCursor
        )
        self.cursor = self.connection.cursor()

    def close_spider(self, spider):
        """关闭连接"""
        self.connection.close()

    def process_item(self, item, spider):
        """处理Item"""
        try:
            # 构建SQL语句(根据你的表结构修改)
            sql = """
                INSERT INTO finance_news(
                    url,
                    title,
                    source,
                    source_url,
                    content,
                    news_source,
                    update_time
                ) VALUES ( %s, %s, %s, %s, %s , %s, %s )
                ON DUPLICATE KEY UPDATE
                title = VALUES(title),
                source = VALUES(source),
                source_url = VALUES(source_url),
                content = VALUES(content),
                news_source = VALUES(news_source),
                update_time = VALUES(update_time);"""
            params = (item["link"],item["title"],item["source"],item["source_url"],item["content"],item["news_source"],item["update_time"])
            # 从item提取数据(字段名需要对应)
            self.cursor.execute(sql,params)
            self.connection.commit()
        except Exception as e:
            self.connection.rollback()
            raise DropItem(f"Error saving item to MySQL: {str(e)}")
        return item

CSV存储

没有mysql的可以用本地文件存储数据

import csv
from itemadapter import ItemAdapter
import datetime
# 获取当前日期
today = datetime.date.today()

class FinanceCsvPipeline(object):
    def process_item(self, item, spider):
        with open(f"finance_news_{today}.csv", "a+", encoding="utf-8") as f:
            w = csv.writer(f)
            row=item["link"],item["title"],item["source"],item["source_url"],item["content"],item["news_source"],item["update_time"]
            w.writerow(row)
        return item

做完pipelines文件修改后,一定要记得修改配置文件(settings)的ITEM_PIPELINES

5、配置文件(settings)

*重点修改

释放DOWNLOADER_MIDDLEWARES

DOWNLOADER_MIDDLEWARES = {
   "financeSpider.middlewares.FinancespiderSpiderMiddleware": 543,#原来的
   "financeSpider.middlewares.SeleniumMiddleware": 600          #新建的selenium中间件

}

#根据自己存储的具体情况修改
ITEM_PIPELINES = {
    # "financeSpider.pipelines.FinanceSpiderPipeline": 300,
    #存储在本地文件csv
    #"financeSpider.pipelines.FinanceCsvPipeline": 300,
    #存储在mysql数据库
    "financeSpider.pipelines.MySQLPipeline": 300,

}

数字代码级别,级别越低优先级越高

完整代码如下(其他配置同上一篇博客):

BOT_NAME = "financeSpider"

SPIDER_MODULES = ["financeSpider.spiders"]
NEWSPIDER_MODULE = "financeSpider.spiders"

#修改请求头,可以弄得完整点,这个是全局配置,spider请求的时候不需要加
# Override the default request headers:
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
    'accept-encoding': 'gzip, deflate, br, zstd',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

#下载中间件
DOWNLOADER_MIDDLEWARES = {
   "financeSpider.middlewares.FinancespiderSpiderMiddleware": 543,
   "financeSpider.middlewares.SeleniumMiddleware": 600          

}



#mysql的配置参数根据自己的mysql地址修改
# mysql SETTING========
MYSQL_HOST='192.168.X.XX'
MYSQL_PORT=3306
MYSQL_USER='root'
MYSQL_PASSWORD='123456'
MYSQL_DATABASE = 'finance'
MYSQL_CHARSET='utf8mb4'

# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    # "financeSpider.pipelines.FinanceSpiderPipeline": 300,
    #存储在csv文件
    #"financeSpider.pipelines.FinanceCsvPipeline": 300,
    #存储在mysql数据库
    "financeSpider.pipelines.MySQLPipeline": 300,

}

#推荐加上的配置参数
ROBOTSTXT_OBEY = False
RETRY_HTTP_CODES = [401, 403, 500, 502, 503, 504]
CONCURRENT_REQUESTS = 10

6、爬虫文件(cls_finance) 

import scrapy
import time
from bs4 import BeautifulSoup
from financeSpider.items import FinancespiderItem

# 爬取财联社的头条新闻,模拟人类点击加载更多 https://www.cls.cn/depth?id=1000  使用selenium + Beautifulsoup技术  scrapy crawl cls_finance

class ClsFinanceSpider(scrapy.Spider):
    name = "cls_finance"
    allowed_domains = ["cls.cn"]
    start_urls = ["https://www.cls.cn/"]

    #因为要使用selenium重写请求首页
    def start_requests(self):
        url="https://www.cls.cn/depth?id=1000"
        yield scrapy.Request(url=url,meta={"use_selenium":True},callback=self.parse) # 标记需要处理 只有需要selenium的时候标记,默认不用

    def parse(self, response):

        soup = BeautifulSoup(response.text, 'lxml')
        # print (soup)
        # 用css模糊匹配
        items = soup.select('div[class*="subject-interest-list"]')
        # print (items)
        for t in items:
            item = FinancespiderItem()
            item['source'] = '财联社'
            item['source_url'] = response.url
            item['title'] = t.select_one('div[class*="subject-interest-title"]').text.replace('原创', '').strip()
            item['link'] = "https://www.cls.cn" + t.a['href']
            # print(item['link'] )
            # print (item['title'])
            time.sleep(1)
            yield scrapy.Request(url=item['link'], meta={"item": item}, callback=self.parse1)

    def parse1(self, response):
        item = response.meta['item']
        soup = BeautifulSoup(response.text, 'lxml')
        print("正在抓取内容")

        # 声明变量并赋值,这样能在解析异常的时候还能正常入表,以防数据丢失
        content = "none"
        dt = "none"
        source = "none"
        # 文本解析异常处理
        try:
            content = soup.select_one('div[class*="detail-content"]').text.strip()
            dt = soup.select_one('div[class="f-l m-r-10"]').text.strip()[0:16]
            source = soup.select_one('div[class="f-l"]').text.strip()

        except AttributeError as e:
            print (f"{e}:文本解析报错")

        item["content"] = content
        item['update_time']=dt
        item['news_source'] =source

        #
        # print  (item["content"] +"\t"+item['update_time'])
        yield item

7、运行测试

在base环境,写入命令

scrapy crawl cls_finance


运行结果如下:

最后运行完的结果 : 

101条新闻全部爬取成功。

数据结果如下:

我们已经可以用scrapy+selenium成功爬取数据啦!

………………………………………………………………………………………………………………

大家有什么优化或者建议欢迎评论区告诉我,我们一起共同进步,谢谢!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐