使用 AutoScraper 进行网页抓取教程

AutoScraper 是一款面向初学者的 Python 工具,它能简化网页抓取,并以最少的代码量来处理结构化与动态网站。
14 min read
使用 AutoScraper 进行网页抓取教程

AutoScraper 是一个 Python 库,可通过根据示例查询自动识别并提取网站数据,从而无需手动检查 HTML,简化了网页抓取流程。与传统的抓取工具不同,AutoScraper 会根据示例查询来学习数据元素的结构,对于初学者和有经验的开发者来说都是不错的选择。它非常适合用于收集商品信息、整合内容或进行市场调研等任务,并且能够高效应对动态网站,无需复杂的配置。

在本文中,你将学习如何使用 Python 中的 AutoScraper 进行网页抓取。

先决条件

安装 AutoScraper 非常简单。你需要在本地安装好 Python 3 及更高版本。和任何其他Python 网页抓取项目一样,你只需要运行以下几条命令即可创建项目目录,并在其中创建并激活虚拟环境:

# Set up project directory
mkdir auto-scrape
cd auto-scrape

# Create virtual environment
python -m venv env
# For Mac & Linux users
source env/bin/activate
# For Windows users
venv\Scripts\activate

使用虚拟环境可以简化管理项目中的依赖。

接下来,通过运行以下命令安装 autoscraper 库:

pip install autoscraper

最后,你需要安装 pandas 来在最后将抓取结果保存到 CSV 文件中。pandas 是一个 Python 库,提供了易于使用的数据分析和处理工具,可以让你轻松地处理并将抓取结果存储为多种格式(如 CSV、XLSX 和 JSON)。运行以下命令进行安装:

pip install pandas

选择目标网站

当你抓取公共网站时,务必要查看网站的服务条款(ToS)或 robots.txt 文件,确保该网站允许抓取数据,以避免任何法律或道德问题。此外,最好选择那些以表格或列表等结构化格式提供数据的网站,这样的数据更易于提取。

传统的抓取工具通常需要分析网页的 HTML 结构来定位目标数据元素,这十分耗时,并且需要熟悉浏览器开发者控制台等工具。然而,AutoScraper 通过基于示例数据(即 wanted_list)自动学习数据的结构,省去了手动检查 HTML 的步骤。

在本教程中,你将首先从 Scrape This Site 的 Countries of the World: A Simple Example 页面 抓取数据。该网页是一个针对抓取工具测试而设计的初学者友好型沙盒,它有一个非常简单的结构,便于演示基本抓取技巧。一旦熟悉了该页面的基本结构后,你将继续抓取 Hockey Teams: Forms, Searching and Pagination 页面,它的结构更为复杂。

使用 AutoScraper 抓取简单数据

现在你已经确定了想要抓取的两个页面,是时候开始抓取了!

由于 Countries of the World: A Simple Example 页面 的结构非常简单,你可以使用以下脚本来抓取国家列表及其首都、人口和面积:

# 1. Import dependencies
from autoscraper import AutoScraper
import pandas as pd

# 2. Define the URL of the site to be scraped
url = "https://www.scrapethissite.com/pages/simple/"

# 3. Instantiate the AutoScraper
scraper = AutoScraper()

# 4. Define the wanted list by using an example from the web page
# This list should contain some text or values that you want to scrape
wanted_list = ["Andorra", "Andorra la Vella", "84000", "468.0"]

# 5. Build the scraper based on the wanted list and URL
scraper.build(url, wanted_list)

# 6. Get the results for all the elements matched
results = scraper.get_result_similar(url, grouped=True)

# 7. Display the keys and sample data to understand the structure
print("Keys found by the scraper:", results.keys())

# 8. Assign columns based on scraper keys and expected order of data
columns = ["Country Name", "Capital", "Area (sq km)", "Population"]

# 9. Create a DataFrame with the extracted data
data = {columns[i]: results[list(results.keys())[i]] for i in range(len(columns))}
df = pd.DataFrame(data)

# 10. Save the DataFrame to a CSV file
csv_filename = 'countries_data.csv'
df.to_csv(csv_filename, index=False)

print(f"Data has been successfully saved to {csv_filename}")

上面的代码包含注释来解释具体发生了什么,但以下是简要说明:脚本首先导入 AutoScraper 和 pandas。然后,你定义了目标网站的 URL,并创建了一个 AutoScraper 实例。

这里最有意思的部分在于:你无需告诉抓取器数据在哪(比如使用 XPath 或其他选择器),只需要提供一些你想要抓取的数据示例。在第四处注释中,给出某个国家的部分数据点作为一个数组(即 wanted_list)。

