内容概要:爬取谷歌搜索结果“standard waveguid sizes”的全部网页链接,并在原码中找到需要的表格,再保存到本地。
1. 爬虫操作的基本原理
1.1. 爬虫需要完成的逻辑内容
爬虫从目标网页得到所需的信息需要经过以下三个步骤
- 从目标网页得到html源代码并下载到本地;
- 从源代码中找到自己需要部分的标签;
- 从标签中提取所需属性值或字符串。
1.2. html语言简单介绍
HTML称为超文本标记语言,是一种标识性的语言。它由一系列标签,以及各个标签的属性组成。Html语言最大的特点是标签可以层层嵌套,有一点类似于python中的父类与子类的关系。这样的形式有助于网页将各种各样的内容组织起来,用统一的界面进行展示。
1.3. 利用Python得到网页原码
Python获得网页原码的方法较多,这里介绍较为常用的两个包:urllib.request和requests。在使用这两种方法之前,需要得到目标网址的网页链接,即url。
1.3.1. urllib.request
urllib.request中的urlopen()方法可以向指定的url发送请求,并返回服务器响应的类文件对象,如:
response = urllib.request.urlopen("https://www.baidu.com")
服务器返回的类文件对象支持Python文件对象的操作方法,如read()方法读取文件全部内容,返回字符串:
html = response.read()
print(html)就可以看到目标网页的源代码。
1.3.2. requests
requests提供get()和post()两种方法向目标发送请求,其中get()方法执行效率较高,但由于请求数据放在url中,因此容易被捕获,不过对于安全性要求不高的请求,一般使用get()方法就可以了。如下:
Import requests
html = requests.get(‘https://www.baidu.com’)
get()和post()方法都返回一个类,其中包括一个属性为网页原码,以下代码即可查看:
print(html.text)
1.4. 从网页原码中提取所需信息
从网页原码中提取需要信息的方法有beautifulsoup、正则表达式、scrapy中的Xpath、selenium模拟浏览器操作等。但我在制作爬虫时没有尝试selenium的内容,这里不多做介绍。
1.4.1. 利用beautifulsoup提取所需信息
beautifulsoup可以实现方便的网页信息提取,通过将html原码包装成python对象、并且将标签属性作为对象中的值,再通过包装好的方法使得使用Python的人更容易理解,更加直观。beautifulsoup首先对html原码进行解析,得到beautifulsoup的对象soup:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc, 'html.parser')
这其中第二个参数为使用的解析器,有以下四个选项:
以下为beautifulsoup的简单使用方法。
使用prettify()方法显示整理后的网页原码:
print(soup.prettify())
显示某子标签p的属性’class’:
print(soup.p['class'])
查找所有’a’标签
print(soup.find_all('a'))
1.4.2. 利用正则表达式提取所需信息
正则表达式是处理字符串的强大工具,它可以通过模糊匹配来确定字符串内是否存在所需内容,并对其进行提取,主要有match()方法、search()方法和findall()方法。
- match()方法
match()方法从字符串头部开始匹配是否存在所需内容。如下,第二行代码将会返回SRE_Match对象,即一个关于匹配子串的包装类。通过group()方法可以显示提取出的字符串(第三行)。如果没有匹配,将会得到None(第四行)。
import re
match = re.match(r'www', 'www.baidu.com')
print(match.group())
re.match(r'baidu', 'www.baidu.com')
- search()方法
search()方法可以查找或者匹配任意位置的字符串。同样,通过group()方法可以得到提取出的字符串。
search = re.search(r'baidu', 'www.baidu.com')
print(search.group())
- findall()方法
findall()方法可以直接得到与指定规则相匹配的所有字符串,而不是SRE_Match对象。
print(re.findall(r'www','www.baidu.com,www.google.com')
- 正则表达式的模式
以上方法中的第一个参数为正则表达式的模式,既可以为确定的字符串,也可以使用模糊匹配。以下为一些模糊匹配的实例
以下为具体案例:
string = "Cats are smarter than dogs"
result = re.match( r'(.*) are (.*?) .*', string)
其中,正则表达式为r’(.) are (.?) .’。第一个r表示在编译这一段字符串时不要将其中的一些符号进行转译,(.)表示可以匹配除了换行符之外的所有字符。(.*?)表示只匹配符合条件的字符串中的最少字符。.*效果与加括号一致,但不在最终结果中显示它匹配的字符。
print(result.group())
print(result.group(1))
print(result.group(2))
上述第一行显示完整的匹配结果。第二行显示(.)的结果,(.?)的结果在第三行中显示。
1.4.3. 利用scrapy中的Xpath提取所需信息
scrapy框架中返回的response类可以直接使用Xpath方法提取需要的信息,在查找所需标签及相关属性上具有明显优势。示例如下:
response.xpath(‘//table’)
上述方法可以得到网页原码中所有table标签及里面的内容。由于”//”表示多级定位,即使table不是原码中的第一级标签也可以被捕获。
response.xpath(‘//table/tbody/tr[@href=”https.//www.everythingf.com”]’)
上述方法表示查找table标签下的tbody子标签下,所有tr子标签中第一个属性href为”https.//www.everythingf.com”的标签。
response.xpath(‘//table/tbody/tr[@href=”https.//www.everythingf.com”]/text()’).getall()
在上述代码中加入getall()方法可以提取所有满足条件的子标签,同时,//text()表示得到该tr标签下所有子标签的text。
1.4.4. 从网页中提取一个表格
一般html代码中,表格总是用标签table表示的,table中一般含有一个或两个标签,其中一个是tbody,代表表格内容;另一个命名各式各样,但都代表表头信息。对于一般的简单表格(如下图),表头也会被放在tbody中,因此只要爬取tbody就可以了。我们可以看到,tbody中全部都是名称为tr的标签,代表表格中的每一行;tr中为td标签,每一td存放一列。因此,最简单的爬取表格代码的流程为:
table = []
trs = find_all('tr')
for tr in trs:
list = []
for td in tr:
list.append(td.string)
table.append(list)
对于“table”中含有表头标签的表格,由于表头中的元素占据的格数可能不为1,因此需要识别其中每个元素的“colspan”和“linespan”属性,分别代表其占据的列数和行数。由于python实现二维指针链表较为困难,而表格中元素长度不等,因此不能使用矩阵的方法进行组织,只能使用list,也给程序设计带来了一定麻烦。对于占多行的元素的情况,无法直接在判断“colspan”之后给下一行赋值,需要等到下一次循环的时候判断当前已经赋值的行数,在到达对应行数时将上一行的内容复制到下一行,因此程序设计为:
table_matrix = []
temp_occupy_infor = []#存储元素占据多列时的情况
present_column = 0#当前行中赋值到的列数
for sub_table in table:#有的表格可能有多个子标题,不止tbody
for tr in sub_table:#每一行
line = []#存储每一行元素
for td in tr:
flag_if_occupied,temp_occupy_infor = if_occupied(present_column,temp_occupy_infor)#判断当前位置是否为占据多列的元素
while flag_if_occupied:#给已经预定的位置
line.append(table_matrix[-1][present_column])
present_column += 1
flag_if_occupied,temp_occupy_infor = if_occupied(present_column,temp_occupy_infor)
#解决多个相邻元素都是占据多列的元素的问题
#判断不是多列元素后,赋值新元素
if ('colspan' in str(td)) and (type(td.get('colspan'))!=None):#判断新元素占几列
td_column = int(td.get('colspan'))
for i in range(td_column):#表格标题占多列的情况
line.append(td.text)
present_column += 1
else:
line.append(td.text)
present_column += 1
#写入新的占位信息
if 'linespan' in str(td):
td_line = int(td.get('linespan'))
temp_occupy_infor.append([present_column-1,td_line-1]) #末尾有需要复制的值
flag_if_occupied,temp_occupy_infor = self.if_occupied(present_column,temp_occupy_infor)
while flag_if_occupied:#给已经预定的位置赋值
line.append(table_matrix[-1][present_column])
present_column += 1
flag_if_occupied,temp_occupy_infor = if_occupied(present_column,temp_occupy_infor)
table_matrix.append(line)
present_column = 0
save_contents(table_matrix)
1.2. 爬谷歌需要完成的一些设置
由于谷歌具有一些反爬虫机制,因此需要修改以下设置:
1.2.1. 设置user-agent
由于python直接访问服务器时user-agent为python -urllib/3.5,直接访问google会被拒绝,因此需要设置类似于浏览器的user-agent。同时,同一个user-agent频繁访问网页时会出现timeouterror(10060)的错误,网站拒绝被访问,因此需要设置一个user-agent库,并进行随机调取。
1.2.2. 设置proxy
代理ip可以在频繁访问网页时帮助网页规避封ip的风险,防止出现403的错误。一般情况下,在爬取大量数据时需要维护自己的ip库,但由于网上免费ip可用率较低,我在尝试十几个ip之后才找到一个可用的,由于时间比较紧,通过测试程序获得ip的方法也没有尝试,因此只使用了一个可用的代理ip。
1.2.3. 设置爬虫延时
设置延时可以保证爬虫以时间换数量,不会在获得较少数据时就被网站限制访问。同时,如果使用了异步框架,可以设置爬虫程序限制同时爬取网站的数量。
2. 利用scrapy框架完成google搜索结果中所需样本的提取
2.1. 介绍scrapy框架
scrapy框架是纯Python实现的专门用于各类爬虫的异步框架,由于各个模块分别运行,提升了系统的稳定性和爬取效率,同时由于使用者众多,资料易得,报错易改,因此成为此次爬取工作的不二之选。
上图是scrapy框架的整体架构,它将爬虫分为pipeline、spiders、scheduler、downloader以及engine五个模块,其中engine负责各部分数据传输和整体运行;scheduler负责存储将要爬取的网页,并依次向downloader提交请求;downloader负责向服务器发送访问请求,并获得相关网页的返回信息,包括网页原码等内容;spiders负责处理网页的返回信息,并将需要保存或做进一步处理的信息包装成元组发送给pipeline,同时将新发现的需要爬取的网页请求发送给scheduler;pipeline是所的数据的最后一道处理工序。
2.2. prase内容实现
prase是spiders的主要内容,我将来自google网页的原码处理放在prase中,可以向scheduler发送进一步爬取的请求。由于google的搜索结果页面十分干净,没有广告,因此来自google的原码可以通过beautifulsoup进行处理,只需要提取其中所有href属性,并滤掉google自身的网页即可。
2.3. pipeline内容实现
pipeline中实现的内容与上述得到表格的逻辑内容基本一致,但由于不同网页的开发者在网页设计的时候会加入个人习惯,添加许多看不见但存在于代码里的空标签和只有空格、tab等内容的标签。因此需要加入对tr、td、table等标签的审查,丢弃不需要的内容。
对于表格是否为含有标准波导尺寸的表格,根据已经获得的表格中各个表格的特点,发现非波导尺寸的表格有以下几类:
- 网站链接的广告表格,特点是表格中所有元素都含有链接;
- 描述具体波导器件的表格,特点是多行两列;
- 非标准表格,特点是table标签下的标签数量大于2;
根据以上内容,可以采用以下三项得到较好的爬取结果: - 网页链接数量小于表格元素数量的一半;
- 表格行数大于5,列数大于4;
- table标签的子标签数量小于等于2;
保存为csv文件时需要注意替换该格式文件不允许的字符,如’\xa0’。
2.4. Item内容实现
item中定义了从spiders传入pipeline的数据结构,即class item,定义如下:
class CrawlerGoogle0730Item(scrapy.Item):
google_page = scrapy.Field()
result_line = scrapy.Field()
body = scrapy.Field()
url = scrapy.Field()
2.5.middleware内容实现
中间件分为蜘蛛中间件和下载中间件,在下载中间件中我放入了user-agent的随机调取函数,实现每一次访问使用不同的user-agent:
import random
class RandomUserAgentMiddleware(object):#随机生成user_agent
def __init__(self, user_agent):
print("use RandomUserAgentMiddleware.__init__")
self.user_agent = user_agent
@classmethod
def from_crawler(cls, crawler):
print("use RandomUserAgentMiddleware.from_crawler")
return cls(user_agent=crawler.settings.get('MY_USER_AGENT'))
def process_request(self, request, spider):
agent = random.choice(self.user_agent)
request.headers['User-Agent'] = agent
2.6. setting内容实现
setting中定义了user-agent库、中间件函数接口及优先级、网站访问延迟时间、同时访问数量等。
3.出现的问题及解决方法
3.1. 典型错误:timeouterror
造成timeouterror的原因有很多,在通过浏览器可以访问的情况下,绝大多数情况都是爬虫被对方发现而导致的主动延迟或不允许访问。而被对方发现的原因无非以上三种:没有设置proxy、user-agent中有python的信息或没有随机抽取、爬虫访问频率过高。通过1.5中介绍的内容可以进行规避。
3.2. 典型错误:无法找到网页
由于google下载的原码与开发者工具中看到的不符,因此从a标签的href属性获取的网页是无法直接访问的。通过观察下载的原码,可以看到a标签中href属性除了其本身的内容,还有ping属性中的一部分,因此可以采用正则表达式或Python的字符串搜索函数进行提取。
3.3. 典型错误:爬取的google原码中无法找到下一页的链接
这个问题还是由于google的原码与开发者工具中看到的不一致而产生的。在这个过程中,我尝试使用精度更高的beautifulsoup编译器、scrapy的Xpath方法、正则表达式等进行寻找,但都没有结果。经过多次尝试,我发现各页搜索结果的超链接可以直接以str_1+str(google_page*10)+str_2的形式进行表示,因此解决了这个问题。
3.4. 典型错误:yield 返回后报IndentationError: unexpected indent
这个错误是由scrapy库产生的。由于python本身对缩进要求较高,而prase是异步调用,因此可能会产生将prase()函数中的yield换作return就不报错、使用yield就报错的情况。在查阅国内论坛无果后,我在github的论坛中找到了解决办法:将prase()函数中的所有整行的注释全部删除,问题就解决了。
3.4.典型错误描述