cookie与session
cookie是浏览器支持的一种本地存储方式, 以键值对的方式存储到浏览器中。
之所以需要cookie是因为http协议是一种无状态的协议, 无状态请求可以理解为两次http访问本身相互独立, 没有依赖关系, 两次访问的依赖关系记录在cookie中。
session是记录在服务端的数据, 用来记录某个客户端的数据记录。
实现模拟登录
考虑复用之前的代码, 所以我们在之前的项目目录下新建一个spider脚本文件:
cd ArticleSpider
scrapy genspider zhihu(爬虫名) www.zhihu.com(目标网站地址)
下载安装Selenium
pip install selenium
同时, 我们需要下载对应浏览器的driver, 以chromedriver为例, 我们需要在http://npm.taobao.org/mirrors/chromedriver/下载对应版本的driver, 我们可在chrome中又上角点击“帮助”, “关于chrome”获得其版本号:
Selenium的使用
直接使用chromedriver打开chrome浏览器并模拟登录时, 很多网站都能识别出来,包括知乎, 这种方式启动的浏览器和平时手动打开的浏览器会有一些区别, 这些区别并不是因为http请求头参数不一致, 而是因为chromedriver有一些JS变量, 会暴露其真实身份, 从而被拒绝登陆。这时我们可以手动启动一个chrome, 然后用chromedriver去连接它, 我们用如下命令启动一个chrome浏览器:
<打开chrome的可执行文件路径> --remote-debugging-port=9222
这样我们就用命令行打开了一个浏览器, 并且在9222号端口上进行监听。然后我们还需要确认这个开启的浏览器是有效的,具体方法是在浏览器中输入地址[http://127.0.0.1:9222/json](http://127.0.0.1:9222/json), 查看是否能打开一个json页面。确认有效后我们开始编写driver的配置代码:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import os
import time
chrome_option = Options()
chrome_option.add_argument("--disable-extensions")
chrome_option.add_experimental_option("debuggerAddress","127.0.0.1:9222")
driver_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ,"drivers/chromedriver")
browser = webdriver.Chrome(executable_path = driver_path,chrome_options=chrome_option)
browser.get('https://www.zhihu.com/signin')
browser.find_element_by_css_selector('form > div.SignFlow-tabs > div:nth-child(2)').click()
browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys("")
browser.find_element_by_css_selector('.SignFlow-password input').send_keys("")
browser.find_element_by_css_selector('form > button').click()
time.sleep(60)
模拟登录优化
以上方法基本能保证登录成功了, 但是selenium不能保证每一次操作都确定有效, 因此我们要对模拟登录进行一些优化, 首先我们在每次输入账号密码前, 模拟全选快捷键, 保证输入信息前, 对<input>中原本的信息进行清空:
from selenium.webdriver.common.keys import Keys
......
......
browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys(Keys.CONTROL + "a")
browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys("")
browser.find_element_by_css_selector('.SignFlow-password input').send_keys(Keys.CONTROL + "a")
browser.find_element_by_css_selector('.SignFlow-password input').send_keys("")
......
接下来, 我们对click()操作进行优化, 因为selenium控制的click()有时候会失效。这里我们可以下载一个工具, 模拟鼠标的操作:
pip install mouse
接下来开始模拟鼠标移动和点击:
from mouse import move,click
......
#获得确定按钮的位置(也可以考虑下载一个屏幕坐标获取工具)
browser.find_element_by_css_selector('form > button').location
move(874,567)
click()
cookie保存
当我们用driver登录过一次后, 再次打开登录页面, 发现用户名密码也已经自动填充了, 这说明driver是能保存我们的cookie的, 现在我们来想办法将driver中的cookie取出来, 我们在登录完成后执行以下代码, 并用debug的方式执行:
......
cookies = browser.get_cookies()
debug后, 我们得到了drivers存储的cookies的值, 我们也可以使用python内置的pickle将cookies存储到文件中:
import pickle
......
cookies = browser.get_cookies()
pickle.dump(cookies,open("<file_path>",'wb'))
cookie_dict = {}
for cookie in cookies:
cookie_dict[[cookie["name"]] = cookie["value"]]
注意,如果我们要使用cookie,需要先去settings配置激活一下两行变量:
......
#如果这里配置为True, scrapy将会自动将上一个request的cookies添加到下一个request中,实际上除了第一个request, 后续的request我们都不需要添加cookies的参数了.
COOKIES_ENABLED = True
#是否允许打印cookie信息
COOKIES_DEBUG = True
......
此外, 我们还需要在settings中设置一个user_agent, 否则scrapy将很容易别服务器识别为爬虫,同时,我们也需要配置一下相应的middleware,这个组件的用法, 后面会提到:
......
USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
......
DOWNLOADER_MIDDLEWARES = {
#'ZhihuSpider.middlewares.ZhihuspiderDownloaderMiddleware': 543,
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware':2
}
知乎倒立文字验证识别
我们还是不能有侥幸心理, 还是要想办法解决倒立验证码的问题, 这里我们可以使用git上的一个开源项目“zheye”
git clone --depth=1 https://github.com/muchrooms/zheye.git
cd zheye
pip install -r requirements.txt
下载完以后, 我们将Zheye这个文件夹放到主目录下, 按如下代码就可以调用:
from ZhihuSpider.zheye import zheye
z = zheye()
positions = z.Recognize('zhihu_images/a.gif')
print(positions)
positions显示的就是倒立字的坐标了, 利用这个坐标加上前面的mouse就可以解决倒立字验证码的问题了。
重写start_requests
在我们已经从技术上实现了模拟登陆过程, 并成功拿到了cookies后, 我们开始将这个过程写到scrapy当中去, 这里我们需要用到scrapy的一个函数start_requests, 每一个spider在处理start_urls时, 首先会进入的就是start_requests, 其原码如下:
def start_requests(self):
cls = self.__class__
if method_is_overridden(cls, Spider, 'make_requests_from_url'):
warnings.warn(
"Spider.make_requests_from_url method is deprecated; it "
"won't be called in future Scrapy releases. Please "
"override Spider.start_requests method instead (see %s.%s)." % (
cls.__module__, cls.__name__
),
)
for url in self.start_urls:
yield self.make_requests_from_url(url)
else:
for url in self.start_urls:
yield Request(url, dont_filter=True)
start_requests方法是spider处理start_urls的入口,即处理start_urls的第一个方法,它会将start_urls遍历, 依次传递给Request, 我们可以重写此方法, 在下载start_urls页面时先进行模拟登录:
def start_requests(self):
return [scrapy.Request(url=' headers= self.headers,callback = self.login]
def login(self):
chrome_option = Options()
chrome_option.add_argument("--disable-extensions")
chrome_option.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
driver_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "drivers/chromedriver")
browser = webdriver.Chrome(executable_path=driver_path, chrome_options=chrome_option)
browser.get('https://www.zhihu.com/signin')
browser.find_element_by_css_selector('form > div.SignFlow-tabs > div:nth-child(2)').click()
browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys(Keys.CONTROL + "a")
browser.find_element_by_css_selector('.SignFlow-accountInput.Input-wrapper input').send_keys("")
browser.find_element_by_css_selector('.SignFlow-password input').send_keys(Keys.CONTROL + "a")
browser.find_element_by_css_selector('.SignFlow-password input').send_keys("")
browser.find_element_by_css_selector('form > button').click()
time.sleep(10)
return [scrapy.Request(url=self.start_urls[0], dont_filter=True)]
编写知乎解析逻辑
start_requests在完成登陆后, 会通过request将第一个start_url, 也就是知乎登陆后进入的首页进行下载解析, 并将response返回给parse方法, 然后在parse方法中通过request从首页中解析出所有的question页面的URL, 回调给parse_question处理, 如果拿到的不是question页面的URL, 则回调给自身进一步解析question的URL:
def parse(self, response):
"""
提取出html页面中所有的url进行判断,如果提取的URL格式是/question/xxx进下载进行解析
"""
all_urls = response.css("a::attr(href)").extract()
all_urls = [urljoin(response.url,url) for url in all_urls]
all_urls = filter(lambda x:True if x.startswith("https") else False,all_urls)
for url in all_urls:
match_obj = re.match("(.*zhihu.com/question/(\d+))(/|$).*",url)
if match_obj:
request_url = match_obj.group(1)
print(request_url)
yield scrapy.Request(request_url,headers=self.headers, dont_filter=True,callback=self.parse_question)
else:
yield scrapy.Request(url,headers=self.headers,callback=self.parse)
parse_question方法用于解析question页面, 获取每一个question的数据:
def parse_question(self, response):
"""
处理question页面, 提取出具体的items
"""
match_obj = re.match("(.*zhihu.com/question/(\d+))(/|$).*", response.url)
if match_obj:
question_id = match_obj.group(2)
item_loader = ItemLoader(item=ZhihuQuestionItem(),response=response)
item_loader.add_css("title",".QuestionHeader-title::text")
item_loader.add_css("content",".QuestionHeader-detail")
item_loader.add_value("url",response.url)
item_loader.add_value("zhihu_id",question_id)
item_loader.add_css("answer_num",".List-headerText span::text")
item_loader.add_css("comments_num",".QuestionHeader-Comment > button::text")
item_loader.add_css("watch_user_num", ".NumberBoard-itemValue::text")
item_loader.add_css("topics", ".QuestionHeader-tags .Popover::text")
question_item = item_loader.load_item()
解析完question的数据后, 我们接下来开始解析answer的数据, answer的数据是通过访问接口动态加载的, 我们通过分析得知访问接口的参数都放在URL中:
start_answer_url = 'https://www.zhihu.com/api/v4/questions/{0}/answers?include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%2Cis_labeled%2Cis_recognized%2Cpaid_info%2Cpaid_info_content%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%2A%5D.topics&limit={1}&offset={2}&platform=desktop&sort_by=default'
该URL返回的数据是json形式了,可以使用json方式解析该数据:
def parse_answer(self,response):
ans_json = json.loads(response.text)
is_end = ans_json.get("paging").get("is_end")
totals_answer = ans_json.get("paging").get("totals")
next_url = ans_json.get("paging").get("next")
配置pipelines
pipelines的使用方式, 之前已经介绍过了, 基本用法不做过多介绍, 这里重点说明, 当我们通过pipelines的items有两个时, 如何在一个pipelines中实现不同的item去执行不同的sql语句呢, 这里有一个办法是将sql写入items中:
class ZhihuQuestionItem(scrapy.Item):
......
def get_sql(self):
<insert_sql>
<params>
return <insert_sql>,<params>
class MysqlTwistedPipeline(object)
......
def do_insert(self,cursor,item):
insert_sql,params = items.get_sql()
......
网友评论