抓取网络源码python
有很多很棒的书可以帮助您学习Python,但是谁真正读了这些A到Z? (剧透:不是我)。
接下来是我的第一个Python抓取项目指南。 假定的Python和HTML知识很少。 这旨在说明如何使用Python库请求访问网页内容以及如何使用BeatifulSoup4以及JSON和pandas解析内容。 我将简要介绍Selenium ,但是我不会深入研究如何使用该库-该主题应有自己的教程。 最终,我希望向您展示一些技巧和窍门,以减少网络抓取的麻烦。
安装我们的依赖
我的GitHub存储库中提供了本指南中的所有资源。 如果需要安装Python 3的帮助,请查看Linux , Windows和Mac的教程。
$ python3
-m venv
$
source venv
/ bin
/ activate
$ pip
install requests bs4 pandas
如果您喜欢使用JupyterLab,则可以使用此笔记本运行所有代码。 有很多安装JupyterLab的方法,这是其中一种:
# from the same virtual environment as above, run:
$ pip
install jupyterlab
为我们的网页抓取项目设定目标
现在我们已经安装了依赖项,但是抓取网页需要什么呢?
让我们退后一步,并确保阐明我们的目标。 这是我成功完成Web抓取项目的要求列表。
- 我们正在收集值得构建有效的Web刮板的信息。
- 我们正在下载可通过网络抓取工具合法地和道德地收集的信息。
- 我们了解如何在HTML代码中找到目标信息。
- 我们拥有正确的工具:在这种情况下,它是库BeautifulSoup和request 。
- 我们知道(或愿意学习)如何解析JSON对象。
- 我们有足够的数据技能可以使用熊猫 。
关于HTML的评论:尽管HTML是运行Internet的野兽,但我们最需要了解的是标签的工作方式。 标签是夹在尖括号括起来的标签之间的信息的集合。 例如,这是一个假装标签,称为“ 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文件未涉及道德收集和数据使用等主题。
在开始抓取项目之前,我问自己以下问题:
- 我要刮取受版权保护的材料吗?
- 我的抓取活动会损害个人隐私吗?
- 我是否发出大量可能导致服务器超载或损坏的请求?
- 报废是否会暴露我不拥有的知识产权?
- 是否有管理网站使用的服务条款,我是否遵循这些条款?
- 我的抓取活动会减少原始数据的价值吗? (例如,我是否打算按原样重新打包数据,或者从原始来源中窃取网站流量)?
当我抓取网站时,请确保我可以对所有这些问题回答“否”。
要更深入地了解法律问题,请参阅Krotov和Silva于2018年出版的Web Scraping的合法性和道德性 以及Sellars的二十年Web Scraping和《计算机欺诈与滥用法》 。
现在该抓东西了!
经过上述评估,我想到了一个项目。 我的目标是提取爱达荷州所有Family Dollar商店的地址。 这些商店在农村地区规模很大,因此我想了解在一个相当乡村的国家中有多少家商店。
起点是Family Dollar的位置页面 。
![爱达荷州家庭美元地点页面 Family Dollar Idaho locations page](https://i-blog.csdnimg.cn/blog_migrate/c2eec1b2667792e2e1841c5086a2199c.png)
首先,让我们在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 —解析的内容
- 标签 -一个标准HTML标签,您将遇到的bs4元素的主要类型
- NavigableString-标签内的文本字符串
- 注释 — NavigableString的一种特殊类型
当我们查看request.get()输出时,还有更多要考虑的问题。 我仅使用page.text()将请求的页面转换为可读的内容,但是还有其他输出类型:
- page.text()用于文本(最常见)
- page.content()用于逐字节输出
- JSON对象的page.json()
- page.raw()用于原始套接字响应(不,谢谢)
我只在使用拉丁字母的纯英语网站上工作。 为此, 请求中的默认编码设置可以正常工作。 但是,除了纯英语网站之外,还有一个丰富的互联网世界。 为确保请求正确解析内容,可以设置文本的编码:
page
= requests.
get
( URL
)
page.
encoding
=
'ISO-885901'
soup
= BeautifulSoup
( page.
text
,
'html.parser'
)
仔细研究BeautifulSoup标签,我们看到:
- bs4元素标签正在捕获HTML标签
- 它具有名称和属性,可以像字典一样访问: tag ['someAttribute']
- 如果标签具有多个具有相同名称的属性,则仅访问第一个实例。
- 可通过tag.contents访问标签的子级 。
- 可以使用tag.contents访问所有标记后代。
- 您始终可以使用re.compile(“ your_string”)作为字符串访问全部内容,而不用浏览HTML树。
确定如何提取相关内容
警告:此过程可能令人沮丧。
卷材刮削过程中的提取可能是一个艰巨的过程,其中充满了错误的步骤。 我认为解决此问题的最佳方法是从一个有代表性的示例开始,然后进行扩展(此原则适用于任何编程任务)。 查看页面HTML源代码至关重要。 有很多方法可以做到这一点。
您可以在终端中使用Python查看页面的整个源代码(不建议使用)。 运行此代码需要您自担风险:
print
( soup.
prettify
(
)
)
虽然打印出页面的整个源代码可能适用于某些教程中显示的玩具示例,但大多数现代网站的任何页面上都有大量内容。 甚至404页面也可能充满了页眉,页脚等代码。
通常最简单的方法是在您喜欢的浏览器中通过“ 查看页面源代码”浏览源代码(单击鼠标右键,然后选择“查看页面源代码”)。 这是找到目标内容的最可靠方法(稍后我将解释原因)。
![Family Dollar页面源代码 Family Dollar page source code](https://i-blog.csdnimg.cn/blog_migrate/b2abeba86ead63d240570355309d00d8.png)
在这种情况下,我需要在广阔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](https://i-blog.csdnimg.cn/blog_migrate/d898a3fbbfd90e6e6ceb8d5892eec7fb.png)
地址信息嵌套在type =“ application / ld + json”内 。 经过大量的地理位置抓取之后,我开始认识到这是用于存储地址信息的通用结构。 幸运的是, soup.find_all()还可启用对type的搜索。
arco
= soup2.
find_all
(
type
=
"application/ld+json"
)
print
( arco
[
1
]
)
地址信息在第二个列表成员中! 最后!
我使用.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
)
在该内容中,有一个称为地址的关键字,该关键字在较小的嵌套字典中具有所需的地址信息。 可以这样检索:
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数据框,删除不需要的列“ @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
)
我们做到了! 所有爱达荷州家庭美元商店都有一个逗号分隔的列表。 多么疯狂的旅程。
关于Selenium和数据抓取的几句话
Selenium是用于与网页自动交互的常用工具。 为了解释为什么有时必须使用它,让我们来看一个使用Walgreens网站的示例。 Inspect Element提供浏览器中显示的代码:
![Walgreens位置页面和代码 Walgreens location page and code](https://i-blog.csdnimg.cn/blog_migrate/8a2bbd1196f4c563d2bd5174061e1a1c.png)
虽然“ 查看页面源”提供了有关将获取哪些请求的代码:
![Walgreens源代码 Walgreens source code](https://i-blog.csdnimg.cn/blog_migrate/1c847aed04971eb169e1e5d24edc3aff.png)
如果这两个不同意,则有一些插件可以修改源代码,因此,应在将页面加载到浏览器后对其进行访问。 请求不能做到这一点,但是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,但是当呈现的内容与源代码不同时,我确实会保留Selenium。
结语
总之,使用网络抓取来完成有意义的任务时:
- 耐心一点
- 查阅手册(它们非常有帮助)
如果您对答案感到好奇:
![Family Dollar位置图 Family Dollar locations map](https://i-blog.csdnimg.cn/blog_migrate/3271b937c3fb9a0ca52ba1ee8fa8362d.png)
美国有许多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进行的演讲的改编。
翻译自: https://opensource.com/article/20/5/web-scraping-python
抓取网络源码python