平时比较忙,事情多而杂乱,没有时间和大家分享一些技术问题。因为项目需要,最近在写爬虫代码,因此分享一些相关内容给大家。这包括了爬虫的部分插件、翻页方法、爬虫与数据结构的关系、json读写、虚拟浏览器、IP代理池等常见问题。我们将本篇文章分为两部分来写,分别是静态爬虫和动态加载爬虫。
静态爬虫
(一)首先我介绍下静态爬虫常用的插件,selenium、bs4、lxml及其使用方式。
(1)selenium:
Selenium是一个用于Web应用程序自动化测试工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等。在爬虫程序中,selenium主要用来设置浏览器及其相关操作。比如,我们使用它来设置火狐浏览器,并模拟点击爱企查搜索:
from selenium import webdriver
# 设置无头浏览器
options = webdriver.FirefoxOptions()
options.add_argument('--headless')
driver = webdriver.Firefox(options=options)
# 设置最长请求等待时间
driver.implicitly_wait(30)
# 锁定网站,以便执行后续步骤
driver.get("https://aiqicha.baidu.com/")
# 获取爱企查的搜索框
input = driver.find_element_by_id("aqc-search-input")
# 获取搜索时的点击按钮
button = driver.find_element_by_xpath('//button[@class="search-btn"]')
# 清空搜索框的默认内容
input.clear()
# 输入搜索内容eve_name
input.send_keys(eve_name)
# 点击搜索按钮
button.click() # 或者模拟回车 input.send_keys(Keys.RETURN)
sleep(2)
# 查询后获得的新网页地址
new_url = driver.current_url
在webdriver中除了使用id、path之外,还可以使用文本信息直接查找字符串地址:
# more_tag = driver.find_element_by_link_text('更多品牌...')
# more = driver.find_element_by_partial_link_text('更多品牌')
在使用selenium模拟浏览器时,最常见的错误是python selenium模块使用出错,即:Message: 'geckodriver' executable needs to be in PATH。这是因为你的浏览器驱动没有添加到合适位置,或者其环境地址没设置。此时,我们的解决方案参见该博客。
如果我们频繁的使用driver.get(url),可能会触发某些网站的反爬机制(比如爱企查)。那么,这种情况下,我们该怎么办呢?其实,我们可以调用IP代理池来赋予options参数(这里,我使用的亿牛云代理):
# 代理服务器(产品官网 www.16yun.cn)
self.proxyHost = "u6226.5.tp.16yun.cn"
self.proxyPort = "6445"
# 代理验证信息
self.proxyUser = "16JNVJKT"
self.proxyPass = "235938"
self.proxyMeta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % {
"host": self.proxyHost,
"port": self.proxyPort,
"user": self.proxyUser,
"pass": self.proxyPass,
}
self.user_agents = []
content = open('user-agent.txt')
for line in content:
if line.strip() != '':
self.user_agents.append(line.strip())
options = webdriver.FirefoxOptions()
user_agent = random.choice(self.user_agents)
# 设置IP切换头
tunnel = random.randint(1, 10000)
headers = {"Proxy-Tunnel": str(tunnel), 'Upgrade-Insecure-Requests': '1', 'User-Agent': user_agent}
# 设置无头浏览器
options.add_argument('--headless')
options.add_argument('--headers = ' + str(headers))# 添加头信息,和requests.get()中的headers参数一样
options.add_argument('--proxy-server = ' + self.proxyMeta)# 和requests.get()中的proxies参数一样
driver = webdriver.Firefox(options=options)
# 设置最长请求等待时间
driver.implicitly_wait(90)
(2)bs4
BS4全称是Beautiful Soup,它提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱,通过解析文档为Beautiful Soup自动将输入文档转换为Unicode编码,输出文档转换为utf-8编码。bs4中有4个典型的对象:
Tag对象:是html中的一个标签,用BeautifulSoup就能解析出来Tag的具体内容,具体的格式为‘soup.name‘,其中name是html下的标签;
BeautifulSoup对象:整个html文本对象,可当作Tag对象;
NavigableString对象:标签内的文本对象;
Comment对象:是一个特殊的NavigableString对象,如果html标签内存在注释,那么它可以过滤掉注释符号保留注释文本。
在爬虫过程中,我最常用的是BeautifulSoup,只需根据下载的网页内容和解析参数'html.parser'来声明一个对象,接下来即可以根据该对象来使用find()、find_all()等函数寻找网页中的元素位置及其内容。
# soup = BeautifulSoup(html_cont, 'html.parser', from_encoding='utf-8')
soup = BeautifulSoup(html_cont, 'html.parser')
# 获取该网页的文本内容(企业名字)
org_name = soup.find('h2',class_ = 'name').get_text()
# 下述a、h3和div是标签名,class和id是标签内属性。当然,还有别的形式和内容。
company_infor = soup.find('h3', class_="title")
branch_infor = soup.find('div', id = "basic-branch")
org_list = bs_parser.find_all('a', class_ = "ellipsis-line-2")
# 值得注意的是,此处的org_list是一个列表
for eve_org in org_list:
org_website = eve_org.get('href')
(3)lxml 是 一个HTML/XML的解析器,主要的功能是如何发现、解析和提取 HTML/XML 数据。网络上已经有不错的文章来对其说明,本文不再赘述。参考文献。
from lxml import etree
# 声明一个lxml的对象
html_lxml = etree.HTML(html_cont)
# 根据位置找到相关元素
org_trade = html_lxml.xpath('//*[@id="basic-business"]/table/tbody/tr[3]/td[4]')
(二)很多网站不会一次性展示全部网页内容,多数需要拓展、翻页或跳转,这时就需要我们在程序中找到相应的位置并进行点击。
比如,使用driver = webdriver.Firefox(options=options),利用其函数find_element_by_xpath来查询元素位置,并接下来实现点击:
driver.find_element_by_xpath('//div[@class="brand-muti-more"]//a[@class="J_ViewMore view-more"]').click()
这里的find_element_by_XXX用法可以参考,参数可以参考。该函数组是html网页解析中一个非常重要的组成成分。
有些时候,我们根据位置或内容查询网页中的某个元素,但是并不一定都能查到。这时,我们可以参考使用is_displayed()函数,即:
more = driver.find_element_by_xpath('//li[@style="z-index:5;"]//span[@class="opt"]/a[last()]')
if more.is_displayed() and more.text == '更多':
more.click()
关于is_displayed()函数的更多内容,请参考。
(三)很多童鞋,特别是后来转行到计算机类的同行,认为学习python程序语言和爬虫的基本框架就好了,刚开始认识不到数据结构的重要性。还有很多同行,工作多年,发现数据结构的知识都用在面试了。这里,我简要举例分析一下数据结构与爬虫的关系。以中关村在线为例,如果我想获取里面的每件产品以及其所属的类别、品牌等信息,我该怎么做呢?下面图片给出了设计思路。

