【深入Scrapy实战】从登录到数据解析构建完整爬虫流程

news2024/11/13 14:58:45

文章目录

  • 1. 写在前面
  • 2. 抓包分析
  • 3. Scrapy提交登陆请求
  • 4. 列表与详情页面数据解析
  • 5. 中间件Middleware配置

【作者主页】:吴秋霖
【作者介绍】:Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作!
【作者推荐】:对JS逆向感兴趣的朋友可以关注《爬虫JS逆向实战》,对分布式爬虫平台感兴趣的朋友可以关注《分布式爬虫平台搭建与开发实战》
还有未来会持续更新的验证码突防、APP逆向、Python领域等一系列文章

1. 写在前面

  Scrapy是爬虫非常经典的一个框架,深受开发者喜爱!因其简洁高效的设计,被广泛选用于构建强大的爬虫工程。很多人会选择使用它来开发自己的爬虫工程。今天我将用一个论坛网站的示例来全面讲述Scrapy框架的使用

以前都是底层开始,现在不一样了,一上来都是框架。导致很多人是知其然,但不知其所以然。而忽略了底层原理的理解


目标网站(感兴趣的可以练练手)

aHR0cHM6Ly9mb3J1bS5heGlzaGlzdG9yeS5jb20v


这是一个国外的BBS论坛,随手挑的一个曾经写过的案例。前几年做舆情相关的项目,写的爬虫真的是很多,境内外社交媒体、论坛、新闻资讯

在这里插入图片描述

在这里插入图片描述

2. 抓包分析

  首先,我们打开这个网站,这个网站是要登陆的。我们先解决登陆这块,简单的构造一下登陆请求抓个包分析一下:

在这里插入图片描述

上图就是登陆请求提交的参数,接下来我们需要在Scrapy爬虫工程的Spider中构造并实现登陆功能

3. Scrapy提交登陆请求

  参数都都是明文的比较简单,唯一的一个sid也不是加密生成的,在HTML中就能够拿到

很多时候一些接口某些参数,你看起来是密文,但是并不一定就是加密算法生成的,很有可能在HTML或者其它接口响应中就能获取的到

sid获取如下:

在这里插入图片描述

现在我们开始编写Scrapy爬虫中登陆的这部分代码,实现代码如下所示:

def parse(self, response):
	text = response.headers['Set-Cookie']
	pa = re.compile("phpbb3_lzhqa_sid=(.*?);")
	sid = pa.findall(text)[0]
	response.meta['sid'] = sid
	login_url = 'https://forum.axishistory.com/ucp.php?mode=login'
	yield Request(login_url, meta=response.meta, callback=self.parse_login)
        
def parse_login(self, response):
	sid=response.meta['sid']
	username ='用户名'
	password = '密码'
	formdata = {
	    "username": username,
	    "password": password,
	    "sid": sid,
	    "redirect": "index.php",
	    "login": "Login",
	}
	yield FormRequest.from_response(response, formid='login', formdata=formdata, callback=self.parse_after_login)

首先我们它通过parse函数从start_urls请求所响应的response中获取sid的值,然后继续交给parse_login的登陆函数实现模拟登陆

另外说一下formid这个参数,在HTML文档中,表单通常通过标签定义,并且可以包含id属性,这个id属性就是表单的ID,如下一个HTML的示例:

<form id="login" method="post" action="/login">
    <!-- 表单的其他字段 -->
    <input type="text" name="username">
    <input type="password" name="password">
    <!-- 其他表单字段 -->
    <input type="submit" value="Login">
</form>

在上面的这个例子中,标签有一个id属性,其值为“login”。所以,formid这个参数用于指定表单,去构造登陆提交请求

4. 列表与详情页面数据解析

  登陆处理完以后,我们就可以使用Scrapy爬虫继续对列表跟详情页构造请求并解析数据,这一部分的无非就是写XPATH规则了,基本对技术的要求并不高,如下使用XPATH测试工具编写列表页链接提取的规则:
