整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

如何为你的机器学习项目抓取网页数据?

不久,我在 LearnML 子论坛上看到一篇帖子。楼主在这篇帖子中提到,他需要为自己的机器学习项目抓取网页数据。很多人在回帖中给出了自己的方法,主要是学习如何使用 BeautifulSoup 和 Selenium。

我曾在一些 数据科学项目中使用过 BeautifulSoup 和 Selenium。在本文中,我将告诉你如何用一些有用的数据抓取一个网页,并将其转换成 pandas 数据结构(DataFrame)。

为什么要将其转换成数据结构呢?这是因为大部分机器学习库都能处理 pandas 数据结构,并且只需少量修改就可对你的模型进行编辑。

首先,我们要在维基百科上找到一个表来转换成数据结构。我抓取的这张表,展示的是维基百科上浏览量最大的运动员数据。

其中一项大量的工作就是,通过浏览 HTML 树来得到我们需要的表。

通过 request 和 regex 库,我们开始使用 BeautifulSoup。

from bs4 import BeautifulSoup
import requests
import re
import pandas as pd

复制代码

下面,我们将从网页中提取 HTML 代码:

website_url = requests.get('https://en.wikipedia.org/wiki/Wikipedia:Multiyear_ranking_of_most_viewed_pages').text
soup = BeautifulSoup(website_url, 'lxml')
print(soup.prettify())
</a>
</li>
<li id="footer-places-disclaimer">
<a href="/wiki/Wikipedia:General_disclaimer" title="Wikipedia:General disclaimer">
Disclaimers
</a>
</li>
<li id="footer-places-contact">
<a href="//en.wikipedia.org/wiki/Wikipedia:Contact_us">
Contact Wikipedia
</a>
</li>
<li id="footer-places-mobileview">
<a class="noprint stopMobileRedirectTog

复制代码

从语料库中收集所有的表,我们有一个较小的表面区域来搜索。

wiki_tables = soup.find_all('table', class_='wikitable')
wiki_tables

复制代码

因为存在很多表,所以需要一种过滤它们的方法。

据我们所知,Cristiano Ronaldo(也就是葡萄牙足球运动员 C 罗)有一个锚标记,这可能在几个表中是独一无二的。

通过 Cristiano Ronaldo 文本,我们可以过滤那些被锚标记的表。此外,我们还发现一些包含这个锚标记的父元素。

links = []
for table in wiki_tables:
_table = table.find('a', string=re.compile('Cristiano Ronaldo'))
if not _table:
continue
print(_table)
_parent = _table.parent
print(_parent)
links.append(_parent)
<a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
<td style="text-align: left;"><a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
</td>
<a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
<td style="text-align: left;"><a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
</td>
<a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
<td style="text-align: left;"><a href="/wiki/Cristiano_Ronaldo" title="Cristiano Ronaldo">Cristiano Ronaldo</a>
</td>

复制代码

父元素只显示单元格。

这是一个带有浏览器 web 开发工具的单元格。

parent_lst = []
for anchor in links:
   _ = anchor.find_parents('tbody')
   print(_)
   parent_lst.append(_)

复制代码

利用 tbody,我们可以返回包含以前的锚标记的其他表。

为进一步过滤,我们可以在以下表中的不同标题进行搜索:

for i in parent_lst:
   print(i[0].find('tr'))
tr>
<th>Rank*</th>
<th>Page</th>
<th>Views in millions
</th></tr>
<tr>
<th>Rank</th>
<th>Page</th>
<th>Views in millions
</th></tr>
<tr>
<th>Rank</th>
<th>Page</th>
<th>Sport</th>
<th>Views in millions
</th></tr>

复制代码

第三张看起来很像我们所需要的表。

接下来,我们开始创建必要的逻辑来提取并清理我们需要的细节。

sports_table = parent_lst[2]


complete_row = []


for i in sports_table:
  rows = i.find_all('tr')
  print('\n--------row--------\n')
  print(rows)


  for row in rows:
    cells = row.find_all('td')
    print('\n-------cells--------\n')
    print(cells)


    if not cells:
      continue


    rank = cells[0].text.strip('\n')
    page_name = cells[1].find('a').text
    sport = cells[2].find('a').text
    views = cells[3].text.strip('\n')


    print('\n-------CLEAN--------\n')
    print(rank)
    print(page_name)
    print(sport)
    print(views)


    complete_row.append([rank, page_name, sport, views])
    
    
for i in complete_row:
  print(i)

复制代码

分解一下:

sports_table = parent_lst[2]
complete_row = []

复制代码

下面我们从上面的列表中选择第三个元素。这就是我们需要的表。

接下来创建一个空列表,用于存储每行的详细信息。在遍历这个表的时候,建立一个循环,遍历表中的每一行,并将其保存到 rows 变量中。

