使用Scrapy爬取掘金热门文章的分析和实现

一、分析掘金网页

1. 获取浏览器URL

直接页面选择30内最热门的文章可得到URL地址为 https://juejin.im/timeline?sort=monthly_hottest 查看该网页Dom元素发现并没有文章的数据,可得知此为动态网页。

2. 获得数据API

请求
响应
由此得到获取文章的API为 https://web-api.juejin.im/query

3. 分析请求参数

在登录状态下访问该接口的 Header 中自定义的参数有:

1
2
3
4
X-Agent: Juejin/Web
X-Legacy-Device-Id: 1575538149621
X-Legacy-Token: eyJhY2Nlc3NfdG9rZWS4iOiJjMHFEbFBnZ1pFMmZtN3NxIiwicmVmcmVzaF90b2tlbiI6IkVoSXJPSlhvTEhRYlRBZmgiLCJ0b2tlbl90eXBlIjoibWFjIiwiZXhwaXJlX2luIjoyNTkyMDAwfDGQ==
X-Legacy-Uid: 5b502c73f265da0f9d19fc58

POST 的参数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
    "operationName":"",
    "query":"",
    "variables":{
        "tags":[

        ],
        "category":"5562b415e4b00c57d9b94ac8",
        "first":20,
        "after":"",
        "order":"MONTHLY_HOTTEST"
    },
    "extensions":{
        "query":{
            "id":"653b587c5c7c8a00ddf67fc66f989d42"
        }
    }
}

未登陆状态下的 Header 自定义参数为:

1
2
3
4
X-Agent: Juejin/Web
X-Legacy-Device-Id:
X-Legacy-Token:
X-Legacy-Uid:

POST 的参数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
    "operationName":"",
    "query":"",
    "variables":{
        "tags":[

        ],
        "category":"5562b415e4b00c57d9b94ac8",
        "first":20,
        "after":"",
        "order":"MONTHLY_HOTTEST"
    },
    "extensions":{
        "query":{
            "id":"653b587c5c7c8a00ddf67fc66f989d42"
        }
    }
}

从上对比得出Header中 X-Legacy-Device-Id X-Legacy-Token X-Legacy-Uid 不必要,只需要 X-Agent: Juejin/Web 即可。 Post 的参数从中推出

1
2
3
4
5
6
Category: 5562b415e4b00c57d9b94ac8  // 此为前端分类的标识
Query : id // 顶部搜索条件
order : // 排序方式
First: // 第一页条数
tags: // Category下的标签
after: //推测为滚动条距离底部的距离 分页相关

返回的数据结构为:

1
2
3
4
5
6
7
8
9
10
11
12
数据结构
{
data: {
articleFeed: {
items: {
edges: [] ,
pageInfo: {
}
}

}
}
}

最终取值应为 edges[index][nodes]

二、抓取数据

1. 使用scrapy创建项目,项目中 item 根据需求配置 item

1
2
3
4
5
6
7
8
9
10
11
## 此处我是用的项目名为project_002
class Project002Item(scrapy.Item):
title = scrapy.Field()
content = scrapy.Field()
url = scrapy.Field()
like = scrapy.Field()
user = scrapy.Field()
category = scrapy.Field()
updated_date = scrapy.Field()
article_id = scrapy.Field()
pass

2. 使用命令 scrapy genspider 文件名 网址生成爬虫文件。

生成后需改造爬虫文件
添加 Header 参数

1
2
3
4
5
6
7
headers = {
"X-Agent": "Juejin/Web",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
"Content-Type": "application/json",
"Host": "web-api.juejin.im",
"Origin": "https://juejin.im"
}

重写 start_requests 方法,由于是直接爬取前100条,就在 first 直接写上100,不做分页处理。

1
2
3
4
5
def start_requests(self):
url = 'https://web-api.juejin.im/query'
query_data = {"operationName":"","query":"","variables":{"first":100,"after":"","order":"MONTHLY_HOTTEST"},"extensions":{"query":{"id":"21207e9ddb1de777adeaca7a2fb38030"}}}
response = scrapy.Request(url, method="POST",headers=self.headers, body=json.dumps(query_data),callback=self.parse_item)
yield response

回调方法处理

1
2
3
4
5
6
7
8
9
def parse_item(self, response):
item = Project002Item()
edges = json.loads(response.text)['data']['articleFeed']['items']['edges']
temp = []
for edge in edges:
node = edge['node']
temp.append({"title": node['title'], "url": node['originalUrl'], "like": node['likeCount'], 'content': node['content'], 'user': node['user']['username'], 'updated_date': node['updatedAt'], 'category': node['category']['name'], 'article_id': node['id']})
item = temp
return item

三、储存到数据库

将配置文件的 ITEM_PIPELINES 选项打开,在 pipelines 写入以下代码(数据库需要设计相应字段的表):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def __init__(self):
db = {
'host': '127.0.0.1',
'port': 3306,
'user': 'root',
'password': 'root',
'database': 'spiders',
'charset': 'utf8'
}
self.conn = pymysql.connect(**db)
self.cursor = self.conn.cursor()

def process_item(self, item, spider):
self.cursor.execute("""INSERT IGNORE INTO juejin (id,title,`url`,`like`,content,category,user,updated_date,article_id)
VALUES (null,%s,%s,%s,%s,%s,%s,%s,%s)""",
(item['title'],
item['url'],
item['like'], item['content'], item['category'], item['user'],
item['updated_date'],
item['article_id']
)
)
self.conn.commit()

使用 scrapy crawl 爬虫名 即可存储相应数据
数据

总结

掘金的爬虫相对比较简单,反爬机制几乎没有,非常适合入门练手。
完整代码