在这里插入图片描述

Scrapy列表页代码实现如下:

def parse_page_list(self, response):
    pagination = response.meta.get("pagination", 1)
    details = response.xpath("//div[@class='inner']/ul/li")
    for detail in details:
        replies = detail.xpath("dl/dd[@class='posts']/text()").extract_first()
        views = detail.xpath("dl/dd[@class='views']/text()").extract_first()
        meta = response.meta
        meta["replies"] = replies
        meta["views"] = views
        detail_link = detail.xpath("dl//div[@class='list-inner']/a[@class='topictitle']/@href").extract_first()
        detail_title = detail.xpath("dl//div[@class='list-inner']/a[@class='topictitle']/text()").extract_first()
        meta["detail_title"] = detail_title
        yield Request(response.urljoin(detail_link), callback=self.parse_detail, meta=response.meta)
    next_page = response.xpath("//div[@class='pagination']/ul/li/a[@rel='next']/@href").extract_first()
    if next_page and pagination < self.pagination_num:
        meta = response.meta
        meta['pagination'] = pagination+1
        yield Request(response.urljoin(next_page), callback=self.parse_page_list, meta=meta)

self.pagination_num是一个翻页最大采集数的配置,这个自行设定即可

通过列表页我们拿到了所有贴文的链接,我们并在代码的最后使用了yield对列表页发起了请求,<font 并通过color=#ff0033 size=3>callback=self.parse_detail交给解析函数去提取数据

首先我们定义在项目的items.py文件中定义Item数据结构,主要帖子跟评论的,如下所示:

class AccountItem(Item):
    account_url = Field()                # 账号url
    account_id = Field()                 # 账号id
    account_name = Field()               # 账号名称
    nick_name = Field()                  # 昵称
    website_name = Field()               # 论坛名
    account_type = Field()               # 账号类型,固定forum
    level = Field()                      # 账号等级
    account_description = Field()        # 账号描述信息
    account_followed_num = Field()       # 账号关注数
    account_followed_list = Field()      # 账号关注id列表
    account_focus_num = Field()          # 账号粉丝数
    account_focus_list = Field()         # 账号粉丝id列表
    regist_time = Field()                # 账号注册时间
    forum_credits = Field()              # 论坛积分/经验值
    location = Field()                   # 地区
    post_num = Field()                   # 发帖数
    reply_num = Field()                  # 跟帖数
    msg_type = Field()
    area = Field()
    
class PostItem(Item):
    type = Field()                 # "post"
    post_id = Field()              # 帖子id
    title = Field()                # 帖子标题
    content = Field()              # 帖子内容
    website_name = Field()         # 论坛名
    category = Field()             # 帖子所属版块
    url = Field()                  # 帖子url
    language = Field()             # 语种, zh_cn|en|es
    release_time = Field()         # 发布时间
    account_id = Field()            # 发帖人id
    account_name = Field()          # 发帖人账号名
    page_view_num = Field()        # 帖子浏览数
    comment_num = Field()          # 帖子回复数
    like_num = Field()             # 帖子点赞数
    quote_from =Field()            # 被转载的帖子id
    location_info = Field()        # 发帖地理位置信息
    images_url = Field()           # 帖子图片链接
    image_file = Field()           # 帖子图片存储路径
    msg_type = Field()
    area = Field()

class CommentItem(Item):
    type = Field()                 # "comment"
    website_name = Field()         # 论坛名
    post_id = Field()
    comment_id = Field()
    content = Field()              # 回帖内容
    release_time = Field()         # 回帖时间
    account_id = Field()           # 帖子回复人id
    account_name = Field()         # 回帖人名称
    comment_level = Field()        # 回帖层级
    parent_id = Field()            # 回复的帖子或评论id
    like_num = Field()             # 回帖点赞数
    comment_floor = Field()        # 回帖楼层
    images_url = Field()           # 评论图片链接
    image_file = Field()           # 评论图片存储路径
    msg_type = Field()
    area = Field()