一旦 wanted_list 准备好后,你就可以使用 URL 和 wanted_list 来构建抓取器。AutoScraper 会下载目标网站并基于这些规则(它存储在其规则栈列表中)来提取未来任何目标 URL 中的数据。

在第六处注释下的代码中,你调用了 AutoScraper 模型的 get_result_similar 方法来提取与 wanted_list 相似的数据。紧接着的 print 语句会显示从目标网站中找到的数据对应的规则 ID,你的输出应类似:

Keys found by the scraper: dict_keys(['rule_4y6n', 'rule_gghn', 'rule_a6r9', 'rule_os29'])

第八和第九处的代码为你的 CSV 文件创建表头,并将提取的数据格式化为一个 pandas DataFrame。最后,第十处的代码将数据保存到 CSV 文件中。

在你运行这个脚本(将上面代码保存为 script.py,然后执行 python script.py),项目目录下就会生成一个名为 countries_data.csv 的新文件,内容看起来大概如下:

Country Name,Capital,Area (sq km),Population
Andorra,Andorra la Vella,84000,468.0
United Arab Emirates,Abu Dhabi,4975593,82880.0
...246 collapsed rows
Zambia,Lusaka,13460305,752614.0
Zimbabwe,Harare,11651858,390580.0

就是这样!这就是使用 AutoScraper 抓取简单网站的全部过程。

处理并提取具有复杂设计的网站数据

当你碰到稍微复杂的网站时,比如包含大量相似数据表格的 Hockey Teams: Forms, Searching and Pagination 页面,上面展示的简单方法可能会失效。你可以尝试用之前的方法来抓取该网站中的球队名称、年份、胜场、败场以及其他字段,就会发现问题。

好在 AutoScraper 允许在构建模型时对收集到的规则进行修剪(prune),在实际提取数据前先进行定制化。下面的代码展示了如何实现:

from autoscraper import AutoScraper
import pandas as pd

# Define the URL of the site to be scraped
url = "https://www.scrapethissite.com/pages/forms/"

def setup_model():

    # Instantiate the AutoScraper
    scraper = AutoScraper()

    # Define the wanted list by using an example from the web page
    # This list should contain some text or values that you want to scrape
    wanted_list = ["Boston Bruins", "1990", "44", "24", "0.55", "299", "264", "35"]

    # Build the scraper based on the wanted list and URL
    scraper.build(url, wanted_list)

    # Get the results for all the elements matched
    results = scraper.get_result_similar(url, grouped=True)

    # Display the data to understand the structure
    print(results)

    # Save the model
    scraper.save("teams_model.json")

def prune_rules():
    # Create an instance of Autoscraper
    scraper = AutoScraper()
    
    # Load the model saved earlier
    scraper.load("teams_model.json")

    # Update the model to only keep necessary rules
    scraper.keep_rules(['rule_hjk5', 'rule_9sty', 'rule_2hml', 'rule_3qvv', 'rule_e8x1', 'rule_mhl4', 'rule_h090', 'rule_xg34'])

    # Save the updated model again
    scraper.save("teams_model.json")
    
def load_and_run_model():
    # Create an instance of Autoscraper
    scraper = AutoScraper()
    
    # Load the model saved earlier
    scraper.load("teams_model.json")

    # Get the results for all the elements matched
    results = scraper.get_result_similar(url, grouped=True)

    # Assign columns based on scraper keys and expected order of data
    columns = ["Team Name", "Year", "Wins", "Losses", "Win %", "Goals For (GF)", "Goals Against (GA)", "+/-"]

    # Create a DataFrame with the extracted data
    data = {columns[i]: results[list(results.keys())[i]] for i in range(len(columns))}
    df = pd.DataFrame(data)

    # Save the DataFrame to a CSV file
    csv_filename = 'teams_data.csv'
    df.to_csv(csv_filename, index=False)

    print(f"Data has been successfully saved to {csv_filename}")

# setup_model()
# prune_rules()
# load_and_run_model()

该脚本定义了三个方法:setup_modelprune_rulesload_and_run_modelsetup_model 方法与之前看到的类似:它新建一个抓取器实例,创建一个 wanted_list,基于该 wanted_list 来构建抓取器,再从目标网站中抓取数据并打印出收集到的规则 ID,最后将当前模型保存到项目目录下名为 teams_model.json 的文件中。

要运行它,你只需在脚本中取消注释 # setup_model() 这一行,然后保存全部脚本到一个文件(如 script.py),运行 python script.py。你的输出会类似:

{'rule_hjk5': ['Boston Bruins', 'Buffalo Sabres', 'Calgary Flames', 'Chicago Blackhawks', 'Detroit Red Wings', 'Edmonton Oilers', 'Hartford Whalers', 'Los Angeles Kings', 'Minnesota North Stars', 'Montreal Canadiens', 'New Jersey Devils', 'New York Islanders', 'New York Rangers', 'Philadelphia Flyers', 'Pittsburgh Penguins', 'Quebec Nordiques', 'St. Louis Blues', 'Toronto Maple Leafs', 'Vancouver Canucks', 'Washington Capitals', 'Winnipeg Jets', 'Boston Bruins', 'Buffalo Sabres', 'Calgary Flames', 'Chicago Blackhawks'], 'rule_uuj6': ['Boston Bruins', 'Buffalo Sabres', 'Calgary Flames', 'Chicago Blackhawks', 'Detroit Red Wings', 'Edmonton Oilers', 'Hartford Whalers', 'Los Angeles Kings', 'Minnesota North Stars', 'Montreal Canadiens', 'New Jersey Devils', 'New York Islanders', 'New York Rangers', 'Philadelphia Flyers', 'Pittsburgh Penguins', 'Quebec Nordiques', 'St. Louis Blues', 'Toronto Maple Leafs', 'Vancouver Canucks', 'Washington Capitals', 'Winnipeg Jets', 'Boston Bruins', 'Buffalo Sabres', 'Calgary Flames', 'Chicago Blackhawks'], 'rule_9sty': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_9nie': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_41rr': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_ufil': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_ere2': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_w0vo': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_rba5': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_rmae': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_ccvi': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_3c34': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_4j80': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_oc36': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_93k1': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_d31n': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_ghh5': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_5rne': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_4p78': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_qr7s': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_60nk': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_wcj7': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_0x7y': ['1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1990', '1991', '1991', '1991', '1991'], 'rule_2hml': ['44', '31', '46', '49', '34', '37', '31', '46', '27', '39', '32', '25', '36', '33', '41', '16', '47', '23', '28', '37', '26', '36', '31', '31', '36'], 'rule_swtb': ['24'], 'rule_e8x1': ['0.55', '14', '0.575', '0.613', '-25', '0', '-38', '0.575', '-10', '24', '8', '-67', '32', '-15', '0.512', '-118', '0.588', '-77', '-72', '0', '-28', '-5', '-10', '-9', '21'], 'rule_3qvv': ['24', '30', '26', '23', '38', '37', '38', '24', '39', '30', '33', '45', '31', '37', '33', '50', '22', '46', '43', '36', '43', '32', '37', '37', '29'], 'rule_n07w': ['24', '30', '26', '23', '38', '37', '38', '24', '39', '30', '33', '45', '31', '37', '33', '50', '22', '46', '43', '36', '43', '32', '37', '37', '29'], 'rule_qmem': ['0.55', '0.388', '0.575', '0.613', '0.425', '0.463', '0.388', '0.575', '0.338', '0.487', '0.4', '0.312', '0.45', '0.412', '0.512', '0.2', '0.588', '0.287', '0.35', '0.463', '0.325', '0.45', '0.388', '0.388', '0.45'], 'rule_b9gx': ['264', '278', '263', '211', '298', '272', '276', '254', '266', '249', '264', '290', '265', '267', '305', '354', '250', '318', '315', '258', '288', '275', '299', '305', '236'], 'rule_mhl4': ['299', '292', '344', '284', '273', '272', '238', '340', '256', '273', '272', '223', '297', '252', '342', '236', '310', '241', '243', '258', '260', '270', '289', '296', '257'], 'rule_24nt': ['264', '278', '263', '211', '298', '272', '276', '254', '266', '249', '264', '290', '265', '267', '305', '354', '250', '318', '315', '258', '288', '275', '299', '305', '236'], 'rule_h090': ['264', '278', '263', '211', '298', '272', '276', '254', '266', '249', '264', '290', '265', '267', '305', '354', '250', '318', '315', '258', '288', '275', '299', '305', '236'], 'rule_xg34': ['35', '14', '81', '73', '-25', '0', '-38', '86', '-10', '24', '8', '-67', '32', '-15', '37', '-118', '60', '-77', '-72', '0', '-28', '-5', '-10', '-9', '21']}

这段输出展示了 AutoScraper 在调用 get_result_similar 时,从目标网站中收集到的完整数据。你会注意到这些数据存在许多重复项。这是因为 AutoScraper 不仅会收集目标网站的数据,还会尝试根据其内部的推断,创建一些规则分组(即它认为彼此相关的字段)。如果它能正确地分组,你就能像前一个示例那样轻松地从相似的网站中提取数据。

