Python 网络爬虫初学者指南

通过使用必要的 Python 工具来抓取完整的 HTML 网站,获得一些实践经验。
157 位读者喜欢这篇文章。
HTML code

Jason Baker 为 Opensource.com 拍摄。 

有很多很棒的书可以帮助你学习 Python,但实际上谁会从头到尾读这些书呢?(剧透:不是我)。

许多人觉得教学书籍很有用,但我通常不是通过从头到尾阅读一本书来学习的。我通过做一个项目,挣扎,弄清楚一些事情,然后再读另一本书来学习。所以,先把你的书扔掉(暂时),让我们来学习一些 Python。

接下来是我在 Python 中第一个爬虫项目的指南。它假设的 Python 和 HTML 知识非常少。本文旨在说明如何使用 Python 库 requests 访问网页内容,并使用 BeatifulSoup4 以及 JSON 和 pandas 解析内容。我将简要介绍 Selenium,但我不会深入探讨如何使用该库——这个主题值得单独写一篇教程。最终,我希望向您展示一些技巧和窍门,以使网络爬虫不那么令人感到压力。

安装我们的依赖项

本指南中的所有资源都可以在我的 GitHub 仓库 中找到。如果您需要安装 Python 3 的帮助,请查看 LinuxWindowsMac 的教程。

$ python3 -m venv 
$ source venv/bin/activate
$ pip install requests bs4 pandas 

如果您喜欢使用 JupyterLab,您可以使用这个 notebook 运行所有代码。有很多方法可以安装 JupyterLab,这是其中一种方法

# from the same virtual environment as above, run:
$ pip install jupyterlab

为我们的网络爬虫项目设定目标

现在我们已经安装了依赖项,但是抓取网页需要什么呢? 

让我们退后一步,确保明确我们的目标。这是我对一个成功的网络爬虫项目的要求清单。

  • 我们正在收集的信息值得我们努力构建一个可工作的网络爬虫。
  • 我们正在下载的信息可以通过网络爬虫合法且合乎道德地收集。
  • 我们对如何在 HTML 代码中找到目标信息有一些了解。
  • 我们拥有正确的工具:在本例中,它是库 BeautifulSouprequests
  • 我们知道(或愿意学习)如何解析 JSON 对象。
  • 我们有足够的数据技能来使用 pandas

关于 HTML 的评论:虽然 HTML 是运行互联网的野兽,但我们主要需要理解的是标签是如何工作的。标签是夹在尖括号标签之间的信息集合。例如,这是一个假想的标签,称为“pro-tip”

<pro-tip> All you need to know about html is how tags work </pro-tip>

我们可以通过调用它的标签“pro-tip”来访问其中的信息(“您需要知道的一切……”)。如何在本文档的后面部分介绍查找和访问标签的方法。要了解更多 HTML 基础知识,请查看这篇文章

在网络爬虫项目中寻找什么

一些数据收集目标比其他目标更适合网络爬虫。我对符合良好项目资格的指导原则如下。

没有可用于数据的公共 API。通过 API 捕获结构化数据会容易得多,并且有助于明确收集数据的合法性和道德性。需要有大量的具有规则、可重复格式的结构化数据来证明这种努力是合理的。网络爬虫可能很麻烦。BeautifulSoup (bs4) 使其更容易,但无法避免网站的个别特性,这将需要自定义。不需要数据格式完全相同,但这确实使事情变得更容易。“边缘情况”(偏离常态的情况)越多,抓取就越复杂。

免责声明:我没有接受过任何法律培训;以下内容不旨在作为正式的法律建议。

关于合法性,访问大量信息可能令人陶醉,但这仅仅是因为它是可能的并不意味着它应该被完成。

值得庆幸的是,有一些公共信息可以指导我们的道德和我们的网络爬虫。大多数网站都有一个与该网站关联的 robots.txt 文件,指示允许哪些抓取活动,哪些不允许。它主要用于与搜索引擎(最终的网络爬虫)交互。但是,网站上的大部分信息被认为是公共信息。因此,有些人认为 robots.txt 文件是一组建议,而不是具有法律约束力的文件。robots.txt 文件不涉及诸如数据的道德收集和使用等主题。

在开始爬虫项目之前我会问自己的问题

  • 我是否正在抓取受版权保护的材料?
  • 我的抓取活动会损害个人隐私吗?
  • 我是否发出了大量可能使服务器过载或损坏的请求?
  • 抓取是否有可能暴露我不拥有的知识产权?
  • 是否有服务条款管辖网站的使用,我是否遵守了这些条款?
  • 我的抓取活动会降低原始数据的价值吗?(例如,我是否计划按原样重新包装数据,并可能从原始来源虹吸网站流量)?