接下来我们需要编写贴文内容的数据解析代码,解析函数代码实现如下所示:

def parse_detail(self, response):
    dont_parse_post = response.meta.get("dont_parse_post")
    category = " < ".join(response.xpath("//ul[@id='nav-breadcrumbs']/li//span[@itemprop='title']/text()").extract()[1:])
    if dont_parse_post is None:
        msg_ele = response.xpath("//div[@id='page-body']//div[@class='inner']")[0]
        post_id = msg_ele.xpath("div//h3/a/@href").extract_first(default='').strip().replace("#p", "")
        post_item = PostItem()
        post_item["url"] = response.url
        post_item['area'] = self.name
        post_item['msg_type'] = u"贴文"
        post_item['type'] = u"post"
        post_item["post_id"] = post_id
        post_item["language"] = 'en'
        post_item["website_name"] = self.allowed_domains[0]
        post_item["category"] = category
        post_item["title"] = response.meta.get("detail_title")
        post_item["account_name"] = msg_ele.xpath("div//strong/a[@class='username']/text()").extract_first(default='').strip()
        post_item["content"] = "".join(msg_ele.xpath("div//div[@class='content']/text()").extract()).strip()
        post_time = "".join(msg_ele.xpath("div//p[@class='author']/text()").extract()).strip()
        post_item["release_time"] = dateparser.parse(post_time).strftime('%Y-%m-%d %H:%M:%S')
        post_item["collect_time"] = dateparser.parse(str(time.time())).strftime('%Y-%m-%d %H:%M:%S')
        user_link =msg_ele.xpath("div//strong/a[@class='username']/@href").extract_first(default='').strip()
        account_id = "".join(re.compile("&u=(\d+)").findall(user_link))
        post_item["account_id"] = account_id
        post_item["comment_num"] = response.meta.get("replies")
        post_item["page_view_num"] = response.meta.get("views")
        images_urls = msg_ele.xpath("div//div[@class='content']//img/@src").extract() or ""
        post_item["images_url"] = [response.urljoin(url) for url in images_urls]
        post_item["image_file"] = self.image_path(post_item["images_url"])
        post_item["language"] = 'en'
        post_item["website_name"] = self.name
        response.meta["post_id"] = post_id
        response.meta['account_id'] = post_item["account_id"]
        response.meta["account_name"] = post_item["account_name"]
        full_user_link = response.urljoin(user_link)
        yield Request(full_user_link, meta=response.meta, callback=self.parse_account_info)
    for comment_item in self.parse_comments(response):
        yield comment_item
    comment_next_page = response.xpath(u"//div[@class='pagination']/ul/li/a[@rel='next']/@href").extract_first()
    if comment_next_page:
        response.meta["dont_parse_post"] = 1
        next_page_link = response.urljoin(comment_next_page)
        yield Request(next_page_link, callback=self.parse_detail, meta=response.meta)

贴文内容的下方就是评论信息,上面代码中我们拿到评论的链接comment_next_page,直接继续发送请求解析评论内容:

在这里插入图片描述