然而,对于这个表格中包含大量相似数字的网站,AutoScraper 并不那么容易分辨。它可能把很多数字都误认为存在强相关性,导致你得到一个带有重复值的大数据集。

现在,你需要仔细查看这些数据,挑选出包含正确数据(即正好包含一列完整数据,且数量与表格行数一致)的规则。

在这份输出中,根据手动检查并保证每个匹配的规则都对应 25 个数据元素(目标页面的表格共有 25 行),我们筛选出以下规则:

['rule_hjk5', 'rule_9sty', 'rule_2hml', 'rule_3qvv', 'rule_e8x1', 'rule_mhl4', 'rule_h090', 'rule_xg34']

你需要在 prune_rules 方法里更新这一列表。然后,在脚本中注释掉 setup_model(),取消注释 prune_rules(),再次运行。它会从 teams_model.json 文件中加载先前训练的模型,只保留指定的规则,然后将精简后的模型再次保存到同一个文件。你可以查看 teams_model.json 的内容,看看当前存储了哪些规则。完成后,你的模型就准备就绪了。

现在,你可以运行 load_and_run_model 方法:注释掉 prune_rules 行,取消注释 load_and_run_model,并再次运行脚本。它会提取并保存正确的数据到项目目录下名为 teams_data.csv 的文件,同时打印以下输出:

Data has been successfully saved to teams_data.csv

这时,你的 teams_data.csv 文件内容如下所示:

Team Name,Year,Wins,Losses,Win %,Goals For (GF),Goals Against (GA),+/-
Boston Bruins,1990,44,0.55,24,299,264,35
Buffalo Sabres,1990,31,14,30,292,278,14
...21 more rows
Calgary Flames,1991,31,-9,37,296,305,-9
Chicago Blackhawks,1991,36,21,29,257,236,21

你可以在这个 GitHub 仓库中查看本教程中所有演示的代码。

AutoScraper 常见挑战

当目标网站的数据规模较小,而且数据点足够具备差异时,AutoScraper 表现会比较好。不过,对于结构更复杂的数据表格(就像示例中的网站),初次的模型搭建就可能有些繁琐。另外,AutoScraper 也不支持 JavaScript 渲染,你需要将它与 SplashSeleniumPuppeteer 等完整库结合使用。

如果你在抓取时遇到 IP 封锁或需要自定义请求头等问题,AutoScraper 支持在内部使用其 requests 模块添加额外的请求参数,如下所示:

# build the scraper on an initial URL
scraper.build(
    url,
    wanted_list=wanted_list,
    request_args=dict(proxies=proxies) # this is where you can pass in a list of proxies or customer headers
)

例如,下面展示了如何自定义 User-Agent 并设置代理:

request_args = { 
  "headers: {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 \
            (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36"  # You can customize this value with your desired user agent. This value is the default used by Autoscraper.
  },
  "proxies": {
    "http": "http://user:[email protected]:3128/" # Example proxy to show you how to use the proxy port, host, username, and password values
  }
}
# build the scraper on an initial URL
scraper.build(
    url,
    wanted_list=wanted_list,
    request_args=request_args
)

但如果你不想一次又一次地被封锁,那么就需要使用一个更优的代理来进行抓取。为此,你可以考虑使用 Bright Data residential proxies,它拥有遍布 195 个国家、数量超过 7200 万的住宅 IP 地址。

AutoScraper 在内部使用 Python 的 requests 库与目标网站交互,本身并不支持速率限制。若要应对网站的速率限制,你需要手动实现一个限速功能,或者使用诸如 ratelimit 这样的预构建库。

由于 AutoScraper 只能应对非动态网站,无法处理包含 CAPTCHA 等挑战的站点。这种情况下,使用诸如 Bright Data Web Scraping API 等更全面的解决方案可能会更合适,该 API 能够直接提供像 LinkedIn、Amazon、Zillow 等网站的结构化数据。

总结

本文介绍了 AutoScraper 的基本原理,并演示了如何从简单和复杂的网站上提取数据。就像你所见,AutoScraper 基于 requests 向目标网站发送请求,这使得它容易在面对动态网站或诸如 CAPTCHA 之类的防护时遇到困难。此外,出于高并发抓取的需要,大多数网站容易识别异常流量来源,你需要使用代理来规避这一问题。对此,Bright Data 能够提供帮助。

Bright Data 是一家在代理网络、AI 驱动的网络抓取工具以及企业级数据集领域领先的供应商。立即注册,开始探索 Bright Data 的产品,其中还包括免费的试用!