当我抓取网站时,我确保我对所有这些问题的回答都是“否”。

要深入了解法律问题,请参阅 2018 年出版的 Krotov 和 Silva 撰写的《网络爬虫的合法性和道德性》 以及 Sellars 撰写的《网络爬虫的二十年与计算机欺诈和滥用法案》

现在是抓取的时候了!

在评估上述内容之后,我想出了一个项目。我的目标是提取爱达荷州所有 Family Dollar 商店的地址。这些商店在农村地区的存在感很强,所以我想了解在这个相当农村的州有多少家。

起点是 Family Dollar 的位置页面

Family Dollar Idaho locations page

首先,让我们在我们的 Python 虚拟环境中加载我们的先决条件。此处的代码旨在添加到 Python 文件(如果您正在寻找名称,则为scraper.py)或在 JupyterLab 中的单元格中运行。

import requests # for making standard html requests
from bs4 import BeautifulSoup # magical tool for parsing html data
import json # for parsing data
from pandas import DataFrame as df # premier library for data organization

接下来,我们从目标 URL 请求数据。

page = requests.get("https://locations.familydollar.com/id/")
soup = BeautifulSoup(page.text, 'html.parser') 

BeautifulSoup 将获取 HTML 或 XML 内容,并将其转换为复杂的对象树。以下是我们将使用几种常见的对象类型。

  • BeautifulSoup——解析的内容
  • Tag——标准的 HTML 标签,您将遇到的 bs4 元素的主要类型
  • NavigableString——标签内的文本字符串
  • Comment——一种特殊的 NavigableString 类型

当我们查看 requests.get() 输出时,还有更多需要考虑的。我只使用了 page.text() 将请求的页面转换为可读的内容,但还有其他输出类型

  • page.text() 用于文本(最常见)
  • page.content() 用于逐字节输出
  • page.json() 用于 JSON 对象
  • page.raw() 用于原始套接字响应(谢谢,不用了)

我只在仅英语网站上使用拉丁字母工作过。requests 中的默认编码设置对此非常有效。但是,除了仅英语网站之外,还有一个丰富的互联网世界。为了确保 requests 正确解析内容,您可以设置文本的编码

page = requests.get(URL)
page.encoding = 'ISO-885901' 
soup = BeautifulSoup(page.text, 'html.parser') 

仔细查看 BeautifulSoup 标签,我们看到

  • bs4 元素 tag 正在捕获 HTML 标签
  • 它既有名称,也有可以像字典一样访问的属性:tag['someAttribute']
  • 如果一个标签有多个同名属性,则只访问第一个实例。
  • 标签的子元素可以通过 tag.contents 访问。
  • 所有标签后代都可以通过 tag.contents 访问。
  • 您始终可以使用以下命令将完整内容作为字符串访问:re.compile("your_string") 而不是导航 HTML 树。

确定如何提取相关内容

警告:此过程可能令人沮丧。

在网络爬虫期间提取内容可能是一个令人望而生畏的过程,充满了错误。我认为最好的方法是从一个有代表性的示例开始,然后扩大规模(这个原则适用于任何编程任务)。查看页面的 HTML 源代码至关重要。有很多方法可以做到这一点。

您可以使用终端中的 Python 查看页面的整个源代码(不推荐)。运行此代码的风险自负

print(soup.prettify())

虽然打印页面的整个源代码可能适用于某些教程中显示的玩具示例,但大多数现代网站的任何一个页面上都有大量内容。即使是 404 页面也可能充满了用于页眉、页脚等的代码。

通常最简单的方法是通过您喜欢的浏览器中的查看页面源代码来浏览源代码(右键单击,然后选择“查看页面源代码”)。这是找到目标内容的最可靠方法(我稍后会解释原因)。

Family Dollar page source code

 

在本例中,我需要在浩瀚的 HTML 海洋中找到我的目标内容——地址、城市、州和邮政编码。通常,简单搜索页面源代码(ctrl + F)将找到我的目标位置所在的章节。一旦我实际看到我的目标内容示例(至少一家商店的地址),我就寻找一个属性或标签,将此内容与其余内容区分开来。

似乎首先,我需要收集爱达荷州不同城市 Family Dollar 商店的网址,并访问这些网站以获取地址信息。这些网址似乎都包含在 href 标签中。太好了!我将尝试使用 find_all 命令搜索它