def parse_comments(self, response):
    comments = response.xpath("//div[@id='page-body']//div[@class='inner']")
    if response.meta.get("dont_parse_post") is None:
        comments = comments[1:]
    for comment in comments:
        comment_item = CommentItem()
        comment_item['type'] = "comment"
        comment_item['area'] = self.name
        comment_item['msg_type'] = u"评论"
        comment_item['post_id'] = response.meta.get("post_id")
        comment_item["parent_id"] = response.meta.get("post_id")
        comment_item["website_name"] = self.allowed_domains[0]
        user_link =comment.xpath("div//strong/a[@class='username']/@href").extract_first(default='').strip()
        account_id = "".join(re.compile("&u=(\d+)").findall(user_link))
        comment_item['comment_id'] = comment.xpath("div//h3/a/@href").extract_first(default='').strip().replace("#p","")
        comment_item['account_id'] = account_id
        comment_item['account_name'] = comment.xpath("div//strong/a[@class='username']/text()").extract_first(default='').strip()
        comment_time = "".join(comment.xpath("div//p[@class='author']/text()").extract()).strip()
        if not comment_time:
            continue
        comment_level_text = comment.xpath("div//div[@id='post_content%s']//a[contains(@href,'./viewtopic.php?p')]/text()" % comment_item['comment_id']).extract_first(default='')
        comment_item['comment_level'] = "".join(re.compile("\d+").findall(comment_level_text))
        comment_item['release_time'] = dateparser.parse(comment_time).strftime('%Y-%m-%d %H:%M:%S')
        comment_content_list = "".join(comment.xpath("div//div[@class='content']/text()").extract()).strip()
        comment_item['content'] = "".join(comment_content_list)
        response.meta['account_id'] = comment_item["account_id"]
        response.meta["account_name"] = comment_item["account_name"]
        full_user_link = response.urljoin(user_link)
        yield Request(full_user_link, meta=response.meta, callback=self.parse_account_info)

评论信息采集中还有一个针对评论用户信息采集的功能,通过调用parse_account_info函数进行采集,实现代码如下所示:

def parse_account_info(self, response):
    about_item = AccountItem()
    about_item["account_id"] = response.meta["account_id"]
    about_item["account_url"] = response.url
    about_item["account_name"] = response.meta["account_name"]
    about_item["nick_name"] = ""
    about_item["website_name"] = self.allowed_domains[0]
    about_item["account_type"] = "forum"
    about_item["level"] = ""
    account_description = "".join(response.xpath("//div[@class='inner']/div[@class='postbody']//text()").extract())
    about_item["account_description"] = account_description
    about_item["account_followed_num"] = ""
    about_item["account_followed_list"] = ""
    about_item["account_focus_num"] = ""
    about_item["account_focus_list"] = ""
    regist_time = "".join(response.xpath("//dl/dt[text()='Joined:']/following-sibling::dd[1]/text()").extract())
    about_item["regist_time"] = dateparser.parse(regist_time).strftime('%Y-%m-%d %H:%M:%S')
    about_item["forum_credits"] = ""
    location = "".join(response.xpath("//dl/dt[text()='Location:']/following-sibling::dd[1]/text()").extract())
    about_item["location"] = location
    post_num_text = response.xpath("//dl/dt[text()='Total posts:']/following-sibling::dd[1]/text()[1]").extract_first(default='')
    post_num = post_num_text.replace(",",'').strip("|").strip()
    about_item["post_num"] = post_num
    about_item["reply_num"] = ""
    about_item["msg_type"] = 'account'
    about_item["area"] = self.name
    yield about_item

最后从帖子到评论再到账号信息,层层采集与调用拿到完整的一个JSON结构化数据,进行yield到数据库

5. 中间件Middleware配置

  因为是国外的论坛网站案例,所以这里我们需要使用我们的Middleware来解决这个问题:

class ProxiesMiddleware():
    logfile = logging.getLogger(__name__)

    def process_request(self, request, spider):
        self.logfile.debug("entry ProxyMiddleware")
        try:
            # 依靠meta中的标记,来决定是否需要使用proxy
            proxy_addr = spider.proxy
            if proxy_addr:
                if request.url.startswith("http://"):
                    request.meta['proxy'] = "http://" + proxy_addr  # http代理
                elif request.url.startswith("https://"):
                    request.meta['proxy'] = "https://" + proxy_addr  # https代理
        except Exception as e:
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            self.logfile.warning(u"Proxies error: %s, %s, %s, %s" %
                                 (exc_type, e, fname, exc_tb.tb_lineno))

settings文件中配置开启Middleware:

DOWNLOADER_MIDDLEWARES = {
	'forum.middlewares.ProxiesMiddleware': 100,
}

  好了,到这里又到了跟大家说再见的时候了。创作不易,帮忙点个赞再走吧。你的支持是我创作的动力,希望能带给大家更多优质的文章

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1231011.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