for i in sports_table:
  rows = i.find_all('tr')
  print('\n--------row--------\n')
  print(rows)

复制代码

for row in rows:
   cells = row.find_all('td')
   print('\n-------cells--------\n')
   print(cells)

复制代码

建立了嵌套的循环。遍历上一个循环中保存的每个行。在遍历这些单元格时,我们将每个单元格保存在一个新的变量。

if not cells:
  continue

复制代码

这段简短的代码允许我们在从单元格中提取文本时,避免空单元格并防止发生错误。

rank = cells[0].text.strip('\n')
page_name = cells[1].find('a').text
sport = cells[2].find('a').text
views = cells[3].text.strip('\n')

复制代码

在此,我们将各种单元格清理为纯文本格式。清除后的值保存在其列名下的变量中。

print('\n-------CLEAN--------\n')
print(rank)
print(page_name)
print(sport)
print(views)
complete_row.append([rank, page_name, sport, views])

复制代码

此处,我们向行列表添加这些值。然后输出清理后的值。

-------cells--------
[<td>13
</td>, <td style="text-align: left;"><a href="/wiki/Conor_McGregor" title="Conor McGregor">Conor McGregor</a>
</td>, <td><a href="/wiki/Mixed_martial_arts" title="Mixed martial arts">Mixed martial arts</a>
</td>, <td>43
</td>]
-------CLEAN--------
13
Conor McGregor
Mixed martial arts
43

复制代码

下面将其转换为数据结构:

headers = ['Rank', 'Name', 'Sport', 'Views Mil']
df = pd.DataFrame(complete_row, columns=headers)
df

复制代码

现在你可以在机器学习项目中使用的 pandas 数据结构了。你可以使用自己喜欢的库来拟合模型数据。

作者介绍:

Tobi Olabode,对技术感兴趣,目前主要关注机器学习。

原文链接:

https://blog.devgenius.io/how-to-scrape-a-website-for-your-ml-project-c3a4d6f160c7

