> 技术文档 > scrapy爬虫实战(二): 结合Selenium实现动态加载网页数据采集(详细解说爬取过程以及完整代码)_scrapy selenium

scrapy爬虫实战(二): 结合Selenium实现动态加载网页数据采集(详细解说爬取过程以及完整代码)_scrapy selenium

目录

一、环境准备

二、实战

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 ./financeSpiderscrapy 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_newsADD 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\') andcontains(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 pymysqlfrom itemadapter import ItemAdapterfrom scrapy.exceptions import DropItemclass 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 csvfrom itemadapter import ItemAdapterimport 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=3306MYSQL_USER=\'root\'MYSQL_PASSWORD=\'123456\'MYSQL_DATABASE = \'finance\'MYSQL_CHARSET=\'utf8mb4\'# Configure item pipelines# See https://docs.scrapy.org/en/latest/topics/item-pipeline.htmlITEM_PIPELINES = { # \"financeSpider.pipelines.FinanceSpiderPipeline\": 300, #存储在csv文件 #\"financeSpider.pipelines.FinanceCsvPipeline\": 300, #存储在mysql数据库 \"financeSpider.pipelines.MySQLPipeline\": 300,}#推荐加上的配置参数ROBOTSTXT_OBEY = FalseRETRY_HTTP_CODES = [401, 403, 500, 502, 503, 504]CONCURRENT_REQUESTS = 10

6、爬虫文件(cls_finance) 

import scrapyimport timefrom bs4 import BeautifulSoupfrom financeSpider.items import FinancespiderItem# 爬取财联社的头条新闻,模拟人类点击加载更多 https://www.cls.cn/depth?id=1000 使用selenium + Beautifulsoup技术 scrapy crawl cls_financeclass 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成功爬取数据啦!

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

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