趣学python编程 (四、数据结构和算法介绍)

数据结构和算法在编程中非常重要。数据结构是组织和存储数据的方式&#xff0c;而算法是解决问题的方法和步骤。你要挑战的蓝桥杯&#xff0c;实际也是在设计算法解决问题。其实各种编程语言都只是工具&#xff0c;而程序的核心数据结构算法。犹如练武&#xff0c;数据结构和算…

Zotero在word中插入带超链接的参考文献/交叉引用/跳转参考文献

Zotero以其丰富的插件而闻名&#xff0c;使用起来十分的带劲&#xff0c;最重要的是它是免费的、不卡顿&#xff0c;不像某专业软件。 然而Zotero在word插入参考文献时&#xff0c;无法为参考文献添加超链接&#xff0c;这是一个不得不提的遗憾。 不过&#xff0c;有大佬已经…

C语言——写一个函数,每调用一次这个函数,就会将num的值增加1

#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>void Add(int* p) {(*p); // 的优先级高于* } int main() {int num0;Add(&num);printf("第一次调用:num %d\n",num);Add(&num);printf("第二次调用:num %d\n",num);Add(&num);p…

如何客观看待通过线上性格测试来刷人的公司?

不论是校招&#xff0c;还是社招&#xff0c;我认为采用性格测评的目的&#xff0c;有两种情况&#xff1a; 1、应聘者多&#xff0c;减轻面试的压力 这种情况是最常见的&#xff0c;如果招不到人&#xff0c;应聘者少&#xff0c;我想HR是恨不得开车去接你过来面试&#xff…

Java 算法篇-链表的经典算法:判断回文链表、判断环链表与寻找环入口节点(“龟兔赛跑“算法实现)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 链表的创建 2.0 判断回文链表说明 2.1 快慢指针方法 2.2 使用递归方式实现反转链表方法 2.3 实现判断回文链表 - 使用快慢指针与反转链表方法 3.0 判断环链表说明…

【数据结构初阶】双链表

双链表 1.双链表的实现1.1结口实现1.2申请结点1.3初始化双链表1.4打印双链表1.5尾插1.6尾删1.7头插1.8头删1.9计算大小1.10查找1.11pos位置插入1.12删除pos位置1.12删除双链表 全部码源 1.双链表的实现 1.1结口实现 #include<stdio.h> #include<stdlib.h> #inclu…

快速排序知识总结

快速排序思维导图&#xff1a; 快速排序算法模版&#xff1a; #include <iostream>using namespace std;const int N 1e5 10;int n; int q[N];void quick_sort(int q[], int l, int r) {if (l > r) return;int x q[(l r) / 2], i l - 1, j r 1;while (i < …

10 Redis的持久化

Redis支持RDB和AOF两种持久化机制 1、RDB(Redis DataBase) 是对命令的全量快照随着key的数量增大&#xff0c;那么写入磁盘的开销也会越来越大 2、RDB文件的生成是否会阻塞主线程 save: 使用save的方式会阻塞主线程&#xff0c;影响redis的性能 bgsave: 一般情况下不会阻塞…

J. Chem. Inf. Model. | 使用GRID描述符进行深度学习预测血脑屏障透过性

今天为大家介绍的是来自Simon Cross团队的一篇论文。深度学习方法能够自动从输入数据中提取相关特征并捕捉输入和输出之间的非线性关系。在这项工作中&#xff0c;作者提出了基于GRID的AI&#xff08;GrAId&#xff09;描述符&#xff0c;这是对GRID MIFs的简单修改&#xff0c…

Javaweb之Ajax的详细解析

1.1 Ajax介绍 1.1.1 Ajax概述 我们前端页面中的数据&#xff0c;如下图所示的表格中的学生信息&#xff0c;应该来自于后台&#xff0c;那么我们的后台和前端是互不影响的2个程序&#xff0c;那么我们前端应该如何从后台获取数据呢&#xff1f;因为是2个程序&#xff0c;所以…

记一次攻防实战渗透