常听别人说通过Python可以轻松实现一个小爬虫,甚至会有部分同学一听到爬虫就认为是Python,我竟无言以对。为了先从自己开始打破这种思想禁锢,花了一天时间查找资料了解网络方面知识,用C程序接收数据。程序框架借鉴于他人。做出来的只是Demo,欢迎批评指正。


  1. /*****************************************************************
  2. * Copyright (C) 2018 XZD. All rights reserved.
  3. *
  4. * File name: networm.c
  5. * Author: xiaozude
  6. * Version: 1.0.0
  7. * Data: 2018-07-21
  8. * Description: Linux平台下,用gcc编译后,通过程序+网址方式调用。
  9. *
  10. *****************************************************************/
  11. #include <stdbool.h>
  12. #include <stdio.h>
  13. #include <string.h>
  14. #include <netdb.h>
  15. #define MAX_STR_LEN 1024
  16. #define PORT 80
  17. /* 获取主机路径和网页路径 */
  18. _Bool GetUrlAndPath(const char * url, char * hostUrl, char * pagePath)
  19. {
  20. strcpy(hostUrl, url);
  21. //去除"https://"与"http://"
  22. if (0 == strncmp(hostUrl, "https://", 8))
  23. {
  24. int i = 8;
  25. do
  26. {
  27. hostUrl[i - 8] = hostUrl[i];
  28. }

文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。

作者: 王平

源自:猿人学Python

PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行获取

http://note.youdao.com/noteshare?id=3054cce4add8a909e784ad934f956cef

前一两年抓过某工商信息网站,几三周时间大约抓了过千万多万张页面。那时由于公司没啥经费,报销又拖得很久,不想花钱在很多机器和带宽上,所以当时花了较多精力研究如何让一台爬虫机器达到抓取极限。

本篇偏爬虫技术细节,先周知。

Python爬虫这两年貌似成为了一项必备技能,无论是搞技术的,做产品的,数据分析的,金融的,初创公司做冷启动的,都想去抓点数据回来玩玩。这里面绝大多数一共都只抓几万或几十万条数据,这个数量级其实大可不必写爬虫,使用 chrome 插件 web scraper 或者让 selenium 驱动 chrome 就好了,会为你节省很多分析网页结构或研究如何登陆的时间。

本篇只关注如何让爬虫的抓取性能最大化上,没有使用scrapy等爬虫框架,就是多线程+Python requests库搞定。

对一个网站定向抓取几十万张页面一般只用解决访问频率限制问题就好了。对机器内存,硬盘空间,URL去重,网络性能,抓取间隙时间调优一般都不会在意。如果要设计一个单台每天抓取上百万张网页,共有一亿张页面的网站时,访问频率限制问题就不是最棘手的问题了,上述每一项都要很好解决才行。硬盘存储,内存,网络性能等问题我们一项项来拆解。

一、优化硬盘存储

所以千万级网页的抓取是需要先设计的,先来做一个计算题。共要抓取一亿张页面,一般一张网页的大小是400KB左右,一亿张网页就是1亿X200KB=36TB 。这么大的存储需求,一般的电脑和硬盘都是没法存储的。所以肯定要对网页做压缩后存储,可以用zlib压缩,也可以用压缩率更好的bz2或pylzma 。

但是这样还不够,我们拿天眼查的网页来举例。天眼查一张公司详情页的大小是700KB 。

对这张网页zlib压缩后是100KB。

一亿个100KB(9TB)还是太大,要对网页特殊处理一下,可以把网页的头和尾都去掉,只要body部分再压缩。因为一张html页面里<head></head>和<footer></footer>大都是公共的头尾信息和js/css代码,对你以后做正文内容抽取不会影响(也可以以后做内容抽取时把头尾信息补回去就好)。

来看一下去掉头尾后的html页面大小是300KB,压缩后是47KB。

一亿张就是4T,差不多算是能接受了。京东上一个4T硬盘600多元。

二、优化内存,URL去重

再来说内存占用问题,做爬虫程序为了防止重复抓取URL,一般要把URL都加载进内存里,放在set()里面。拿天眼查的URL举例:

https://www.tianyancha.com/company/23402373

这个完整URL有44个字节,一亿个URL就是4G,一亿个URL就要占用4G内存,这还没有算存这一亿个URL需要的数据结构内存,还有待抓取URL,已抓取URL还保存在内存中的html等等消耗的内存。

所以这样直接用set()保存URL是不建议的,除非你的内存有十几个G。

一个取巧的办法是截断URL。只把URL:

https://www.tianyancha.com/company/23402373

的后缀:23402373放进set()里,23402373只占8个字节,一亿个URL占700多M内存。

但是如果你是用的野云主机,用来不断拨号用的非正规云主机,这700多M内存也是吃不消的,机器会非常卡。

就还需要想办法压缩URL的内存占用,可以使用BloomFilter算法,是一个很经典的算法,非常适用海量数据的排重过滤,占用极少的内存,查询效率也非常的高。它的原理是把一个字符串映射到一个bit上,刚才23402373占8个字节,现在只占用1个bit(1字节=8bit),内存节省了近64倍,以前700M内存,现在只需要10多M了。

BloomFilter调用也非常简单,当然需要先install 安装bloom_filter:

from bloom_filter import BloomFilter # 生成一个装1亿大小的
bloombloom = BloomFilter(max_elements=100000000, error_rate=0.1)
 # 向bloom添加URL bloom.add('https://www.tianyancha.com/company/23402373') #判断URL是否在bloombloom.__contains__('https://www.tianyancha.com/company/23402373')

不过奇怪,bloom里没有公有方法来判断URL是否重复,我用的contains()方法,也可能是我没用对,不过判重效果是一样的。

三、反抓取访问频率限制

单台机器,单个IP大家都明白,短时间内访问一个网站几十次后肯定会被屏蔽的。每个网站对IP的解封策略也不一样,有的1小时候后又能重新访问,有的要一天,有的要几个月去了。突破抓取频率限制有两种方式,一种是研究网站的反爬策略。有的网站不对列表页做频率控制,只对详情页控制。有的针对特定UA,referer,或者微信的H5页面的频率控制要弱很多。 另一种方式就是多IP抓取,多IP抓取又分IP代理池和adsl拨号两种,我这里说adsl拨号的方式,IP代理池相对于adsl来说,我觉得收费太贵了。要稳定大规模抓取肯定是要用付费的,一个月也就100多块钱。

adsl的特点是可以短时间内重新拨号切换IP,IP被禁止了重新拨号一下就可以了。这样你就可以开足马力疯狂抓取了,但是一天只有24小时合86400秒,要如何一天抓过百万网页,让网络性能最大化也是需要下一些功夫的,后面我再详说。

至于有哪些可以adsl拨号的野云主机,你在百度搜”vps adsl”,能选择的厂商很多的。大多宣称有百万级IP资源可拨号,我曾测试过一段时间,把每次拨号的IP记录下来,有真实二三十万IP的就算不错了。

选adsl的一个注意事项是,有的厂商拨号IP只能播出C段和D段IP,110(A段).132(B段).3(C段).2(D段),A和B段都不会变,靠C,D段IP高频次抓取对方网站,有可能对方网站把整个C/D段IP都封掉。

C/D段加一起255X255就是6万多个IP全都报废,所以要选拨号IP范围较宽的厂商。 你要问我哪家好,我也不知道,这些都是野云主机,质量和稳定性本就没那么好。只有多试一试,试的成本也不大,买一台玩玩一个月也就一百多元,还可以按天买。

上面我为什么说不用付费的IP代理池?

因为比adsl拨号贵很多,因为全速抓取时,一个反爬做得可以的网站10秒内就会封掉这个IP,所以10秒就要换一个IP,理想状况下一天86400秒,要换8640个IP。

如果用付费IP代理池的话,一个代理IP收费4分钱,8640个IP一天就要345元。 adsl拨号的主机一个月才100多元。

adsl拨号Python代码

怎么拨号厂商都会提供的,建议是用厂商提供的方式,这里只是示例:

windows下用os调用rasdial拨号:

import os # 拨号断开 
os.popen('rasdial 网络链接名称 /disconnect') # 拨号
os.popen('rasdial 网络链接名称 adsl账号 adsl密码')

linux下拨号:

import os # 拨号断开
code = os.system('ifdown 网络链接名称')# 拨号
code = os.system('ifup 网络链接名称')

四、网络性能,抓取技术细节调优

上面步骤做完了,每天能达到抓取五万网页的样子,要达到百万级规模,还需把网络性能和抓取技术细节调优。

1.调试开多少个线程,多长时间拨号切换IP一次最优。

每个网站对短时间内访问次数的屏蔽策略不一样,这需要实际测试,找出抓取效率最大化的时间点。先开一个线程,一直抓取到IP被屏蔽,记录下抓取耗时,总抓取次数,和成功抓取次数。 再开2个线程,重复上面步骤,记录抓取耗时,总的和成功的抓取次数。再开4个线程,重复上面步骤。整理成一个表格如下,下图是我抓天眼查时,统计抓取极限和细节调优的表格:

从上图比较可以看出,当有6个线程时,是比较好的情况。耗时6秒,成功抓取80-110次。虽然8个线程只耗时4秒,但是成功抓取次数已经在下降了。所以线程数可以设定为开6个。

开多少个线程调试出来了,那多久拨号一次呢?

从上面的图片看到,貌似每隔6秒拨号是一个不错的选择。可以这样做,但是我选了另一个度量单位,就是每总抓取120次就重新拨号。为什么这样选呢?从上图也能看到,基本抓到120次左右就会被屏蔽,每隔6秒拨号其实误差比较大,因为网络延迟等各种问题,导致6秒内可能抓100次,也可能抓120次。

2.requests请求优化

要优化requests.get(timeout=1.5)的超时时间,不设置超时的话,有可能get()请求会一直挂起等待。而且野云主机本身性能就不稳定,长时间不回请求很正常。如果要追求抓取效率,超时时间设置短一点,设置10秒超时完全没有意义。对于超时请求失败的,大不了以后再二次请求,也比设置10秒的抓取效率高很多。

3.优化adsl拨号等待时间

上面步骤已算把单台机器的抓取技术问题优化到一个高度了,还剩一个优化野云主机的问题。就是每次断开拨号后,要等待几秒钟再拨号,太短时间内再拨号有可能又拨到上一个IP,还有可能拨号失败,所以要等待6秒钟(测试值)。所以要把拨号代码改一下:

import os # 断开拨号
os.popen('rasdial 网络名称 /disconnect')
time.sleep(6) # 拨号 
os.popen('rasdial 网络名称 adsl账号名 adsl密码')

而且 os.popen(‘rasdial 网络名称 adsl账号名 adsl密码’) 拨号完成后,你还不能马上使用,那时外网还是不可用的,你需要检测一下外网是否联通。

我使用 ping 功能来检测外网连通性:

import os 
code = os.system('ping www.baidu.com')

code为0时表示联通,不为0时还要重新拨号。而ping也很耗时间的,一个ping命令会ping 4次,就要耗时4秒。

上面拨号等待6秒加上 ping 的4秒,消耗了10秒钟。上面猿人学Python说了,抓120次才用6秒,每拨号一次要消耗10秒,而且是每抓120次就要重拨号,想下这个时间太可惜了,每天8万多秒有一半时间都消耗在拨号上面了,但是也没办法。

当然好点的野云主机,除了上面说的IP范围的差异,就是拨号质量差异。好的拨号等待时间更短一点,拨号出错的概率要小一点。

通过上面我们可以轻松计算出一组抓取的耗时是6秒,拨号耗时10秒,总耗时16秒。一天86400秒,就是5400组抓取,上面说了一组抓取是120次。一天就可以抓取5400X120=64万张网页。

按照上述的设计就可以做到一天抓60多万张页面,如果你把adsl拨号耗时再优化一点,每次再节约2-3秒,就趋近于百万抓取量级了。

另外野云主机一个月才100多,很便宜,所以你可以再开一台adsl拨号主机,用两台一起抓取,一天就能抓一百多万张网页。几天时间就能镜像一个过千万网页的网站。