我们实际上是以主网站为树根,然后层次遍历树的每一层。在遍历当前层时,将其子节点添加到队列中,以便进行下一层的遍历。所以,这个爬虫设计是一个典型的树结构。
动态爬虫
(一)动态网页,即指只有网页在加载时内容才会出现。这种网页一般是写好了网页格式,所有同级网页的静态信息都是相同的,只是在加载时将动态信息填入到格式空隙中。这里,我们以某网站为例:

进入网址后,按F12可见此图。如图所示,1表示动态加载面,2是你需要的内容形式,3是你需要的某个内容,4是内容预览,5是具体内容(一般是json形式)。我们要爬取的信息就是5,爬下来的结果就是json文件。也许有同学要问:网址还是我输入的网址吗?如果不是,那么是哪个网址?我该怎么构造呢?这里,我们用下图给出了解释。

网址就是Request URL,请求方式就是Request Method。
(2)上一步骤之后,我们得到的是网页的json信息。接下来我会讲解一下json的读取,因为这不光关系到网页的分析,而且很多文件的读写都是以json为主,学习json会非常有用。网上有不少优秀的内容,我们确实可以参考一下,比如内容。如果我们直接读取json文件,那么读出来的内容类型是字典,此时我们可以使用参考文献中的方法查询我们需要的内容,查询结果是list类型:
with codecs.open("yangqi.txt", 'r', encoding='utf8') as read_file:
# json读出来的是dict type
ent_infor = json.load(read_file)
# jsonpath读取公司列表
name_list = jsonpath.jsonpath(ent_infor, '$..name')
print("读取json文件中的企业信息")
print(name_list)
read_file.close()
由于json的反馈结果是list类型,因此如果需要查询当中的信息,需要添加索引号。
attr_dict = dict()
entType = jsonpath.jsonpath(json_cont, '$..basicData.entType')
if entType:
attr_dict['企业类型'] = entType[0]
else:
attr_dict['企业类型'] = '-'
除了json.load()读取文件内容成为字典类型。我们常用的还包括了json.dump(),json.loads()(将str转化成dict格式),json.dumps()(将dict转化成str格式),Ref1,Ref2。其中,json.dump是把字典内容写入到json文件中:
# 每一层写入json
wrt_file = codecs.open('All_data', 'w', encoding='utf-8')
json.dump(self.json_dict, wrt_file, ensure_ascii=False, indent=4) #这里的indent和ensure_ascii可以查一下资料
wrt_file.close()
其他
(一)代理:
因为有些网站反爬,为了保证我们的爬虫效率,有些时候需要代理池。这里,我用的亿牛云代理,效果还不错。分享给大家相关代码:
class GetRequests(object):
def __init__(self):
# 初始化对象
self.user_agents = []
self.read_uas()
def get_requests(self, url):
# XXXX是购买后代理商提供的信息
# 代理服务器(产品官网 www.16yun.cn)
proxyHost = "XXXX"
proxyPort = "XXXX"
# 代理验证信息
proxyUser = "XXXXXX"
proxyPass = "XXXXXX"
proxyMeta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % {
"host": proxyHost,
"port": proxyPort,
"user": proxyUser,
"pass": proxyPass,
}
cookies = {
}
# 设置 http和https访问都是用HTTP代理
proxies = {
"http": proxyMeta,
"https": proxyMeta,
}
# 尝试重连3次
request_iter = 0
while request_iter < 3:
try:
# 随机选择ua
user_agent = random.choice(self.user_agents)
# 设置IP切换头
tunnel = random.randint(1, 10000)
headers = {"Proxy-Tunnel": str(tunnel), 'Upgrade-Insecure-Requests': '1', 'User-Agent': user_agent,
"Referer": url}
resp = requests.get(url, proxies=proxies, headers=headers, timeout=30, cookies=cookies)
if resp.status_code == 200:
text_data = resp.text
resp.encoding = 'utf-8'
# 网站反馈访问频繁,我会重新试几下
elif resp.status_code == 429 or resp.status_code == 407:
request_iter = request_iter + 1
sleep(1)
continue
else:
print("The status_code is error")
resp.close()
break
except Exception as e:
# 出现异常,我也会多试几下
request_iter = request_iter + 1
sleep(1)
# 要么返回正常内容,要么返回异常(变量未定义,但是会在上一级调用函数处理该异常)
return text_data
def read_uas(self):
# user-agent.txt是user-agent的文件,包含很多
content = open('user-agent.txt')
for line in content:
self.user_agents.append(line.strip())
(二)防止程序异常中断,包括断网、异常奔溃,断电等。我们可以采用ping的方式来测试是否断网(win下ping不需要参数,linux下需要参数),比如:
os.system('chcp 65001')
# win环境下
# exit_code = os.system('ping www.baidu.com')
# Linux环境下
exit_code = os.system('ping -c 4 www.baidu.com')
if exit_code:
# 断网了
print("Connect failed in download_requests.")
return 'Fail'
else:
# 没断网
print("Wrong html_content in download_requests !")
return None
在程序频繁访问网址的情况下,我们其实可以将没跑出来的网址重新加入到网址序列,方便等待后重新跑。这里,我就设置了重跑7次。但程序断网、断网、异常崩溃,我们需要及时的把结果以及没运行的网址储存到文本中,方便后面从中断处继续运行程序。
for layer in range(self.init_layer, 4):
print("开始第%s层的数据解析" % layer)
lower_pid_list = []
if layer == self.init_layer:
lower_pid_list = self.low_pid_list*1
net_discon = False
# 设置每层的每个pid最多可以请求7次
pid_req_times = dict()
for each_org_pid in upper_pid_list:
pid_req_times[each_org_pid] = 7
while len(upper_pid_list) > 0:
org_pid = upper_pid_list[0]
# 移除已被解析的公司url(无论是否解析成功)
upper_pid_list.remove(org_pid)
# HTML下载器下载网页全部内容
json_content = self.jsondownload.download(org_pid)
if json_content is None:
pid_req_times[org_pid] = pid_req_times[org_pid] - 1
if pid_req_times[org_pid] > 0:
upper_pid_list.append(org_pid)
else:
print("The html_page is wrong surely !")
elif json_content is 'Fail':
# 只有断网时(请求主页时断网),才能重新加入该pid
upper_pid_list.append(org_pid)
net_discon = True
break
else:
# 解析该公司的内容,并返回其子公司pid
ret = self.jsonparser.parser_main_web(json_content, org_pid, self.json_dict)
if ret is 'Fail':
# 只有断网时(点击下一页时断网),才能重新加入该pid
upper_pid_list.append(org_pid)
net_discon = True
break
else:
suborgs = ret*1
suborgs = list(set(suborgs))
lower_pid_list.extend(suborgs)
lower_pid_list = list(set(lower_pid_list))
self.count = self.count + 1
# 每一层写入json
wrt_file = codecs.open('All_data', 'w', encoding='utf-8')
json.dump(self.json_dict, wrt_file, ensure_ascii=False, indent=4)
wrt_file.close()
if len(upper_pid_list) == 0:
print("第%s层的数据解析结束" % layer)
if layer == 3:
print("全部处理完毕")
break
# 刷新记录list,防止程序中间出现问题
upper_layer_num = open("upper_list", 'w')
upper_layer_num.write(str(layer) + '\n')
upper_layer_num.close()
upper_layer_list = open('upper_list', 'a+')
upper_layer_list.write(str(upper_pid_list))
upper_layer_list.close()
if layer < 3:
lower_layer_list = open("lower_list", 'w')
lower_layer_list.write(str(lower_pid_list))
lower_layer_list.close()
if net_discon:
break
if layer < 3:
# 不会出现断网的情况下,将解析出的下一层作为新的上一层
upper_pid_list = lower_pid_list * 1
# 解析完成后(无论是否正常解析),输出数量
print('重启后获得了%s家公司的信息' % self.count)
except Exception as e:
print(str(e))
print("crawler failed")
(三)动态网页中的下一页:
动态网页由于在页面中无法查找下一页的网址内容及其位置,我们只好在加载的当前网页中搜寻数据的总页码,并把当前页码与总页码做对比,并持续+1执行。
hold_pid_list, hold_page, hold_pagesize, hold_pagecount\
= self.parser_table(json_cont, 'holds', 'first', holds_dict)
sub_orgs.extend(hold_pid_list)
while hold_page < hold_pagecount:
next_hold_json_content = self.nextjson.click_next('holds', hold_page + 1, hold_pagesize, pid)
if next_hold_json_content is 'Fail':
return 'Fail'
elif next_hold_json_content is not None:
hold_pid_list, hold_page, hold_pagesize, hold_pagecount\
= self.parser_table(next_hold_json_content, 'holds', 'next', holds_dict)
sub_orgs.extend(hold_pid_list)
(四)由于部分网页拿下来的json文件内容不是严格规范,有时候需要很多方式把字符串信息转成字典信息。这包括json.load(),eval(),yaml.load():
data = yaml.load(text_data, Loader=yaml.FullLoader)
# 关于yaml.load()的错误问题:https://blog.youkuaiyun.com/sinat_40831240/article/details/90054108
# 或者 data = json.loads(text_data)
(五)很多时候,我们真的需要对爬下来的网页字符串信息进行正则匹配:
text = html_cont.replace('\/', '').encode().decode('unicode_escape')
# 正则表达式匹配出"result":与"facets":{之间的内容:因为这两个字符串在所有网页中是通用的。
# 如果不使用re.S参数,则只在每一行内进行匹配,如果一行没有,就换下一行重新开始;而使用re.S参数以后,正则表达式会将这个字符串作为一个整体,在整体中进行匹配。
# 正则表达式中的括号会把匹配结果分成若干组,其中group(X)表示提取第X组的内容
# 正则表达式加了()小括号之后,不仅会输出匹配的内容,还会再一次输出小括号中匹配的内容。见:https://m.imooc.com/qadetail/229445
result = '{' + re.search('"result":{(.*?)"facets":{"', text, flags=re.S).group(1) + '}'
result = re.sub('class=".*?"', '', result).replace('<em>', '')
# eval用来执行字符串的表达式,并返回相关结果,其可以将字符串转换成字典。见:https://www.runoob.com/python/python-func-eval.html
# data = eval(result)
# pid = data['resultList'][0]['pid']
# 选择第一个查询结果
result = re.search('''{"pid":(.*?),"entName"''', result, flags=re.S)
if result is not None:
pid = result.group(1)
return pid.strip('"')
else:
# 匹配不出任何企业信息
print("There is no such company in aiqicha")
return None
except Exception as e:
print("The html_content of parser_search is error!")
return None
(六)我们也许会用相同的程序在多台机器上跑数据,那么如何进行区分呢?实际上,最简单的是利用IP来区分:
import socket
def get_host_ip(self):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
finally:
s.close()
return ip
本文项目的具体地址。
589

被折叠的 条评论
为什么被折叠?