经典开局一个登录框 由于漏洞应该还未修复。对于数据和相关网址打个码见谅一下 常规思路&#xff08;爆破&#xff09; 常规操作进行一波 尝试弱口令然后开始爆破 对于此种有验证码的爆破&#xff0c;可以借用一个bp插件。 captcha-killer-modified-jdk14.jar 具体使用我就…

读懂:“消费报销”模式新零售打法,适用连锁门店加盟的营销方案

读懂&#xff1a;“消费报销”模式新零售打法&#xff0c;适用连锁门店加盟的营销方案 引言&#xff1a;2023年的双十一已经落下帷幕&#xff0c;作为每年的经典电商促销节&#xff0c;今年已是第15个年头&#xff0c;但是今年各大电商平台却都是非常默契的&#xff0c;没有公布…

算法学习 day26

第二十六天 最大子数组和 53. 最大子数组和 - 力扣&#xff08;LeetCode&#xff09; 动态规划问题 class Solution {public int maxSubArray(int[] nums) {int len nums.length;int[] dp new int[len];dp[0] nums[0];int res dp[0];for(int i 1; i < len; i){dp[i] …

【封装UI组件库系列】搭建项目及准备工作

封装UI组件库系列第一篇搭建项目 前言 &#x1f31f;搭建项目 创建工程 基本结构 1.创建8个组件展示页面 ​ 2.配置路由文件router/index.js 3.页面布局 &#x1f31f;总结 前言 在前端开发中&#xff0c;大家可能已经用过各种各样的UI组件库了&#xff0c;现在市面上热…

ANSYS网格无关性检查

网格精度对应力结果存在很大的影响&#xff0c;有时候可以发现&#xff0c;随着网格精度逐渐提高&#xff0c;所求得的最大应力值逐渐趋于收敛。 默认网格&#xff1a; 从默认网格下计算出的应力云图可以发现&#xff0c;出现了的三处应力奇异点&#xff0c;此时算出的应力值是…

聊一聊go的单元测试

文章目录 概要一、测试框架1.1、testing1.2、stretchr/testify1.3、smartystreets/goconvey1.4、cweill/gotests 二、打桩和mock2.1、打桩2.2、mock2.2.1、mockgen 三、基准测试和模糊测试3.1、基准测试3.2、模糊测试 四、总结4.1、小结4.2、其他4.3、参考资料 概要 软件测试是…

vue3 ts vite 主题色功能

开发工具&#xff1a;vue3 ts vite 如上图&#xff0c;选择个颜色整个变化&#xff0c;如下图 默认主题为绿色 切换成其它色。 这里面的颜色块&#xff0c;你也可以给个取器色组件&#xff0c;可切换成任意色。切换时主要执行下方的方法&#xff0c;有兴趣可自己研究下。 /…

【运维篇】5.6 Redis server 主从复制配置

文章目录 0. 前言1. 配置方式步骤1: 准备硬件和网络步骤2: 安装Redis步骤3: 配置主服务器的Redis步骤4: 配置从服务器的Redis步骤5: 测试复制功能步骤6: 监控复制状态 2. 参考文档 0. 前言 在Redis运维篇的第5.6章节中&#xff0c;将讨论Redis服务器的主从复制配置。在开始之前…

根据nginx日志统计页面访问次数

静态页面部署在nginx上&#xff0c;页面只有查看下载功能。 需求是统计每条访问次数和下载次数&#xff0c;根据日志分析写了一个shell脚本&#xff0c;触发脚本后生成一个html可以远程查看统计的数量。 #!/bin/bash # nginx日志文件路径 LOG_FILE"/usr/local/nginx/l…

vue安装three.js并创建第一个入门场景

vue安装three.js&#xff0c;并创建第一个入门场景 安装three.js npm install --save three引入three.js import * as THREE from threethree.js结构 three.js坐标 创建一个场景 scene场景&#xff0c;camera相机&#xff0c;renderer渲染器 创建一个场景 this.scene new T…