网页抓取是自动从互联网中提取特定数据的过程。它有许多用例,例如为机器学习项目获取数据、创建价格比较工具或任何其他需要大量数据的创新想法。虽然理论上您可以手动进行数据提取,但互联网的大量内容使这种方法在许多情况下不切实际。因此,知道如何构建网络爬虫可以派上用场。这篇文章的目的是教你如何用 Python 创建一个网页爬虫。您将学习如何检查网站以准备抓取、使用 BeautifulSoup 提取特定数据、使用 Selenium 等待 JavaScript 渲染,以及将所有内容保存在新的 JSON 或 CSV 文件中。
但首先,我应该警告您网络抓取的合法性。虽然抓取行为是合法的,但您可能提取的数据使用可能是非法的。确保你没有爬取:
- 受版权保护的内容 – 由于它是某人的知识产权,因此受法律保护,您不能只是重复使用它。
- 个人数据——如果您收集的信息可用于识别个人身份,则它被视为个人数据,对于欧盟公民而言,它受 GDPR 保护。除非您有合法的理由来存储这些数据,否则最好完全跳过它。
一般来说,在抓取之前,您应该始终阅读网站的条款和条件,以确保您不会违反他们的政策。如果您不确定如何继续,请联系网站所有者并征求同意。
您的Scraper需要什么?
要开始构建您自己的网络爬虫,您首先需要在您的机器上安装Python。Ubuntu 20.04 和其他版本的 Linux 预装了 Python 3。
要检查您的设备上是否已经安装了 Python,请运行以下命令:
python3 -v
如果您安装了 Python,您应该会收到类似如下输出:
Python 3.8.2
此外,对于我们的网络爬虫,我们将使用 Python 包 BeautifulSoup(用于选择特定数据)和 Selenium(用于呈现动态加载的内容)。要安装它们,只需运行以下命令:
pip3 install beautifulsoup4
和
pip3 install selenium
最后一步是确保在您的机器上安装了 Google Chrome和Chrome 驱动程序。如果我们想使用 Selenium 抓取动态加载的内容,这些将是必要的。
使用火狐浏览器或者其他浏览器也需要对应的浏览器驱动。
如何检查页面
现在你已经安装了所有东西,是时候开始我们的抓取项目了。
您应该根据需要选择要抓取的网站。请记住,每个网站的内容结构都不同,因此当您开始自己抓取时,您需要调整在此处学到的内容。每个网站都需要对代码进行细微的更改。
对于本文,我决定从 IMDb 的前 250 部电影列表中抓取前十部电影的信息:https : //www.imdb.com/chart/top/。
首先,我们将获得标题,然后我们将通过从每部电影的页面中提取信息来进一步深入研究。一些数据将需要 JavaScript 呈现。
要开始了解内容的结构,您应该右键单击列表中的第一个标题,然后选择“检查元素”。
通过按 CTRL+F 并在 HTML 代码结构中搜索,您将看到页面上只有一个<table>标记。这很有用,因为它为我们提供了有关如何访问数据的信息。
一个 HTML 选择器将为我们提供页面中的所有标题table tbody tr td.titleColumn a
。那是因为所有标题都位于具有“titleColumn
”类的表格单元格内的锚点中。
使用这个 CSS 选择器并获取每个锚点的innerText
将为我们提供我们需要的标题。您可以在刚刚打开的新窗口中使用 JavaScript 行在浏览器控制台中模拟:
document.querySelectorAll("table tbody tr td.titleColumn a")[0].innerText
你会看到这样的结果:
现在我们有了这个选择器,我们可以开始编写 Python 代码并提取我们需要的信息。
如何使用 BeautifulSoup 提取静态加载的内容
我们列表中的电影标题是静态内容。这是因为如果您查看页面源代码(页面上的 CTRL+U 或右键单击然后选择查看页面源代码),您将看到标题已经存在。
静态内容通常更容易抓取,因为它不需要 JavaScript 渲染。为了提取列表中的前十个标题,我们将使用 BeautifulSoup 获取内容,然后将其打印在我们的Scraper的输出中。
import requests
from bs4 import BeautifulSoup
page = requests.get('https://www.imdb.com/chart/top/') # Getting page HTML through request
soup = BeautifulSoup(page.content, 'html.parser') # Parsing content using beautifulsoup
links = soup.select("table tbody tr td.titleColumn a") # Selecting all of the anchors with titles
first10 = links[:10] # Keep only the first 10 anchors
for anchor in first10:
print(anchor.text) # Display the innerText of each anchor
上面的代码使用我们在第一步中看到的选择器从页面中提取电影标题锚点。然后循环遍历前十个并显示每个的innerText。
输出应如下所示:
如何提取动态加载的内容
随着技术的进步,网站开始动态加载其内容。这提高了页面的性能、用户的体验,甚至消除了爬虫的额外障碍。
但是,这使事情变得复杂,因为从简单请求中检索到的 HTML 将不包含动态内容。幸运的是,有了Selenium,我们可以在浏览器中模拟一个请求,等待动态内容显示出来。
如何使用 Selenium 进行请求
您需要知道 chromedriver 的位置。以下代码与第二步中的代码相同,但这次我们使用 Selenium 发出请求。我们仍然会像以前一样使用 BeautifulSoup 解析页面的内容。
from bs4 import BeautifulSoup
from selenium import webdriver
option = webdriver.ChromeOptions()
# I use the following options as my machine is a window subsystem linux.
# I recommend to use the headless option at least, out of the 3
option.add_argument('--headless')
option.add_argument('--no-sandbox')
option.add_argument('--disable-dev-sh-usage')
# Replace YOUR-PATH-TO-CHROMEDRIVER with your chromedriver location
driver = webdriver.Chrome('YOUR-PATH-TO-CHROMEDRIVER', options=option)
driver.get('https://www.imdb.com/chart/top/') # Getting page HTML through request
soup = BeautifulSoup(driver.page_source, 'html.parser') # Parsing content using beautifulsoup. Notice driver.page_source instead of page.content
links = soup.select("table tbody tr td.titleColumn a") # Selecting all of the anchors with titles
first10 = links[:10] # Keep only the first 10 anchors
for anchor in first10:
print(anchor.text) # Display the innerText of each anchor
不要忘记将“YOUR-PATH-TO-CHROMEDRIVER”替换为您提取 chromedriver 的位置。此外,您应该注意到page.content,当我们创建 BeautifulSoup 对象时,我们现在使用的是driver.page_source,它提供页面的 HTML 内容 。
如何使用 Selenium 提取静态加载的内容
使用上面的代码,我们现在可以通过调用每个锚点上的 click 方法来访问每个电影页面。
first_link = driver.find_elements_by_css_selector('table tbody tr td.titleColumn a')[0]
first_link.click()
这将模拟点击第一部电影的链接。但是,在这种情况下,我建议您继续使用driver.get instead. 这是因为click()进入不同页面后您将无法再使用该方法,因为新页面没有指向其他九部电影的链接。
因此,单击列表中的第一个标题后,您需要返回第一页,然后单击第二页,依此类推。这是对性能和时间的浪费。相反,我们将只使用提取的链接并一一访问它们。
对于“肖申克的救赎”,电影页面将是https://www.imdb.com/title/tt0111161/。我们将从页面中提取电影的年份和时长,但这次我们将使用 Selenium 的函数而不是 BeautifulSoup 作为示例。在实践中,您可以使用任何一种,因此请选择您最喜欢的。
要检索电影的年份和持续时间,您应该重复我们在电影页面上执行的第一步。
您会注意到您可以在带有类ipc-inline-list
(“.ipc-inline-list
”选择器)的第一个元素中找到所有信息,并且列表中的所有元素都有role属性值presentation([role=’presentation’]
选择器)。
from bs4 import BeautifulSoup
from selenium import webdriver
option = webdriver.ChromeOptions()
# I use the following options as my machine is a window subsystem linux.
# I recommend to use the headless option at least, out of the 3
option.add_argument('--headless')
option.add_argument('--no-sandbox')
option.add_argument('--disable-dev-sh-usage')
# Replace YOUR-PATH-TO-CHROMEDRIVER with your chromedriver location
driver = webdriver.Chrome('YOUR-PATH-TO-CHROMEDRIVER', options=option)
page = driver.get('https://www.imdb.com/chart/top/') # Getting page HTML through request
soup = BeautifulSoup(driver.page_source, 'html.parser') # Parsing content using beautifulsoup
totalScrapedInfo = [] # In this list we will save all the information we scrape
links = soup.select("table tbody tr td.titleColumn a") # Selecting all of the anchors with titles
first10 = links[:10] # Keep only the first 10 anchors
for anchor in first10:
driver.get('https://www.imdb.com/' + anchor['href']) # Access the movie’s page
infolist = driver.find_elements_by_css_selector('.ipc-inline-list')[0] # Find the first element with class ‘ipc-inline-list’
informations = infolist.find_elements_by_css_selector("[role='presentation']") # Find all elements with role=’presentation’ from the first element with class ‘ipc-inline-list’
scrapedInfo = {
"title": anchor.text,
"year": informations[0].text,
"duration": informations[2].text,
} # Save all the scraped information in a dictionary
totalScrapedInfo.append(scrapedInfo) # Append the dictionary to the totalScrapedInformation list
print(totalScrapedInfo) # Display the list with all the information we scraped
如何使用 Selenium 提取动态加载的内容
网络抓取的下一个重要步骤是提取动态加载的内容。您可以在编辑列表部分的每个电影页面(例如https://www.imdb.com/title/tt0111161/)上找到此类内容。
如果您在页面上使用检查,您会看到您可以找到该部分作为属性data-testid
设置为的元素firstListCardGroup-editorial
。但是如果你查看页面源代码,你不会在任何地方找到这个属性值。这是因为编辑列表部分是由 IMDB 动态加载的。
在下面的示例中,我们将抓取每部电影的编辑列表,并将其添加到我们当前的总抓取信息结果中。
为此,我们将导入更多包,以便等待我们的动态内容加载。
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
option = webdriver.ChromeOptions()
# I use the following options as my machine is a window subsystem linux.
# I recommend to use the headless option at least, out of the 3
option.add_argument('--headless')
option.add_argument('--no-sandbox')
option.add_argument('--disable-dev-sh-usage')
# Replace YOUR-PATH-TO-CHROMEDRIVER with your chromedriver location
driver = webdriver.Chrome('YOUR-PATH-TO-CHROMEDRIVER', options=option)
page = driver.get('https://www.imdb.com/chart/top/') # Getting page HTML through request
soup = BeautifulSoup(driver.page_source, 'html.parser') # Parsing content using beautifulsoup
totalScrapedInfo = [] # In this list we will save all the information we scrape
links = soup.select("table tbody tr td.titleColumn a") # Selecting all of the anchors with titles
first10 = links[:10] # Keep only the first 10 anchors
for anchor in first10:
driver.get('https://www.imdb.com/' + anchor['href']) # Access the movie’s page
infolist = driver.find_elements_by_css_selector('.ipc-inline-list')[0] # Find the first element with class ‘ipc-inline-list’
informations = infolist.find_elements_by_css_selector("[role='presentation']") # Find all elements with role=’presentation’ from the first element with class ‘ipc-inline-list’
scrapedInfo = {
"title": anchor.text,
"year": informations[0].text,
"duration": informations[2].text,
} # Save all the scraped information in a dictionary
WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-testid='firstListCardGroup-editorial']"))) # We are waiting for 5 seconds for our element with the attribute data-testid set as `firstListCardGroup-editorial`
listElements = driver.find_elements_by_css_selector("[data-testid='firstListCardGroup-editorial'] .listName") # Extracting the editorial lists elements
listNames = [] # Creating an empty list and then appending only the elements texts
for el in listElements:
listNames.append(el.text)
scrapedInfo['editorial-list'] = listNames # Adding the editorial list names to our scrapedInfo dictionary
totalScrapedInfo.append(scrapedInfo) # Append the dictionary to the totalScrapedInformation list
print(totalScrapedInfo) # Display the list with all the information we scraped
对于前面的示例,您应该获得以下输出:
如何保存抓取的内容
现在我们拥有了所需的所有数据,我们可以将其保存为 .json 或 .csv 文件,以便于阅读。
为此,我们将只使用 Python 中的 JSON 和 CVS 包并将我们的内容写入新文件:
import csv
import json
...
file = open('movies.json', mode='w', encoding='utf-8')
file.write(json.dumps(totalScrapedInfo))
writer = csv.writer(open("movies.csv", 'w'))
for movie in totalScrapedInfo:
writer.writerow(movie.values())
抓取技巧和窍门
虽然到目前为止我们的指南已经足够先进,可以处理 JavaScript 渲染场景,但在 Selenium 中还有很多东西需要探索。
在本节中,我将分享一些可能会派上用场的提示和技巧。
1. 为您的请求计时
如果您在短时间内向服务器发送数百个请求的垃圾邮件,很可能在某个时候会出现验证码,或者您的 IP 甚至可能被阻止。不幸的是,Python 中没有解决方法可以避免这种情况。
因此,您应该在每个请求之间放置一些超时间隔,以便流量看起来更自然。
import time
import requests
page = requests.get('https://www.imdb.com/chart/top/') # Getting page HTML through request
time.sleep(30) # Wait 30 seconds
page = requests.get('https://www.imdb.com/') # Getting page HTML through request
2. 错误处理
由于网站是动态的并且可以随时更改结构,如果您经常使用相同的网络抓取工具,错误处理可能会派上用场。
try:
WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.CSS_SELECTOR, "your selector")))
break
except TimeoutException:
# If the loading took too long, print message and try again
print("Loading took too much time!")
当您等待一个元素、提取它时,甚至当您只是发出请求时,try and error 语法会很有用。
3. 截屏
如果您需要随时获取正在抓取的网页的屏幕截图,可以使用:
driver.save_screenshot(‘screenshot-file-name.png’)
这有助于在您处理动态加载的内容时进行调试。
4. 阅读文档
最后但并非最不重要的一点是,不要忘记阅读Selenium的文档。该库包含有关如何执行您可以在浏览器中执行的大多数操作的信息。
使用 Selenium,您可以填写表单、按下按钮、回答弹出消息以及做许多其他很酷的事情。
如果您遇到新问题,他们的文档可能是您最好的朋友。
最后的想法
本文的目的是为您提供使用 Python 和 Selenium 和 BeautifulSoup 进行网络抓取的高级介绍。虽然这两种技术仍有许多功能需要探索,但您现在已经有了如何开始抓取的坚实基础。
有时网页抓取可能非常困难,因为网站开始在开发人员的道路上设置越来越多的障碍。其中一些障碍可能是验证码、IP 块或动态内容。仅使用 Python 和 Selenium 来克服它们可能很困难,甚至是不可能的。
所以,我也给你一个替代方案。尝试使用Web 抓取 API来为您解决所有这些挑战。它还使用轮换代理,因此您不必担心在请求之间添加超时。请记住始终检查您想要的数据是否可以合法提取和使用。