dollar_tree_list = soup.find_all('href')
dollar_tree_list

搜索 href 没有产生任何结果,真糟糕。这可能是因为 href 嵌套在类 itemlist 中而失败的。对于下一次尝试,搜索 item_list。因为“class”是 Python 中的保留字,所以使用 class_ 代替。bs4 函数 soup.find_all() 原来是 bs4 函数的瑞士军刀。

dollar_tree_list = soup.find_all(class_ = 'itemlist')
for i in dollar_tree_list[:2]:
  print(i)

有趣的是,我发现搜索特定类通常是一种成功的方法。我们可以通过找出对象的类型和长度来了解有关对象的更多信息。

type(dollar_tree_list)
len(dollar_tree_list)

可以使用 .contents 提取此 BeautifulSoup “ResultSet”中的内容。这也是创建单个代表性示例的好时机。

example = dollar_tree_list[2] # a representative example
example_content = example.contents
print(example_content)

使用 .attr 查找此对象内容中存在的属性。注意:.contents 通常返回一个正好包含一个项目的列表,因此第一步是使用方括号表示法索引该项目。

example_content = example.contents[0]
example_content.attrs

既然我可以看到 href 是一个属性,那么可以像字典项一样提取它

example_href = example_content['href']
print(example_href)

将我们的网络爬虫组合在一起

所有这些探索都为我们指明了前进的方向。这是我们上面弄清楚的逻辑的清理版本。

city_hrefs = [] # initialise empty list

for i in dollar_tree_list:
    cont = i.contents[0]
    href = cont['href']
    city_hrefs.append(href)

#  check to be sure all went well
for i in city_hrefs[:2]:
  print(i)

输出是要抓取的爱达荷州 Family Dollar 商店的 URL 列表。

话虽如此,我仍然没有地址信息!现在,需要抓取每个城市 URL 以获取此信息。因此,我们使用单个代表性示例重新开始该过程。

page2 = requests.get(city_hrefs[2]) # again establish a representative example
soup2 = BeautifulSoup(page2.text, 'html.parser')

Family Dollar map and code

地址信息嵌套在 type= "application/ld+json" 中。在进行了大量的地理位置抓取之后,我已经认识到这是一种用于存储地址信息的常见结构。幸运的是,soup.find_all() 也支持搜索 type

arco = soup2.find_all(type="application/ld+json")
print(arco[1])

地址信息在第二个列表成员中!终于!

我使用 .contents(这是过滤 soup 后的一个很好的默认操作)提取了内容(从第二个列表项中)。同样,由于 contents 的输出是一个列表,我索引了该列表项

arco_contents = arco[1].contents[0]
arco_contents

哇,看起来不错。此处呈现的格式与 JSON 格式一致(此外,类型名称中确实有“json”)。JSON 对象可以像字典一样工作,内部嵌套字典。一旦您熟悉了它,实际上它是一种很好的格式(并且它肯定比长串 RegEx 命令更容易编程)。虽然从结构上看这看起来像一个 JSON 对象,但它仍然是一个 bs4 对象,需要正式的程序转换才能转换为 JSON 对象才能作为 JSON 对象访问

arco_json =  json.loads(arco_contents)
type(arco_json)
print(arco_json)

在该内容中,有一个名为 address 的键,其中在较小的嵌套字典中包含所需的地址信息。可以这样检索

arco_address = arco_json['address']
arco_address

好吧,这次我们是认真的。现在我可以迭代爱达荷州商店 URL 列表了

locs_dict = [] # initialise empty list

for link in city_hrefs:
  locpage = requests.get(link)   # request page info
  locsoup = BeautifulSoup(locpage.text, 'html.parser') 
      # parse the page's content
  locinfo = locsoup.find_all(type="application/ld+json") 
      # extract specific element
  loccont = locinfo[1].contents[0]  
      # get contents from the bs4 element set
  locjson = json.loads(loccont)  # convert to json
  locaddr = locjson['address'] # get address
  locs_dict.append(locaddr) # add address to list

使用 pandas 清理我们的网络爬虫结果

我们在一个字典中加载了大量数据,但是我们还有一些额外的垃圾,这将使重用我们的数据比需要的更复杂。为了执行一些最终的数据组织步骤,我们转换为 pandas 数据框,删除不需要的列“@type”和“country”),并检查前五行以确保一切看起来正常。

locs_df = df.from_records(locs_dict)
locs_df.drop(['@type', 'addressCountry'], axis = 1, inplace = True)
locs_df.head(n = 5)

确保保存结果!!

