Skip to content

Commit

Permalink
完善 item: 添加自定义异常,优化文档及示例等
Browse files Browse the repository at this point in the history
  • Loading branch information
shengchenyang committed Jun 29, 2023
1 parent 2edf8a4 commit 6709ca9
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 58 deletions.
15 changes: 15 additions & 0 deletions ayugespidertools/common/typevars.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"KafkaConf",
"DynamicProxyConf",
"ExclusiveProxyConf",
"FieldAlreadyExistsError",
"EmptyKeyError",
]

AiohttpRequestMethodStr = Literal["GET", "POST"]
Expand Down Expand Up @@ -133,3 +135,16 @@ class KafkaConf(NamedTuple):
bootstrap_servers: list
topic: str
key: str


class FieldAlreadyExistsError(Exception):
def __init__(self, field_name: str):
self.field_name = field_name
self.message = f"字段 {field_name} 已存在!"
super().__init__(self.message)


class EmptyKeyError(Exception):
def __init__(self):
self.message = "字段名不能为空!"
super().__init__(self.message)
109 changes: 79 additions & 30 deletions ayugespidertools/items.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from abc import ABCMeta
from dataclasses import dataclass
from typing import Any, Dict, Literal, NamedTuple, TypeVar, Union
from typing import Any, Dict, Literal, NamedTuple, Optional, TypeVar, Union

import scrapy
from scrapy.item import Field, Item

from ayugespidertools.common.typevars import EmptyKeyError, FieldAlreadyExistsError

ItemModeStr = Literal["Mysql", "MongoDB"]
# python 3.8 无法优雅地使用 LiteralString,以下用 Literal 代替
MysqlItemModeStr = Literal["Mysql"]
Expand Down Expand Up @@ -53,26 +55,37 @@ class ItemMeta(ABCMeta):
def __new__(cls, class_name, bases, attrs):
new_class = super().__new__(cls, class_name, bases, attrs)

# 动态添加字段方法
def add_field(
self: Union[object, AyuItemTypeVar],
key: str = None,
key: Union[str, Any],
value: Any = None,
) -> None:
assert key is not None, "添加字段时 key 为空!"
"""
动态添加字段方法
Args:
self: self
key: 需要添加的字段名,这里类型为 str,为了消除 ide 的警告才加上了 Any
value: 需要添加的字段对应的值
Returns:
None
"""
if not key:
raise EmptyKeyError()
if key in self._AyuItem__fields:
raise FieldAlreadyExistsError(key)
setattr(self, key, value)
self.fields.append(key)
self._AyuItem__fields.add(key)

def _asdict(
self,
) -> Dict[str, Any]:
"""
将 AyuItem 转换为 dict
"""
_item_dict = {key: getattr(self, key) for key in self.fields}
_item_dict["_table"] = self._table
if self._mongo_update_rule:
_item_dict["_mongo_update_rule"] = self._mongo_update_rule
self._AyuItem__fields.discard("_AyuItem__fields")
_item_dict = {key: getattr(self, key) for key in self._AyuItem__fields}
return _item_dict

