Python爬虫初阶
type
Post
status
Published
summary
之前零散的学过一些爬虫,并且最近自己用的也比较频繁,借此机会整理一篇大体的爬虫框架笔记。一个基本的爬虫通常分为网页下载、网页解析和数据存储三个部分的内容。
slug
python-spider-1
date
Feb 9, 2023
tags
爬虫
蜘蛛
Selenium
Scrapy
category
基础知识
password
icon
URL
Property
Feb 28, 2024 01:07 PM
一个基本的爬虫通常分为数据采集(网页下载)、数据处理(网页解析)和数据存储(将有用的信息持久化)三个部分的内容。
一、数据采集
数据采集主要是向目标网址发起请求并获取到目标网址的数据(这里的数据通常是指网页的源代码,第二步就需要对网页的源代码进行解析以便获取到我们真正想要的数据)。
在使用Python爬虫时,需要模拟发起网络请求,主要用到的库有requests库和python内置的urllib库,现在一般建议使用requests,它是对urllib的再次封装,功能强大并且使用起来也更优雅。
1.1、requests简介
requests是基于 Python 标准库进行了封装,简化了通过 HTTP 或 HTTPS 访问网络资源的操作。通过requests库,我们可以让 Python 程序向浏览器一样向 Web 服务器发起请求,并接收服务器返回的响应,从响应中我们就可以提取出想要的数据。
requests可以发送GET、POST、PUT、DELETE等多种类型的HTTP请求,其中最常用的是 get 和 post 请求。
GET
请求通常用于请求服务器发送某个资源。POST
请求通常用于向服务器提交要被处理的数据,例如,提交表单数据。实例:
import requests response = requests.get('https://api.github.com', params={'key': 'value'}) # 向URL发送GET请求 print(response.text) # 打印响应内容 response = requests.post('https://httpbin.org/post', data={'key': 'value'}) # 向URL发送POST请求 print(response.text) # 打印响应内容
GET 请求与 POST 请求的区别
- 数据传输方式:
GET
请求通过URL传递数据,而POST
请求通常在请求体中传递数据。
- 数据大小限制:由于
GET
请求的数据附加在URL之后,因此受URL长度限制,而POST
请求由于数据在请求体中,理论上没有大小限制。
- 安全性:
POST
请求比GET
请求更安全,因为GET
请求的数据暴露在URL中,容易被拦截。POST
请求的数据则在请求体中,相对更隐私。
- 缓存和历史记录:
GET
请求的URL可以被缓存和保存在浏览器历史记录中,而POST
请求则不会。
1.2、requests.get详解
requests.get(url=url,params=params,headers=headers,timeout=10) # url (必需): 字符串,表示要请求的URL。 # params: 字典或字节序列,用于在请求的查询字符串中添加额外的参数。例如,params={'key1': 'value1', 'key2': 'value2'}会将?key1=value1&key2=value2添加到URL中。 # headers: 字典,表示要发送的HTTP头。这可以用来定制请求头,例如模拟浏览器的请求或设置认证信息等。请求头,从浏览器中获取 # cookies: 字典或CookieJar,用于向服务器发送cookies。如果服务器需要在请求中包含特定的cookie信息,可以通过这个参数提供。 # auth: 元组,用于支持HTTP认证功能。通常是一个(username, password)的元组。 # timeout: 浮点数或元组,设置请求的超时时间,超过时间会抛出异常;。如果指定一个数值,它将应用于连接和读取的超时时间。如果提供一个元组,则第一个元素是连接超时,第二个元素是读取超时。 # allow_redirects: 布尔值,控制是否允许请求自动跟随重定向。默认为True。 # proxies: 字典,用于设置访问URL时使用的代理。格式为{'http': 'http://10.10.1.10:3128', 'https': 'https://10.10.1.10:1080'}。 # verify: 布尔值或字符串。默认为True,意味着SSL证书会被验证。你可以提供一个CA_BUNDLE文件的路径来自定义验证,或者设置为False来禁用验证。 # stream: 布尔值,决定是否立即下载响应内容。默认为False,意味着立即下载全部内容。如果设置为True,则在访问Response.content属性时才开始下载内容。 # cert: 字符串或元组,用于指定SSL客户端证书。可以是单个文件(包含密钥和证书)的路径,或一个包含两个元素的元组(第一个是证书文件的路径,第二个是密钥文件的路径)。
1.3、requests.post详解
requests.post(url=url,json=json,headers=headers,timeout=10) # url (必需): 字符串,表示要请求的URL。 # data: 字典、字节序列或文件对象,用于发送到服务器的表单数据。如果data是字典,requests会默认使用表单编码(application/x-www-form-urlencoded)。 # json: 任意JSON序列化的对象,用于发送JSON格式的数据。如果使用json参数,requests会自动将数据编码为JSON,并设置正确的Content-Type头部。 # headers: 字典,表示要发送的HTTP头。可以用来定制请求头,例如设置内容类型(Content-Type)或认证(Authorization)等。 # cookies: 字典或CookieJar,用于向服务器发送cookies。如果在与服务器交互时需要使用特定的cookie,可以通过这个参数传递。 # files: 字典,用于传输文件。每个键是文件名,每个值可以是文件内容或文件对象。 # auth: 元组,用于支持HTTP认证功能。通常是一个(username, password)的元组。 # timeout: 浮点数或元组,设置请求的超时时间。如果指定一个数值,它将应用于连接和读取的超时时间。如果提供一个元组,则第一个元素是连接超时,第二个元素是读取超时。 # allow_redirects: 布尔值,控制是否允许请求自动跟随重定向。默认为True。 # proxies: 字典,用于设置访问URL时使用的代理。格式为{'http': 'http://10.10.1.10:3128', 'https': 'https://10.10.1.10:1080'}。 # verify: 布尔值或字符串。默认为True,意味着SSL证书会被验证。你可以提供一个CA_BUNDLE文件的路径来自定义验证,或者设置为False来禁用验证。 # stream: 布尔值,决定是否立即下载响应内容。默认为False,意味着立即下载全部内容。如果设置为True,则在访问Response.content属性时才开始下载内容。 # cert: 字符串或元组,用于指定SSL客户端证书。可以是单个文件(包含密钥和证书)的路径,或一个包含两个元素的元组(第一个是证书文件的路径,第二个是密钥文件的路径)。 # params: 字典或字节序列,用于在请求的查询字符串中添加额外的参数。尽管通常在GET请求中使用,但有时也可能在POST请求中添加URL参数。
表单数据仅支持一维的键值对结构,不支持嵌套;而JSON数据支持复杂的数据结构,包括嵌套的对象和数组。适用场景:表单数据适用于简单的文本数据提交和文件上传,常见于HTML表单;JSON数据适用于复杂数据结构的提交。data 参数主要用于发送表单数据(application/x-www-form-urlencoded)或自定义编码的数据(如text/xml)如果data是字典,requests会自动将其编码为表单格式;json 参数主要用于发送JSON格式的数据,requests会自动将Python对象(如字典)序列化为JSON字符串,并自动设置Content-Type头部为application/json
不要同时使用data和json参数,因为json参数会被忽略。如果你需要在请求中同时发送文件和JSON数据,你可以将JSON数据作为一个文件发送,或者将JSON编码为字符串并通过data参数发送。
同时使用files和data参数是常见的,特别是在表单中既有文件上传又有其他数据时。但要注意,如果同时使用files,即使是非文件字段也应该包含在files参数中,以确保整个请求被正确编码为multipart/form-data格式。
1.4、响应对象详解
无论是
GET
请求还是POST
请求,requests
模块都会返回一个Response
对象,该对象包含了服务器的响应数据。Response
对象的主要属性包括(按照使用频率排序):status_code
:几乎每次HTTP请求后都会检查状态码,以确定请求是否成功。
text
:对于文本响应内容(如HTML、JSON、XML等),这是获取响应体最直接的属性。
json()
:当响应内容是JSON格式时,这个方法非常有用,因为它可以直接将JSON响应转换为Python字典。
content
:用于获取二进制响应内容,如下载图片、文件等(如果需要在代码中直接使用,则需要先进行解码)。
content_type = response.headers.get('Content-Type') charset = 'UTF-8' # 默认编码 if 'charset=' in content_type: charset = content_type.split('charset=')[-1] text = response.content.decode(charset) # 使用获取到的编码方式解码 text = response.content.decode(response.encoding) # 或者直接使用response.encoding属性,然后解码 text = response.content.decode() # 使用 python 默认的解码方式解码(通常是 utf-8,取决于 Python 环境的配置)
headers
:响应头包含了诸如内容类型(可以通过打印response.headers
来查看内容类型Content-Type是 text、html、还是 json,来决定后续的解析方式)、服务器信息、缓存控制等重要信息,经常被检查。
cookies
:如果需要处理响应中的Cookies,这个属性会被用到。
url
:有时候需要确认最终请求的URL,特别是在处理重定向时。
history
:如果你需要追踪请求的重定向链,这个属性会很有用。
elapsed
:这个属性可以告诉你请求花费了多长时间,对于性能分析和调试很有帮助。
encoding
:虽然不太常用,但有时候需要检查或修改响应的编码方式。
使用
dir()
函数可以查看Response
对象的所有属性和方法。二、数据解析
以下不同方法的解析案例都以【如何从豆瓣电影获取排前250名的电影的名称】为例;
豆瓣电影TOP250每页共展示了25部电影,如果要获取全部数据,我们共需要访问10个页面,对应的地址是
https://movie.douban.com/top250?start=xxx
,这里的xxx如果为0就是第一页,如果xxx的值是100,那么我们可以访问到第五页。2.1、正则解析
import random import re import time import requests for page in range(1, 11): resp = requests.get( url=f'https://movie.douban.com/top250?start={(page - 1) * 25}', # 如果不设置HTTP请求头中的User-Agent,豆瓣会检测出不是浏览器而阻止我们的请求。 # 通过get函数的headers参数设置User-Agent的值,具体的值可以在浏览器的开发者工具查看到。 # 用爬虫访问大部分网站时,将爬虫伪装成来自浏览器的请求都是非常重要的一步。 headers={'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36'} ) # 通过正则表达式获取class属性为title且标签体不以&开头的span标签并用捕获组提取标签内容 pattern1 = re.compile(r'<span class="title">([^&]*?)</span>') titles = pattern1.findall(resp.text) # 通过正则表达式获取class属性为rating_num的span标签并用捕获组提取标签内容 pattern2 = re.compile(r'<span class="rating_num".*?>(.*?)</span>') ranks = pattern2.findall(resp.text) # 使用zip压缩两个列表,循环遍历所有的电影标题和评分 for title, rank in zip(titles, ranks): print(title, rank) # 随机休眠1-5秒,避免爬取页面过于频繁 time.sleep(random.random() * 4 + 1)
2.2、XPath 解析
XPath 是在 XML(eXtensible Markup Language)文档中查找信息的一种语法,XPath 使用路径表达式来选取 XML 文档中的节点或者节点集,这里所说的节点包括元素、属性、文本、命名空间、处理指令、注释、根节点等。
实现 XPath 解析需要三方库
lxml
的支持,可以使用下面的命令安装lxml
。pip install lxml
用 XPath 解析方式改写之前获取豆瓣电影 Top250的代码:
from lxml import etree import requests for page in range(1, 11): resp = requests.get( url=f'https://movie.douban.com/top250?start={(page - 1) * 25}', headers={'User-Agent': 'BaiduSpider'} ) tree = etree.HTML(resp.text) # 通过XPath语法从页面中提取电影标题 title_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li/div/div[2]/div[1]/a/span[1]') # 通过XPath语法从页面中提取电影评分 rank_spans = tree.xpath('//*[@id="content"]/div/div[1]/ol/li[1]/div/div[2]/div[2]/div/span[2]') for title_span, rank_span in zip(title_spans, rank_spans): print(title_span.text, rank_span.text)
XPath解析原理:
1、实例话entree对象,将需要解析的页面源码数据加载到对象中
- 将本地的html文件中源码数据加载到etree中
tree = etree.parse(filePath)
- 将互联网中获取的带源码数据加载到etrre中
tree = etree.HTML('page_text')
2、调用etree对象中的XPath方法结合XPath表达式实现内容定位和捕获
tree.xpath('xpath表达式') # 返回的是列表
XPath表达式详解:
/:表示从根节点开始定位,表示一个层级;实例:tree.xpath('/html/body/div') //://在中间,表示多个层级(两个或两个以上);实例:tree.xpath('/html//div') //://在开头,表示从任意位置开始定位;实例tree.xpath('//div') [@]:属性定位;实例:tree.xpath("//div[@class='song']");tree.xpath("//tag[@attrName='attrValue']") [n]:索引定位:实例:tree.xpath("//div[@class='song']/p[3]") 索引是从1开始的 /text():获取直系文本;实例:tree.xpath('//div[class="tang"]//li[5]/a/text()')[0] //text():获取非直系文本(标签下所有文本);实例:tree.xpath('//li[7]//text()') /@attrName:获取属性;实例:tree.xpath('//div[class="song"]/img/@src')
常见的XPath语法如下:
XPath具体教程详见:菜鸟教程——XPath教程
2.3、CSS 选择器解析
浏览器中运行的 JavaScript 本身就可以document对象的querySelector()和querySelectorAll()方法基于 CSS 选择器获取页面元素。在 Python 中,我们可以利用三方库beautifulsoup4或pyquery来做同样的事情。Beautiful Soup 可以用来解析 HTML 和 XML 文档,修复含有未闭合标签等错误的文档,通过为待解析的页面在内存中创建一棵树结构,实现对从页面中提取数据操作的封装。
安装Beautiful Soup:
pip install beautifulsoup4
使用
bs4
改写的获取豆瓣电影Top250电影名称的代码:import bs4 import requests for page in range(1, 11): resp = requests.get( url=f'https://movie.douban.com/top250?start={(page - 1) * 25}', headers={'User-Agent': 'BaiduSpider'} ) # 创建BeautifulSoup对象 soup = bs4.BeautifulSoup(resp.text, 'lxml') # 通过CSS选择器从页面中提取包含电影标题的span标签 title_spans = soup.select('div.info > div.hd > a > span:nth-child(1)') # 通过CSS选择器从页面中提取包含电影评分的span标签 rank_spans = soup.select('div.info > div.bd > div > span.rating_num') for title_span, rank_span in zip(title_spans, rank_spans): print(title_span.text, rank_span.text)
bs4具体教程详见:官方文档
2.4、简单的总结
三、数据存储
数据存储根据实际的需求有很多方式,文本数据直接存储到本地(txt)或结合Excel、数据库等工具的使用;二进制文件如压缩文件、图片等需要读写到本地。
二进制资源以百度首页的logo图片为例
import requests resp = requests.get('https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png') with open('baidu.png', 'wb') as file: file.write(resp.content)
四、重要的其他内容
4.1、用Selenium抓取网页动态内容
4.1.1、Selenium简介
根据权威机构发布的全球互联网可访问性审计报告,全球约有四分之三的网站其内容或部分内容是通过JavaScript动态生成的,这就意味着在浏览器窗口中“查看网页源代码”时无法在HTML代码中找到这些内容,也就是说我们之前用的抓取数据的方式无法正常运转了。解决这样的问题基本上有两种方案,一是获取提供动态内容的数据接口,这种方式也适用于抓取手机 App 的数据;另一种是通过自动化测试工具 Selenium 运行浏览器获取渲染后的动态内容。对于第一种方案,我们可以使用浏览器的“开发者工具”或者更为专业的抓包工具(如:Charles、Fiddler、Wireshark等)来获取到数据接口,后续的操作同上;第二种方案主要使用自动化测试工具 Selenium 来获取网站的动态内容。
Selenium 是一个自动化测试工具,利用它可以驱动浏览器执行特定的行为,最终帮助爬虫开发者获取到网页的动态内容。简单的说,只要我们在浏览器窗口中能够看到的内容,都可以使用 Selenium 获取到,对于那些使用了 JavaScript 动态渲染技术的网站,Selenium 会是一个重要的选择。下面,我们还是以 Chrome 浏览器为例,来讲解 Selenium 的用法,大家需要先安装 Chrome 浏览器并下载它的驱动。Chrome 浏览器的驱动程序可以在ChromeDriver官网进行下载,驱动的版本要跟浏览器的版本对应,如果没有完全对应的版本,就选择版本代号最为接近的版本。
4.1.2、Selenium使用
安装 Selenium
pip install selenium
加载页面:通过下面的代码驱动 Chrome 浏览器打开百度
# ----------旧版版的selenium--------- from selenium import webdriver # 创建Chrome浏览器对象 browser = webdriver.Chrome(executable_path='/***/chromedriver',) # executable_path驱动文件的位置 # 加载指定的页面 browser.get('https://www.baidu.com/') browser.quit() # 关闭浏览器 # ----------4.0 之后版的selenium--------- from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service(executable_path='chromedriver-mac-arm64/chromedriver') # 创建ChromeDriver服务 browser = webdriver.Chrome(service=service) # 创建Chrome浏览器对象 browser.get('https://www.google.com/search') # 加载指定的页面 browser.quit() # 关闭浏览器
查找元素和模拟用户行为
接下来,我们可以尝试模拟用户在百度首页的文本框输入搜索关键字并点击“百度一下”按钮。在完成页面加载后,可以通过Chrome对象的find_element和find_elements方法来获取页面元素,Selenium 支持多种获取元素的方式,包括:CSS 选择器、XPath、元素名字(标签名)、元素 ID、类名等,前者可以获取单个页面元素(WebElement对象),后者可以获取多个页面元素构成的列表。获取到WebElement对象以后,可以通过send_keys来模拟用户输入行为,可以通过click来模拟用户点击操作,代码如下所示。
from selenium import webdriver from selenium.webdriver.common.by import By browser = webdriver.Chrome() browser.get('https://www.baidu.com/') # 通过元素ID获取元素 kw_input = browser.find_element(By.ID, 'kw') # 模拟用户输入行为 kw_input.send_keys('Python') # 通过CSS选择器获取元素 su_button = browser.find_element(By.CSS_SELECTOR, '#su') # 模拟用户点击行为 su_button.click()
无头模式(无可视化界面,浏览器后台运行)
如果使用操作链保存图片,则无法使用无头模式(操作链的输入和点击是需要在浏览器界面完成的,不限时浏览器界面,相应的键盘输入会输入到当前屏幕聚焦的应用中)
from selenium.webdriver.chrome.options import Options chrome_options = Options() chrome_options.add_argument('--headless') # 添加无头参数 bro = webdriver.Chrome(options=chrome_options)
修改浏览器默认参数和防范被识别风险
设置的路径需要输入绝对路径
# 设置浏览器路径 filepath_options = webdriver.ChromeOptions() # 浏览器首选项 prefs = { 'profile.default_content_settings.popups': 0, # 设置为 0 禁止弹出窗口 'excludeSwitches': ['enable-automation'], # 用来规避门户网站识别selenium请求 'savefile.default_directory': f'/***/{img_folder}', # 设置保存路径 'download.default_directory': f'/***/{img_folder}' # 设置下载路径 } filepath_options.add_experimental_option('prefs', prefs) # 初始化浏览器对象,传入浏览器驱动 # service = bro = webdriver.Chrome(executable_path='/***/chromedriver', chrome_options=filepath_options)
4.1.3、常用的属性和方法
以下是浏览器对象和WebElement对象常用的属性和方法。具体的内容大家还可以参考 Selenium 官方文档的中文翻译。
4.1.4、360图片下载实例
下面的例子演示了如何使用 Selenium 从“360图片”网站搜索和下载图片。
import os import time from concurrent.futures import ThreadPoolExecutor import requests from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys DOWNLOAD_PATH = 'images/' def download_picture(picture_url: str): """ 下载保存图片 :param picture_url: 图片的URL """ filename = picture_url[picture_url.rfind('/') + 1:] resp = requests.get(picture_url) with open(os.path.join(DOWNLOAD_PATH, filename), 'wb') as file: file.write(resp.content) if not os.path.exists(DOWNLOAD_PATH): os.makedirs(DOWNLOAD_PATH) browser = webdriver.Chrome() browser.get('https://image.so.com/z?ch=beauty') browser.implicitly_wait(10) kw_input = browser.find_element(By.CSS_SELECTOR, 'input[name=q]') kw_input.send_keys('苍老师') kw_input.send_keys(Keys.ENTER) for _ in range(10): browser.execute_script( 'document.documentElement.scrollTop = document.documentElement.scrollHeight' ) time.sleep(1) imgs = browser.find_elements(By.CSS_SELECTOR, 'div.waterfall img') with ThreadPoolExecutor(max_workers=32) as pool: for img in imgs: pic_url = img.get_attribute('src') pool.submit(download_picture, pic_url)
4.1.5、实操案例:使用操作链保存图片
由于本案例的门户有反爬措施,使用requests向图片发起请求无法获取到数据,所以在浏览器里面使用自动化操作链来下载内容。
本案例使用截止目前为止最新版本的谷歌浏览器110.0.5481.77;网上常见的使用【右键图片——输入v下载图片】模式以失效,本案例对其进行了小改动,并成功下载图片。
import warnings import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.action_chains import ActionChains import pyautogui # 操作鼠标键盘 surs = [] for n, url in enumerate(surs): """ 先使用requests模块拿到链接和标题,根据标题创建文件夹,然后再把创建后的文件夹路径设置为浏览器的默认路径,大功告成! """ img_folder = pag_num + '——' + pag_title """ 拿到下载路径之后,开始请求下载 """ # warnings.simplefilter('ignore',ResourceWarning) # 去除开头警告 # 设置浏览器路径 filepath_options = webdriver.ChromeOptions() # 浏览器首选项 prefs = { 'profile.default_content_settings.popups': 0, # 设置为 0 禁止弹出窗口 'excludeSwitches': ['enable-automation'], # 用来规避门户网站识别selenium请求 'savefile.default_directory': f'/Users/ayd/Downloads/其他/20230204/spider/photos18/{img_folder}', # 设置保存路径 'download.default_directory': f'/Users/ayd/Downloads/其他/20230204/spider/photos18/{img_folder}' # 设置下载路径 } filepath_options.add_experimental_option('prefs', prefs) # 初始化浏览器对象,传入浏览器驱动 # service = bro = webdriver.Chrome(executable_path='/Users/ayd/package/Chrome插件/chromedriver_mac_arm64(selenium驱动)/chromedriver', chrome_options=filepath_options) # # 让浏览器发起请求 bro.get(url=url) pag_text = bro.page_source # 获取网站源码数据 # print('这里是pag_text:',pag_text) # 下载图片 for i,img_url in enumerate(img_list): # 通过执行js代码,将图片url在新的标签页中打开 js = f"window.open(\'{img_url}\')" bro.execute_script(js) # 切换窗口:bro.window_handles获取所有窗口组成的列表;bro.switch_to.window切换窗口 bro.switch_to.window(bro.window_handles[-1]) time.sleep(1) # 等待图片加载1s # 保存图片系列操作 pic = bro.find_element(By.XPATH, '/html/body/img') # 获取元素 action = ActionChains(bro).move_to_element(pic) # 移动到图片 action.context_click(pic).perform() # 右击并执行 pyautogui.typewrite(['s','down','enter']) # 操作键盘输入s(这里输入什么都行,只是为了选中右键出来的菜单),然后输入方向键选择“图片另存为”,最后输入回车确认 time.sleep(1) # 等待保存窗口弹出 pyautogui.typewrite(['enter']) # 回车确认 # time.sleep(1) bro.close() # 关闭标签页 bro.switch_to.window(bro.window_handles[-1]) # 切换回初始标签页,不然会报错 # time.sleep(1) print(f'{img_folder}——{n+1}/{len(surs)}完成进度:{i+1}/{len(img_list)}') bro.quit() # 完成一个页面之后退出浏览器 print(f'{img_folder}的{len_original}个文件已完成,整体完成进度:{n+1}/{len(surs)}') print('所有页已完成!')
4.2、Scrapy框架
当你写了很多个爬虫程序之后,你会发现每次写爬虫程序时,都需要将页面获取、页面解析、爬虫调度、异常处理、反爬应对这些代码从头至尾实现一遍,这里面有很多工作其实都是简单乏味的重复劳动。那么,有没有什么办法可以提升我们编写爬虫代码的效率呢?答案是肯定的,那就是利用爬虫框架,而在所有的爬虫框架中,Scrapy 应该是最流行、最强大的框架。Scrapy 是基于 Python 的一个非常流行的网络爬虫框架,可以用来抓取 Web 站点并从页面中提取结构化的数据。
4.2.1、Scrapy的组件
我们先来说说 Scrapy 中的组件。
- Scrapy 引擎(Engine):用来控制整个系统的数据处理流程。
- 调度器(Scheduler):调度器从引擎接受请求并排序列入队列,并在引擎发出请求后返还给它们。
- 下载器(Downloader):下载器的主要职责是抓取网页并将网页内容返还给蜘蛛(Spiders)。
- 蜘蛛程序(Spiders):蜘蛛是用户自定义的用来解析网页并抓取特定URL的类,每个蜘蛛都能处理一个域名或一组域名,简单的说就是用来定义特定网站的抓取和解析规则的模块。
- 数据管道(Item Pipeline):管道的主要责任是负责处理有蜘蛛从网页中抽取的数据条目,它的主要任务是清理、验证和存储数据。当页面被蜘蛛解析后,将被发送到数据管道,并经过几个特定的次序处理数据。每个数据管道组件都是一个 Python 类,它们获取了数据条目并执行对数据条目进行处理的方法,同时还需要确定是否需要在数据管道中继续执行下一步或是直接丢弃掉不处理。数据管道通常执行的任务有:清理 HTML 数据、验证解析到的数据(检查条目是否包含必要的字段)、检查是不是重复数据(如果重复就丢弃)、将解析到的数据存储到数据库(关系型数据库或 NoSQL 数据库)中。
- 中间件(Middlewares):中间件是介于引擎和其他组件之间的一个钩子框架,主要是为了提供自定义的代码来拓展 Scrapy 的功能,包括下载器中间件和蜘蛛中间件。
4.2.2、数据处理流程
Scrapy 的整个数据处理流程由引擎进行控制,通常的运转流程包括以下的步骤:
- 引擎询问蜘蛛需要处理哪个网站,并让蜘蛛将第一个需要处理的 URL 交给它。
- 引擎让调度器将需要处理的 URL 放在队列中。
- 引擎从调度那获取接下来进行爬取的页面。
- 调度将下一个爬取的 URL 返回给引擎,引擎将它通过下载中间件发送到下载器。
- 当网页被下载器下载完成以后,响应内容通过下载中间件被发送到引擎;如果下载失败了,引擎会通知调度器记录这个 URL,待会再重新下载。
- 引擎收到下载器的响应并将它通过蜘蛛中间件发送到蜘蛛进行处理。
- 蜘蛛处理响应并返回爬取到的数据条目,此外还要将需要跟进的新的 URL 发送给引擎。
- 引擎将抓取到的数据条目送入数据管道,把新的 URL 发送给调度器放入队列中。
上述操作中的第2步到第8步会一直重复直到调度器中没有需要请求的 URL,爬虫就停止工作。
4.2.3、安装和使用
pip install scrapy
接下来用scrapy实现一个爬取豆瓣电影 Top250 电影标题、评分和金句的爬虫
1、创建项目和文件
在命令行中使用
scrapy
命令创建名为demo
的项目scrapy startproject demo
项目的目录结构如下
demo |____ demo |________ spiders |____________ __init__.py |________ __init__.py |________ items.py |________ middlewares.py |________ pipelines.py |________ settings.py |____ scrapy.cfg
切换到
demo
目录,用下面的命令创建名为douban
的蜘蛛程序,movie.douban.com
是指定爬取范围。scrapy genspider douban movie.douban.com
创建好文件之后默认增加一下代码:
import scrapy class DoubanSpider(scrapy.Spider): name = 'douban' allowed_domains = ['movie.douban.com'] start_urls = ( 'http://movie.douban.com/', ) def parse(self, response): pass # name = "" : 这个爬虫的识别名称,必须是唯一的,在不同的爬虫必须定义不同的名字。 # allow_domains = []: 是搜索的域名范围,也就是爬虫的约束区域,规定爬虫只爬取这个域名下的网页,不存在的URL会被忽略。 # start_urls = (): 爬取的URL元祖/列表。爬虫从这里开始抓取数据,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。 # parse(self, response): 解析的方法,每个初始URL完成下载后将被调用,调用的时候传入从每一个URL传回的Response对象来作为唯一参数, # 主要作用如下:负责解析返回的网页数据(response.body),提取结构化数据(生成item)生成需要下一页的URL请求。
2、明确目标数据
在
items.py
的Item
类中定义字段,这些字段用来保存数据import scrapy class DoubanItem(scrapy.Item): title = scrapy.Field() score = scrapy.Field() motto = scrapy.Field()
3、制作爬虫
修改
spiders
文件夹中名为douban.py
的文件,它是蜘蛛程序的核心,需要我们添加解析页面的代码import scrapy from scrapy import Selector, Request from scrapy.http import HtmlResponse from demo.items import MovieItem class DoubanSpider(scrapy.Spider): name = 'douban' allowed_domains = ['movie.douban.com'] start_urls = ['https://movie.douban.com/top250?start=0&filter='] # 将start_urls的值修改为需要爬取的第一个url # 解析数据函数 def parse(self, response: HtmlResponse): sel = Selector(response) # 也可以使用 XPath 或正则表达式进行页面解析 movie_items = sel.css('#content > div > div.article > ol > li') for movie_sel in movie_items: item = MovieItem() item['title'] = movie_sel.css('.title::text').extract_first() item['score'] = movie_sel.css('.rating_num::text').extract_first() item['motto'] = movie_sel.css('.inq::text').extract_first() yield item hrefs = sel.css('#content > div > div.article > div.paginator > a::attr("href")') # 获取翻页按钮对应的a标签的href属性 for href in hrefs: full_url = response.urljoin(href.extract()) # 将href链接拼接到不带参数的start_urls上 yield Request(url=full_url) # 修改下次请求的Request的URL属性为拼接后的URL # 这里比较难懂,主要是要理解yield的运行规则和parse函数是自动调用的,并且url拼接是要去掉初始URLstart_urls的参数再拼接 # 推荐阅读 # [从原理到实战,一份详实的 Scrapy 爬虫教程-腾讯云](https://cloud.tencent.com/developer/article/1856557) # [如何理解Python中的yield用法? - 知乎](https://zhuanlan.zhihu.com/p/268605982)
执行爬虫
scrapy crawl douban # 这里的文件名要和name 属性对应
4、数据持久化
5、配置设置
修改
settings.py
文件对项目进行配置# 用户浏览器 USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36' # 并发请求数量 CONCURRENT_REQUESTS = 4 # 下载延迟 DOWNLOAD_DELAY = 3 # 随机化下载延迟 RANDOMIZE_DOWNLOAD_DELAY = True # 是否遵守爬虫协议 ROBOTSTXT_OBEY = True # 配置数据管道 ITEM_PIPELINES = { 'demo.pipelines.MovieItemPipeline': 300, }