关于python爬虫

Python3.10

Python3.10

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

平时比较忙,事情多而杂乱,没有时间和大家分享一些技术问题。因为项目需要,最近在写爬虫代码,因此分享一些相关内容给大家。这包括了爬虫的部分插件、翻页方法、爬虫与数据结构的关系、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格式),Ref1Ref2。其中,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

本文项目的具体地址

您可能感兴趣的与本文相关的镜像

Python3.10

Python3.10

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值