def _asitem(
Expand All @@ -81,6 +94,7 @@ def _asitem(
) -> ScrapyItem:
"""
将 AyuItem 转换为 ScrapyItem
Args:
assignment: 是否将 AyuItem 中的值赋值给 ScrapyItem,默认为 True
Expand All @@ -103,28 +117,42 @@ def _asitem(
@dataclass
class AyuItem(metaclass=ItemMeta):
"""
这个是 Scrapy item 的 Mysql 的存储结构
用于创建和动态添加 item 字段,以及提供转换为 dict 和 ScrapyItem 的方法。
Attributes:
_table (str): 数据库表名。
_mongo_update_rule (Dict[str, Any]): MongoDB 存储场景下可能需要的查重条件,默认为 None。
_table: 数据库表名。
_mongo_update_rule: MongoDB 存储场景下可能需要的查重条件,默认为 None。
__fields: 为保护字段,用于存放所有字段名,不用理会。
Examples:
>>> item = AyuItem(
>>> _table="test_table",
>>> title="test_title",
>>> _mongo_update_rule={"title": "test_title"},
>>> )
>>> item._table
'test_table'
... _table="ta",
... )
>>> # 获取字段;.field 和 ["field"] 两种方式
>>> [ item._table, item["_table"] ]
['ta', 'ta']
>>>
>>> # 添加 / 修改字段,不存在则创建,存在则修改:
>>> # 同样支持 .field 和 ["field"] 两种方式
>>> item._table = "tab"
>>> item["title"] = "tit"
>>> # 也可通过 add_field 添加字段,但不能重复添加相同字段
>>> item.add_field("num", 10)
>>> [ item._table, item["title"], item["num"] ]
['tab', 'tit', 10]
>>> item.asdict()
{'title': 'test_title', '_table': 'test_table', '_mongo_update_rule': {'title': 'test_title'}}
{'title': 'tit', '_table': 'tab', 'num': 10}
>>> type(item.asitem())
<class 'ayugespidertools.items.ScrapyItem'>
>>> # 删除字段:
>>> del item["title"]
>>> item
{'_table': 'tab', 'num': 10}
"""

_table: str = None
_mongo_update_rule: Dict[str, Any] = None
__fields: Optional[set] = None

def __init__(
self,
Expand All @@ -133,36 +161,57 @@ def __init__(
**kwargs,
):
"""
初始化 AyuItem 实例
初始化 AyuItem 实例
Args:
_table: 数据库表名。
_mongo_update_rule: MongoDB 存储场景下可能需要的查重条件,默认为 None。
"""
self._table = _table
self._mongo_update_rule = _mongo_update_rule
self.fields = []
self.__fields = set()
if _table:
self.__fields.add("_table")
setattr(self, "_table", _table)
if _mongo_update_rule:
self.__fields.add("_mongo_update_rule")
setattr(self, "_mongo_update_rule", _mongo_update_rule)
for key, value in kwargs.items():
setattr(self, key, value)
self.fields.append(key)
self.__fields.add(key)

def __getitem__(self, key):
return getattr(self, key)

def __setitem__(self, key, value):
if key not in self.fields:
if key not in self.__fields:
setattr(self, key, value)
self.fields.append(key)
self.__fields.add(key)
else:
setattr(self, key, value)

def __delitem__(self, key):
if key in self.fields:
delattr(self, key)
self.fields.remove(key)
if key not in self.__fields:
raise KeyError(f"{key} not found")
delattr(self, key)
self.__fields.discard(key)

def __setattr__(self, name, value):
super().__setattr__(name, value)
self.__fields.add(name)

def __delattr__(self, name):
super().__delattr__(name)
self.__fields.discard(name)

def __str__(self: Any):
return f"{self.__class__.__name__}({self._asdict()})"
# 与下方 __repr__ 一样,不返回 AyuItem(field=data) 的格式
return f"{self._asdict()}"

def __repr__(self: Any):
return f"{self._asdict()}"

def fields(self):
self.__fields.discard("_AyuItem__fields")
return self.__fields

def asdict(self: Any):
return self._asdict()
Expand Down
24 changes: 24 additions & 0 deletions docs/additional/news.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Release notes

## AyugeSpiderTools 3.3.1 (2023-06-29)

### Deprecation removals

- 无。

### Deprecations

- 无。

### New features

- 无。

### Bug fixes

- 无。

### Code optimizations

- 优化 `item` 使用体验,完善功能及对应文档内容,具体请查看 [readthedocs](https://ayugespidertools.readthedocs.io/en/latest/topics/items.html) `item` 部分。

<hr/>

## AyugeSpiderTools 3.3.0 (2023-06-21)

### Deprecation removals
Expand Down
79 changes: 65 additions & 14 deletions docs/topics/items.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

> 以下为本库中推荐的 `mysql``MongoDB` 存储时的主要 `Item` 示例:
本库将所有需要存储的字段直接在对应的 `Item` (`AyuItem`) 中赋值即可,其中下划线开头的参数为必须参数,需要自定义(但 IDE 会提示参数的,不用担心效率或用户体验问题),也可以使用 `add_field` 方法动态添加字段。
本库将所有需要存储的字段直接在对应的 `Item` (`AyuItem`) 中赋值即可,其中 `_table` 参数为必须参数,需要自定义(但 IDE 有参数提示,不用担心效率或用户体验问题),也可以使用 `add_field` 方法动态添加字段。

```python
def parse(self, response):
Expand All @@ -35,12 +35,13 @@ def parse(self, response):
# 这里表示以 article_detail_url 为去重规则,若存在则更新,不存在则新增
_mongo_update_rule={"article_detail_url": article_detail_url},
)

# 其实,以上可以只赋值一次 AyuItem ,然后在 ITEM_PIPELINES 中激活对应的 pipelines 即可,这里是为了方便展示功能。
```

以上可知,目前可直接将需要的参数在对应 `Item` 中直接按 `key=value` 赋值即可,`key` 即为存储至库中字段,`value` 为存储内容。
其实,以上可以只赋值一次 `AyuItem` ,然后在 `ITEM_PIPELINES` 中激活对应的 `pipelines` 即可,这里是为了方便展示功能。

当然,目前也支持动态赋值,但是我不推荐使用,直接按照上个方式即可
当然,目前也支持动态赋值,但我还是推荐直接创建好 `AyuItem` ,方便管理

```python
def parse(self, response):
Expand All @@ -55,23 +56,80 @@ def parse(self, response):
另外,本库的 `item` 提供类型转换,以方便后续的各种使用场景:

```python
# 将本库 Item 转为 dict 的方法
# 将本库 AyuItem 转为 dict 的方法
item_dict = mdi.asdict()
# 将本库 Item 转为 scrapy Item 的方法
# 将本库 AyuItem 转为 scrapy Item 的方法
item = mdi.asitem()
```

## AyuItem 使用详解

> 详细介绍 `AyuItem` 支持的使用方法:
创建 `AyuItem` 实例:

```python
item = AyuItem(_table="ta")
```

获取字段:

```python
>>> item._table
'ta'
>>> item["_table"]
'ta'
>>>
>>> # 注意:以上两种风格都可以。
```

添加 / 修改字段(不存在则创建,存在则修改):

```python
>>> item._table = "tab"
>>> item["title"] = "tit"
>>>
>>> # 也可通过 add_field 添加字段,但不能重复添加相同字段
>>> item.add_field("num", 10)
>>>
>>> [ item._table, item["title"], item["num"] ]
['tab', 'tit', 10]
>>>
>>> 注:以上几种风格可以任选一个自己喜欢的。
```

类型转换:

```python
>>> # 内置转为 dict 和 scrapy Item 的方法
>>>
>>> item.asdict()
{'title': 'tit', '_table': 'tab', 'num': 10}
>>>
>>> type(item.asitem())
<class 'ayugespidertools.items.ScrapyItem'>
```

删除字段:

```python
>>> # 删除字段:
>>>
>>> del item["title"]
>>> item
{'_table': 'tab', 'num': 10}
```

## 使用示例

> 只需要在 `yield item` 时,按需提前导入 `AyuItem`,将所有的存储字段和场景补充字段全部添加完整即可。
以本库模板中的 `basic.tmpl` 为例
`AyuItem``spider` 中常用的基础使用方法示例,以本库模板中的 `basic.tmpl` 为例来作解释

```python
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import pandas
from DemoSpider.settings import logger
from scrapy.http.response.text import TextResponse

from ayugespidertools.common.utils import ToolsForAyu
Expand All @@ -96,16 +154,9 @@ class DemoOneSpider(AyuSpider):
name = 'demo_one'
allowed_domains = ['csdn.net']
start_urls = ['https://www.csdn.net/']

# 数据库表的枚举信息
custom_table_enum = TableEnum
# 初始化配置的类型
settings_type = 'debug'
custom_settings = {
# scrapy 日志等级配置
'LOG_LEVEL': 'DEBUG',
# 以 loguru 来管理日志,本库会在 settings 中生成规则示例,可自行修改。也可不配置
'LOGURU_CONFIG': logger,
'ITEM_PIPELINES': {
# 激活此项则数据会存储至 Mysql
'ayugespidertools.pipelines.AyuFtyMysqlPipeline': 300,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "AyugeSpiderTools"
version = "3.3.0"
version = "3.3.1"
description = "scrapy 扩展库:用于扩展 Scrapy 功能来解放双手,还内置一些爬虫开发中的通用方法。"
authors = ["ayuge <[email protected]>"]
maintainers = ["ayuge <[email protected]>"]
Expand Down
Loading

0 comments on commit 6709ca9

Please sign in to comment.