vlambda博客
学习文章列表

如何创建一个可复用的网页爬虫

本文翻译自:How to Create a Reusable Web Scraper

网页爬虫是个非常有趣的玩具。不过不好玩的是,我们需要根据不同网页上的元素不断的调整自己的代码。这就是为什么我要着手实现一个更好的网页爬虫项目——通过该项目可以以最少的更改实现对新网页的爬取。

第一步是将网页爬虫按照逻辑分成每个独立的部分:

  1. 页面请求器
  2. 页面验证器
  3. 模板页面处理器

页面请求器

页面请求器的实现有一些技巧。下载网页时要考虑很多因素。你需要确保你可以随机的使用用户代理,并且不要过于频繁地从同一域中请求。

此外,停下手头的工作去分析为什么网页无法下载是一件出力不讨好的事。尤其是当你的爬虫已经在多个站点运行了好几个小时的情况下。因此,我们会处理一些请求,并将它们保存为文件。

将请求保存到文件中还有另外一个好处。你不必担心一个标签的消失会影响到你的爬虫。如果页面处理器是独立的,并且你已经完成了页面的下载,你还可以根据需要快速且频繁的对其进行处理。如果发现有另一个要抓取的数据元素怎么办?别担心。只需添加一个标签,然后在你已下载的页面上重新运行处理器即可。

以下是一些实际情况下的示例代码:

import requestsimport picklefrom random import randintfrom urllib.parse import urlparsedef _random_ua(): ua_list = ["user agent1, user agent2", "user agent3"] random_num = randint(0, len(ua_list)) return ua_list[random_num]def _headers(): return { 'user-agent': _random_ua() }def _save_page(response): uri = urlparse(response.url) filename = uri.netloc + ".pickle" with open(filename, 'wb+') as pickle_file: pickle.dump(response, pickle_file)def download_page(url): response = requests.get(url) _save_page(request)

页面验证器

照片来自 Medium.

页面验证器浏览文件并释放请求。它将读取请求的状态码,如果请求代码类似于 408(超时),你可以让它重新排队下载网页。否则,验证器会将文件移动到实际的 web 抓取模块中进行处理。

你还可以收集为什么页面没有下载的数据。也许你请求页面的速度太快而被禁止了。此数据可用于调整你的页面下载器,以便它可以运行尽可能快且错误量最小。

模板页面处理器

终于到这里了。我们要做的第一步是创建数据模型。让我们从 URL 开始,对于每个不同的站点/路径,可能都有不同的提取数据的方法。我们从一个字典开始,就像这样:

models = { 'finance.yahoo.com':{}, 'news.yahoo.com'{}, 'bloomberg.com':{}}

在我们的用例中,我们想要提取这些网站的 article 内容。要做到这一点,我们需要创建一个选择器,用于包含所有数据的最小外部元素。举个例子,下面是 finance.yahoo.com 的示例页面:

Webpage Sample<div> <a>some link</a> <p>some content</p> <article class="canvas-body"> <h1>Heading</h1> <p>article paragraph 1</p> <p class="ad">Ad Link</p> <p>article paragraph 2</p> <li>list element</li> <li> <a>unrelated link</a> </li> </article></div>

在上面的代码段中,我们希望定位 article 元素。因此,我们将使用 article 标签和 class 作为标识符,因为这是包含 article 内容的最小元素。

models = { 'finance.yahoo.com':{ 'root-element':[ 'article', {'class': "canvas-body"} ] }, 'news.yahoo.com'{}, 'bloomberg.com':{}}

接下来,我们将确定本文中的哪些元素是无用的。我们可以看到一个有 ad 类(值得注意的是,在真实场景中它永远不会这么简单)。因此,为了删除指定的元素,我们将在配置模型中创建一个 unwanted_elements 元素:

models = { 'finance.yahoo.com':{ 'root-element':[ 'article', {'class': "canvas-body"} ], 'unwanted_elements': [ 'p', {'class': "ad"} ] }, 'news.yahoo.com'{}, 'bloomberg.com':{}}

现在我们已经删除了一些无用元素,我们还要注意哪些元素需要保留。因为我们只寻找 article 元素,所以我们只需要指定保留 ph1 元素即可:

models = { 'finance.yahoo.com':{ 'root-element':[ 'article', {'class': "canvas-body"} ], 'unwanted_elements': [ 'p', {'class': "ad"} ] 'text_elements': [ ['p'], ['li'] ] }, 'news.yahoo.com'{}, 'bloomberg.com':{}}

现在是最后一部分——主聚合器!这里我将不关注配置文件的解析和加载。如果我把所有代码都放上来,这一篇文章不足以全部介绍完。

# 获取外部元素def outer_element(page, identifier): root = page.find(*identifier) if root == None: raise Exception("Could not find root element") return root

# 移除不需要的元素def trim_unwanted(page, identifier_list): # 判断 list 中是否有该元素 if len(identifier_list) != 0: for identifier in identifier_list: for element in page.find_all(*identifier): element.decompose() return page

# 提取文字def get_text(page, identifier_list): # 判断 list 中是否有该元素 if len(identifier_list) == 0: raise Exception("Need text elements") page_text = []
for identifier in identifier_list: for element in page.find_all(*identifier): page_text.append(element.text) return page_text

# 获取页面配置def load_scrape_config(): '''加载页面爬取配置数据''' return get_scrape_config()

# 获取站点的抓取配置def get_site_config(url): '''获取站点的抓取配置''' domain = extract_domain(url) config_data = load_scrape_config() config = config_data.get(domain, None) if config == None: raise Exception(f"Config does not exist for: {domain}") return config

# 构建返回字段def page_processer(request): '''返回文本''' # 获取站点的抓取配置 site_config = get_site_config(request.url)
# 解析页面 soup = BeautifulSoup(request.text, 'lxml')
# 获取根元素 root = outer_element(soup, site_config["root_element"]) # 移除不需要的元素 trimmed_tree = trim_unwanted(root, site_config["unwanted"]) # 获得所需的元素 text = get_text(trimmed_tree, site_config["text_elements"]) return " ".join(text)

总结

使用此代码,你可以创建一个模板,从任何网站提取文章文本。你可以在我的 GitHub 上看到完整的代码并查看我是如何实现它的。