df.to_csv(locs_df, "family_dollar_ID_locations.csv", sep = ",", index = False)

我们做到了!这里有一个逗号分隔的爱达荷州所有 Family Dollar 商店的列表。真是疯狂的旅程。

关于 Selenium 和数据抓取的几句话

Selenium 是一个用于自动与网页交互的常用实用程序。为了解释为什么有时必须使用它,让我们通过一个使用 Walgreens 网站的示例。检查元素提供了浏览器中显示的代码

Walgreens location page and code

 

查看页面源代码 提供了 requests 将获取的代码

Walgreens source code

opensource.com

当这两者不一致时,会有插件修改源代码——因此,应该在页面加载到浏览器后访问它。requests 无法做到这一点,但 Selenium 可以。

Selenium 需要一个 Web 驱动程序来检索内容。它实际上打开一个 Web 浏览器,并收集此页面内容。Selenium 功能强大——它可以以多种方式与加载的内容交互(阅读文档)。使用 Selenium 获取数据后,继续像以前一样使用 BeautifulSoup

url = "https://www.walgreens.com/storelistings/storesbycity.jsp?requestType=locator&state=ID"
driver = webdriver.Firefox(executable_path = 'mypath/geckodriver.exe')
driver.get(url)
soup_ID = BeautifulSoup(driver.page_source, 'html.parser')
store_link_soup = soup_ID.find_all(class_ = 'col-xl-4 col-lg-4 col-md-4') 

在 Family Dollar 的案例中我不需要 Selenium,但我确实保留了它以备不时之需,因为有时渲染的内容与源代码不同。

总结

总而言之,当使用网络爬虫来完成有意义的任务时

  • 要有耐心
  • 查阅手册(这些手册非常有用)

如果您对答案感到好奇

Family Dollar locations map

美国有很多很多的 Family Dollar 商店。

完整的源代码是

import requests
from bs4 import BeautifulSoup
import json
from pandas import DataFrame as df

page = requests.get("https://www.familydollar.com/locations/")
soup = BeautifulSoup(page.text, 'html.parser')

# find all state links
state_list = soup.find_all(class_ = 'itemlist')

state_links = []

for i in state_list:
    cont = i.contents[0]
    attr = cont.attrs
    hrefs = attr['href']
    state_links.append(hrefs)

# find all city links
city_links = []

for link in state_links:
    page = requests.get(link)
    soup = BeautifulSoup(page.text, 'html.parser')
    familydollar_list = soup.find_all(class_ = 'itemlist')
    for store in familydollar_list:
        cont = store.contents[0]
        attr = cont.attrs
        city_hrefs = attr['href']
        city_links.append(city_hrefs)
# to get individual store links
store_links = []

for link in city_links:
    locpage = requests.get(link)
    locsoup = BeautifulSoup(locpage.text, 'html.parser')
    locinfo = locsoup.find_all(type="application/ld+json")
    for i in locinfo:
        loccont = i.contents[0]
        locjson = json.loads(loccont)
        try:
            store_url = locjson['url'] 
            store_links.append(store_url)
        except:
            pass

# get address and geolocation information
stores = []

for store in store_links:
    storepage = requests.get(store)
    storesoup = BeautifulSoup(storepage.text, 'html.parser')
    storeinfo = storesoup.find_all(type="application/ld+json")
    for i in storeinfo:
        storecont = i.contents[0]
        storejson = json.loads(storecont)
        try:
            store_addr = storejson['address'] 
            store_addr.update(storejson['geo']) 
            stores.append(store_addr)
        except:
            pass

# final data parsing
stores_df = df.from_records(stores)
stores_df.drop(['@type', 'addressCountry'], axis = 1, inplace = True)
stores_df['Store'] = "Family Dollar"

df.to_csv(stores_df, "family_dollar_locations.csv", sep = ",", index = False)

--

作者注:本文改编自我于 2020 年 2 月 9 日在俄勒冈州波特兰市 PyCascades 上发表的演讲

接下来阅读
标签
User profile image.
博士后研究员,在作物改良计划和高等教育方面拥有经验。 擅长数量遗传学、统计学、应用遗传学、分子遗传学、细胞遗传学、植物育种、旱地农艺学和光谱学。 具有本科和研究生水平的教学经验。 R、SAS、C 和 Python 编程技能。

3 条评论

当我为另一个站点抓取 json 时,我得到 'b' 和随机字符。知道为什么吗?

这是一份很棒的指南,谢谢!我很感激您还想到了提及伦理方面的考虑——否则我可能不会想到要考虑这一点。

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.