Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

2022年5月劳动节爬虫项目记录

2022/05/07 爬虫 Python
Word count: 11,013 | Reading time: 46min

retry模块

retry是一个用于错误处理的模块,功能类似try-except,但可以更加快捷方便的设置重试的次数,以及每次重试之间相隔的时间。

1
2
3
4
@retry()
def make_trouble():
'''Retry until succeed'''
print ('retrying...')
  • exceptions:传入指定的错误类型,默认为Exception,即捕获所有类型的错误,也可传入元组形式的多种指定错误类型

  • tries:定义捕获错误之后重复运行次数,默认为-1,即为无数次。

  • delay:定义每次重复运行之间的停顿时长,单位秒,默认为0,即无停顿。

  • backoff:呈指数增长的每次重复运行之间的停顿时长,需要配合delay来使用,譬如delay设置为3,backoff设置为2,则第一次间隔为3*2^0=3秒,第二次3*2^1=6秒,第三次3*2^2=12秒,以此类推,默认为1。

  • max_delay:定义backoff和delay配合下出现的最大等待时间上限,当

    delay*backoff**n大于max_delay时,等待间隔固定为该值而不再增长。

项目结构

关于项目结构组织。由于Python在执行程序时,会自动将.即当前路径加入到库搜索路径,所以当下路径下的包(文件夹下有__init__.py)都能被检测到使用,如下面的handler、helper都可以直接在其他文件里通过helper.xxx来调用,但是顶层的settings.py不能通过feiyu_shoot.settings来调用,即使feiyu_shoot工程下也有__init__.py,因为项目工程的上层目录并没有加入到Python环境搜索中,所以他实际不知道谁是feiyu_shoot

因此,根据这种设计,可以将项目结构组织成两种

  1. 不依赖子模块的放在root目录的top接口:需要被其他文件引用的列入文件夹中作为库使用,不需要被引用的可以直接放顶层

    1
    2
    3
    4
    5
    # helper/runner.py
    # handler/ 在.下作为包导入
    from handler imoprt ConfigHandler
    # settings.py 在.下作为模块导入
    from settings import USER_AGENT

    topFile

  2. ★将入口放在最外层,核心内容作为单独一个文件夹(模块):由于feiyu是个整体的包,所以在feiyu下的任意Python文件中都可以通过from feiyu.xxx import yyy来导入。

oneEntry

根据Python打包利器:auto-py-to-exe中打包计算机程序的方式,更推荐第二种,这样代码资源文件位置更统一,也更加清晰一些。

more: [Python中模块、包、库定义](

pyinstaller

自我学Python以来推荐的就是这个,经过时光变迁,这个仍然是主流。

  • PyInstaller是一个跨平台的Python应用打包工具,支持 Windows/Linux/MacOS三大主流平台,能够把 Python 脚本及其所在的 Python 解释器打包成可执行文件,从而允许最终用户在无需安装 Python 的情况下执行应用程序。
  • PyInstaller 制作出来的执行文件并不是跨平台的,如果需要为不同平台打包,就要在相应平台上运行PyInstaller进行打包。
  • PyInstaller打包的流程:读取编写好的Python项目–>分析其中条用的模块和库,并收集其文件副本(包括Python的解释器)–>将副本和Python项目文件(放在一个文件夹//封装在一个可执行文件)中。

安装就不细说了,主要讲用法:

  • 通过命令行命令打包pyinstaller -F main.py,常用参数

    • -F: 表示生成单个可执行文件;对应的是-D: 生成文件夹形式的可执行程序(默认)
    • -w: 表示去掉控制台窗口,这在GUI界面时非常有用。不过如果是命令行程序的话那就把这个选项删除吧!
    • -p: 表示你自己自定义需要加载的库路径,一般情况下用不到
    • -i: 表示可执行文件的图标
  • 通过.spec打包定义文件来打包,对应命令行的参数,.spec文件都会有对应的生成内容。

    首先根据main文件生成spec文件: pyi-makespec -D main.py、填写好后再pyinstaller main.spec

    • -D是让spec中多一个coll的实例,从而变成文件夹
    • -i相当于.spec中EXE中icon=".\\debugs\\favicon.ico"
    • …(更多spec文件参数选择见:https://blog.csdn.net/tangfreeze/article/details/112240342)
    • -p: 相当于Analysis实例中的pathex,就是填入自己的模块
    • –hiden-import: 相当于Analysis实例中的hiddenimports

实际上,根据命令行的参数会生成对应的.spec文件。

注:可以看到无论是pyinstallerpyi-makespec后面都是main.py,因为其是main函数入口文件

附:根据feiyu,在此列两个可行的:

  • F:\aDevelopment\Python\ShowYourCode\env\Scripts\pyinstaller.exe main.py --add-data="feiyu\push_config.ini;.\feiyu" --add-data="feiyu\user_config.toml;.\feiyu" -i debugs\favicon.ico

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    # -*- mode: python ; coding: utf-8 -*-
    block_cipher = None # 此处在使用--key= 会有变化

    a = Analysis(['ai\\main.py'],
    pathex=['C:\\Users\\Admin\\Downloads\\marsai-master'],
    binaries=[],
    datas=[],# 此处可以添加静态资源,例如你有个图片文件夹imgs,可以这样写[('imgs','imgs'),('test.txt','.')],打包以后会有一个一样的文件夹,点表示当前文件夹。
    hiddenimports=[],
    hookspath=[],
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False)
    pyz = PYZ(a.pure, a.zipped_data,
    cipher=block_cipher)
    exe = EXE(pyz,
    a.scripts,
    [],
    exclude_binaries=True,
    name='main', # 生成的exe的名字
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True, # 打包的时候进行压缩,False表示不压缩
    console=True # 是否显示黑窗口,刚开始打包的时候一般都会有问题,建议设为True,解决所有问题后可以设置为False)
    coll = COLLECT(exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='main' # 文件夹的名字)

pyinstaller全都是控制台的命令,因此没那么直观,有人做了GUI相对直白、简单些,见Python打包利器:auto-py-to-exe,其关于计算器程序与Additional Files的使用会让人对项目该如果布置结构和使用-p参数有个更确信的认识(导入自己些的模块)。

dataclass

dataclasses嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__

def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
# 注意__annotations__属性
field_type = cls.__annotations__.get(name, None)
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
if isinstance(value, list) and is_dataclass(field_type[0]):
res = []
for c in value:
new_obj = field_type[0](**c)
res.append(new_obj)
kwargs[name] = res
original_init(self, *args, **kwargs)

cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper

# 使用
@nested_dataclass
class TagResult:
"""拍卖展示的分页结果"""
# 当前页下具体商品的信息
content: [Content]
# ...

**解包得到的额外信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    # 额外信息
@classmethod
def from_kwargs(cls, **kwargs):
# split the kwargs into native ones and new ones
native_args, new_args = {}, {}
for name, val in kwargs.items():
if name in cls.__annotations__:
native_args[name] = val
else:
new_args[name] = val

# use the native ones to create the class ...
ret = cls(**native_args)

# ... and add the new ones by hand
for new_name, new_val in new_args.items():
setattr(ret, new_name, new_val)
return ret

# 使用
return Goods.from_kwargs(**resp) if resp else None

pydantic.dataclasses.dataclassdataclasses.dataclass with validation的替代品, 而不是pydantic.BaseModel 的替代品(在初始化挂钩的工作方式上有一点不同),在某些情况下,将pydanticis.BaseModel子类化是更好的选择.

toml语法学习

TOML是前GitHub CEO, Tom Preston-Werner,于2013年创建的语言,其目标是成为一个小规模的易于使用的语义化配置文件格式.TOML是大小写敏感的,必须是UTF-8编码。

toml对象

字符串:跟python中一样,""""''''

注释#

数组:数组使用方括号包裹。空格会被忽略,包括换行符。元素使用逗号分隔。

日期时间:toml中日期是一等公民,跟数组一样

键值对

TOML 文档的主要构建块

  • 可以是裸露的裸键,也可以是被包裹在一对 " 的内部的引用键裸键只能存在字母、数字、下划线和破折号(a-z、A-Z、0-9、-)。注意,裸键可以只由数字组成,例如 1234,但它总是被解析为字符串。引用键遵循与基本字符串字面字符串完全相同的规范,并允许使用更广泛的键名称。最好的做法是使用裸键,除非必要情况。
  • 可以是以下类型:字符串整数浮点数布尔值时间数组内联表*。未确定的值是非法的。
  • 注:周围的空白符忽略不计,=必须位于同一行

表格

  • 空表是允许的,只要里面没有键值对就行了。

  • 表不能定义多于一次,不允许使用 [table] 头重定义这样的表。

    1
    2
    3
    4
    5
    6
    7
    # 不要这样做

    [fruit]
    apple = "红"

    [fruit]
    orange = "橙"
  • 同样地,使用点分隔键来重定义已经以 [table] 形式定义过的表也是不允许的。

  • 不过,[table] 形式可以被用来定义通过点分隔键定义的表中的子表

  • 不鼓励无序地定义表,即同级别子表建议放在一起定义

内联表

内联表提供了一种更为紧凑的语法来表示表。

内联表被完整地定义在花括号之中:{}。 括号中,可以出现零或更多个以逗号分隔的键值对。

  • 内联表得出现在同一行内。
  • 内联表中,最后一对键值对后不允许终逗号(也称为尾逗号)。
  • 不允许花括号中出现任何换行,除非在值中它们合法。
1
2
3
4
5
6
7
8
9
10
11
12
name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }
animal = { type.name = "pug" }
# 等价于
[name]
first = "Tom"
last = "Preston-Werner"
[point]
x = 1
y = 2
[animal]
type.name = "pug"

实际上就是表的另一种紧凑写法,定义的还是表

表数组

表头的第一例定义了这个数组及其首个表元素,而后续的每个则在该数组中创建并定义一个新的表元素。
这些表按出现顺序插入该数组。

首先是个数组;其次,数组中的元素为表内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[[products]]
name = "Hammer"
sku = 738594937
[[products]] # 数组里的空表
[[products]]
name = "Nail"
sku = 284758393
color = "gray"
# 等价于 JSON 的如下结构。
{
"products": [
{ "name": "Hammer", "sku": 738594937 },
{ },
{ "name": "Nail", "sku": 284758393, "color": "gray" }
]
}

📖Tom 的(语义)明显、(配置)最小化的语言——中文手册

库:

  • Python:toml

    1
    2
    3
    4
    5
    6
    import toml

    class UserConfigHandler(metaclass=Singleton):
    def __init__(self):
    with open(USER_PARAM_FILE, 'r', encoding="utf8") as f:
    self.r = toml.load(f)
  • Go: go get github.com/BurntSushi/toml

自用抢购尝试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

def order_auction_goods(self, goodsId: int, goodsName: str = "", by_fail_time=False):
"""
立刻抢拍, 提供①指定尝试次数; ②直到没抢到为止 两种策略
@param by_fail_time: 失败次数重试 or 不断重试
"""
tryTimes = 0
if by_fail_time:
resp = None
# 3次重试
while tryTimes < self.config.userConfig.maxFailCount:
data = {
'goodsId': str(goodsId),
}
resp = self._post(self.URL.orderAuctionGoods, data=data, timeout=self.config.timeoutTime)
if resp.get("code") != 0:
tryTimes += 1
else:
# 抢购成功说明有
resp.update({"goodsId": str(goodsId), "goodsName": str(goodsName)})
return resp
else:
resp = {}
# 3次重试
while not resp.get("msg") or "规定时间内抢拍" in resp.get("msg"):
data = {
'goodsId': str(goodsId),
}
resp = self._post(self.URL.orderAuctionGoods, data=data, timeout=self.config.timeoutTime)
tryTimes += 1
# 抢购成功 resp.get("result") == true
if resp.get("result"):
break
resp.update({"goodsId": goodsId, "goodsName": goodsName})
return resp, tryTimes

base数据库

判断重定向

由于有个需求是,没有investor就不进行爬取,而是否有investor可以通过https://xxxx.com/company_finance/investors会不会进行重定向进行判断

那么如何判断是否发生重定向呢?两种

  1. 禁止requests.getallow_redirect属性
    1.
  2. 运行allow_redirect,然后通过重定向history查看

图片懒加载

图片懒加载是一种网页优化技术。图片作为一种网络资源,在被请求时也与普通静态资源一样,将占用网络资源,而一次性将整个页面的所有图片加载完,将大大增加页面的首屏加载时间。为了解决这种问题,通过前后端配合,使图片仅在浏览器当前视窗内出现时才加载该图片,达到减少首屏图片请求数的技术就被称为“图片懒加载”——优先加载网页结构,以及逻辑脚本,等重要内容全部加载完成之后再加载重要优先级低一些的图片资源(同时也比较耗网络资源),是一个从用户体验上进行分层的技术。

网站一般如何实现图片懒加载技术呢?

在网页源码中,在img标签中首先会使用一个“伪属性”(通常使用src2origin……)去存放真正的图片链接而并非是直接存放在src属性中。当图片出现到页面的可视化区域中,会动态将伪属性替换成src属性,完成图片的加载。

比如:站长素材案例后续分析:通过细致观察页面的结构后发现,网页中图片的链接是存储在了src2这个伪属性中。

因此针对爬取这类图片而言,可以

  1. 找到要爬取图片的位置(网页上的位置)
  2. 查看该网址的链接img_url
  3. 查看网页源代码,Ctrl+F后文本搜索该图片链接img_url
  4. 如果3没有则在开发者工具中Ctrl+F搜索带img_url的资源
  5. 如果是3,则通过找到的真正的xpath路径获取,或者re提取;如果是4则模仿接口去请求图片

本项目案例

pathlib库使用

  • 获得当前脚本的绝对路径:Path(__file__).resolve()

  • 获得当前脚本所在目录的绝对路径: Path(__file__).resolve().parent

  • 根据路径创建文件夹: LOG_PATH = Path(__file__).resolve().with("logs")–>./logs/LOG_PATH.mkdir(parents=True, exist_ok=True)

    • parents:如果父目录不存在,是否创建父目录,即支持递归创建
    • exist_ok:只有在目录不存在时创建目录,目录已存在时不会抛出异常。
  • 获得当前路径下的所有文件(遍历文件夹): [path for path in example_path.iterdir()]

  • 返回目录中最后一个部分的扩展名:Path('/Users/Anders/Documents/abc.gif').suffix

  • 返回目录中多个扩展名列表: Path('/Users/Anders/Documents/abc.tar.gz').suffixes

  • 返回目录中最后一个部分的文件名: Path('/Users/Anders/Documents/abc.gif').name

  • 返回目录中最后一个部分的文件名(但是不包含后缀): Path('/Users/Anders/Documents/abc.gif').stem

  • 文件操作

    1
    2
    3
    4
    5
    6
    7
    example_path = Path('/Users/Anders/Documents/information/JH.txt')
    # 以open的形式
    with example_path.open(encoding = 'GB2312') as f:
    print(f.read())
    # 或者直接用pathlib提供的获取内容的方式
    example_path = Path('/Users/Anders/Documents/information/JH.txt')
    example_path.read_text(encoding='GB2312')
    • .read_text(): 以文本模式打开路径并并以字符串形式返回内容。

    • .read_bytes(): 以二进制/字节模式打开路径并以字节串的形式返回内容。

    • .write_text(): 打开路径并向其写入字符串数据。

      1
      2
      path = Path('data.json')
      path.write_text('{"id": 123, "name": "James"}')
    • .write_bytes(): 以二进制/字节模式打开路径并向其写入数据。

参考:https://blog.csdn.net/itanders/article/details/88754606

按住验证码

该网站对频率检测十分敏感,很快就会出现"按住"验证码,模拟按住倒不难,但是按完以后有时并不能解决验证…

但是意外发现,在“无痕模式”下进行重新登录后,就可以解决验证…但是通过requests进行模拟登录后cookies还是无效。

🌟记录一下这个问题,之后看看有没有时间弄清楚这个问题。

解决方案

既然模拟不行,那么就手动,因此想用selenium来模拟点击登陆。但是因为俄罗斯的问题,无法登陆。

被封

于是只好更换selenium为pyppeteer。之前就听说了pyppeteer是可以代替selenium的模拟神器,相比Selenium 不太方便的地方:比如速度太慢、对版本配置要求严苛,最麻烦是经常要更新对应的驱动,还有些网页是可以检测到是否使用了Selenium ,pyppeteer都有提升,这次也借机会学习一下。

安装:pip install pyppeteer, 需要Python3.6以上(因为用了async)

pyppeteer

最简单demo:打开网页,并截图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
from pyppeteer import launch

async def main():
# 启动浏览器
browser = await launch(headless=True)
# 打开并跳转页面
page = await browser.newPage()
await page.goto('https://example.com')
# 截图
await page.screenshot({'path': 'example.png'})
await browser.close()

asyncio.get_event_loop().run_until_complete(main())

执行JS,相当于requests-html中的render

1
2
3
4
5
6
7
8
9
10
11
12

dimensions = await page.evaluate('''() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio,
}
}''')

print(dimensions)
# >>> {'width': 800, 'height': 600, 'deviceScaleFactor': 1}
await browser.close()

元素选择器

Page.querySelector()/Page.querySelectorAll()/Page.xpath()缩写为Page.J(), Page.JJ(), and Page.Jx().

xpath进阶:

  • starts-with 顾名思义,匹配一个属性开始位置的关键字
  • contains 匹配一个属性值中包含的字符串
  • text() 匹配的是显示文本信息: //a[contains(text(), "百度搜索")]

xpath的contains用法

元素操作:点击、输入

  • pyppeteer.input.Mouse
    • click(x: float*, y: float, options: dict = None*, ***kwargs)
      • button (str): left, right, or middle, defaults to left
        • 右键点击:await page.click('a#cccccc',{"button": "right"})
      • clickCount (int): defaults to 1.
      • delay (int|float): Time to wait between mousedown and mouseup in milliseconds. Defaults to 0.
    • down(options: dict = None, *kwargs) `
    • button (str): left, right, or middle, defaults to left.
    • clickCount (int): defaults to 1.
    • move(x: float, y: float, options: dict = None, **kwargs*)
    • p(options: dict = None, **kwargs*)
1
2
3
4
5
6
7
8
9
10
11
# 使用xpath
await page.waitForXPath('//input[contains(@id, "mat-input")]')
await asyncio.sleep(10)
inputs = await page.xpath('//input[contains(@id, "mat-input")]')
print(len(inputs))
await inputs[1].type('janetgao@indiana.edu')
await inputs[2].type('KAProject2021')
# 使用selector
await page.waitForSelector('#mat-input-1')
await page.type('#mat-input-1', 'janetgao@indiana.edu')
await page.type('#mat-input-2', 'KAProject2021')

launch

  • headless:无界面模式

  • devtools :启用开发者助手

  • userDataDir='./userdata': 记录些用户信息,比如cookies

  • dumpio=True 防止浏览器开多个页面而卡死

  • executablePath: 指定自定义的chrome路径

  • 参数args设置,可以类比selenium

    • --start-maximized: 页面全屏

    • --window-size={width},{height}:设置窗口大小,区别于页面的page.setViewport({'width': 1366, 'height': 768})

    • --disable-infobars:不显示信息栏 比如 chrome正在受到自动测试软件的控制

    • --incognito:百度搜到的chrome.exe无痕命令(selenium是如此,pyppeteer不推荐)

    • --user-agent: 比如'--user-agent=Mozilla/5.0'

    • --proxy-server={proxy}: 浏览器代理 配合某些中间人代理使用

    • --no-sandbox, # 取消沙盒模式 沙盒模式下权限太小

    • '--log-level=3'

    • '--load-extension={}'.format(chrome_extension)'--disable-extensions-except={}'.format(chrome_extension) # 加载插件

      1
      2
      3
      4
      5
      chrome_extension_path = "插件所在目录"
      args = [
      "--load-extension={}".format(chrome_extension_path),
      "--disable-extensions-except={}".format(chrome_extension_path),
      ]

      pyppeteer控制Chromium在隐身模式下启用插件

  • more: launch参数说明

执行JS:

  • page.evaluate('window.scrollBy(100, document.body.scrollHeight)')

  • await page.evaluate('alert("在浏览器执行js脚本!")')

    1
    2
    3
    # 相当于拿到了dom对象
    element = await page.J('#ul>a[name="tj_trtieba"]')
    print(await page.evaluate('el => el.innerHTML', element))
  • page.evaluateOnNewDocument(pageFunction[, …args]), 指定的函数在所属的页面被创建并且所属页面的任意script执行之前被调用。常用于修改页面JS环境。

    1
    2
    3
    4
    page = await browser.newPage()
    await page.setViewPort({'width': 1366, 'height': 768})
    await page.evaluateOnNewDocument('''() => {
    Object.defineProperty(navigator, 'webdriver', {get: () => false });

    附—ElementHandle元素操作:

    1. 获取元素边界框坐标:boundingBox(),返回元素的边界框(相对于主框架)=> x 坐标、 y 坐标、width、height
    2. 元素是否可见:isIntersectingViewport()
    3. 上传文件:uploadFile(*filpaths)
    4. ElementHandle 类 转 Frame类:contentFrame(),如果句柄未引用iframe,则返回None。
    5. 聚焦该元素:focus()
    6. 与鼠标相关:hover () ,将鼠标悬停到元素上面
    7. 与键盘相关:press (key[, options]),按键,key 表示按键的名称,option可配置:
      • text (string) - 如果指定,则使用此文本生成输入事件
      • delay (number) - keydown 和 keyup 之间等待的时间。默认是 0
      • page.keyboard.down('Shift'):按下shift

    more: 页面元素操作

安全性:

  1. 在使用 Pyppeteer 仍然会遇到无头浏览器检测,这里安利一个第三方库「pyppeteer-stealth」,「puppeteer-extra-plugin-stealth」引用Github上的说明「Applies various evasion techniques to make detection of headless puppeteer harder.」「A plugin for puppeteer-extra to prevent detection.」(Github地址:https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth )。可见,「pyppeteer-stealth」也是用于防止机器人检测的。用法也很简单,给page加上stealth
1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
from pyppeteer import launch
from pyppeteer_stealth import stealth

async def main():
browser = await launch(headless=True)
page = await browser.newPage()

await stealth(page) # <-- Here

await page.goto("https://bot.sannysoft.com/")
await browser.close()
asyncio.get_event_loop().run_until_complete(main())

这样就可以省去隐藏WebDriver等操作,可谓省时省力省心。

  1. await page.evaluateOnNewDocument('Object.defineProperty(navigator,"webdriver",{get:()=>undefined})')
  2. stealth.min.js最完美方案!模拟浏览器如何正确隐藏特征

无痕隐身模式访问

1
2
3
4
5
6
7
8
9
async def start_browser():
browser = await launch(**{"headless": False})
# 创建隐身上下文
browser_context = await browser.createIncognitoBrowserContext()
page = await browser_context.newPage()

await page.goto("http://www.baidu.com")
await browser.close()
return

较全的API说明:pyppeteer的基本使用页面信息、设置

问题:按钮不生效

网页输入账号和密码之前,按钮状态为disable=true,两个都输入后才能按下,而使用pyppeteer时,发现无论怎么尝试都不能使得其disable变化(用开发者助手修改后,无法生成正确的提交请求)。突然巧合之中,想到了是不是元素没加载成功,于是确实在开发者助手的console中看到有很多不支持的,比如angular、bootstrap……

于是百度问题,看看是不是什么设置没开,但都没找到。后来想到升级下pyppeteer版本会不会好一点,发现自己的版本为1.0.2,已经是最新的了(https://github.com/miyakogi/pyppeteer/issues/306)。由于不通过程序直接通过浏览器进入程序也出现了无法点击的问题。再加上下面有人提出chromium有点难下载,并通过源码找道了默认的配置和设置,于是经过一阵的折腾过后,我发现**“按钮无法点击”的问题就在于chromium版本过低**。

1
2
3
4
5
6
7
8
9
import pyppeteer.chromium_downloader
print('默认版本是:{}'.format(pyppeteer.__chromium_revision__))
print('可执行文件默认路径:{}'.format(pyppeteer.chromium_downloader.chromiumExecutable.get('win64')))
print('win64平台下载链接为:{}'.format(pyppeteer.chromium_downloader.downloadURLs.get('win64')))
"""
默认版本是:588429
可执行文件默认路径:C:\Users\mrli\AppData\Local\pyppeteer\pyppeteer\local-chromium\588429\chrome-win32\chrome.exe
win64平台下载链接为:https://storage.googleapis.com/chromium-browser-snapshots/Win_x64/588429/chrome-win32.zip
"""

登录这个storage的网站可以看到,上面所记录的chromium全都是2013年的版本,必然版本落后了。于是下载最新的chromium覆盖C:\Users\mrli\AppData\Local\pyppeteer\pyppeteer\local-chromium\588429\chrome-win32目录后再运行,按钮不能点击的问题就消失了!~😄

chromium下载地址:https://registry.npmmirror.com/binary.html?path=chromium-browser-snapshots/Win_x64/,注:chromium跟chromedriver不一样,他跟自己已经安装的chrome并没有关联。chromium自带浏览器引擎核心,所以直接下载最新的版本即可,比如我直接下载的就是999303。

启发于:使用pyppeteer 下载chromium 报错 或速度慢pyppeteer使用时常见的bug及解决办法【转载】

zsxq

操作word——docx

安装: pip install python-docx

  • 插入图片
1
2
3
4
5
6
7
8
doc = Document()
# 增加标题
doc.add_heading('python-docx 基础讲解(二)')

# 在文档中增加图片,并对设置图片大小
# 当只设置一个方向的长度(宽或高)时,另一方向会自动缩放
doc.add_picture('1.png',width=shared.Inches(1)) # 按英寸设置
doc.add_picture('1.png',height=shared.Cm(2)) # 按厘米设置
  • 插入节
1
2
3
4
5
6
7
add_heading(text=u'', level=1)	        # 写入标题段落
add_paragraph(text =u'',style=None) # 写入普通段落
add_picture(image_path_or_stream,width = None,height = None ) #插入指定图片
add_table(rows, cols) # 插入指定行数、列数的表格
add_section(self, start_type=2) # 添加节;
add_page_break(self) # 分页符;
add_paragraph(self, text='', style=None) # 添加段落;
  • 添加页眉
1
2
3
4
5
6
7
8
>>> document = Document()
# 每个节section都有其页眉header
>>> section = document.sections[0]
>>> header = section.header
>>> paragraph = header.paragraphs[0]
>>> paragraph.text = "Title of my document"
# 设置页眉是否连接上一节
>>> header.is_linked_to_previous = True

注:“页眉也是一个块级对象,里面也包含了 Paragraph 对象,” “所以对齐方式,文字格式设置方式和前文介绍一致。”

1
2
head_par = head.paragraphs[0]
head_par.add_run('页眉')
  • 修改页脚
1
2
3
4
5
font0 = sec0.footer  # 返回页脚对象
# 设置页脚
font0_par = font0.paragraphs[0]
font0_par.add_run('页脚')
# 注: 设置页脚按序列增加的方式暂未找到
  • 添加表格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
table1 = doc.add_table(2,3,style ='Table Grid' )
# 表格设置自动调整列宽,(默认也为真)
table1.autofit = True
# 为表格对象增加列
table1.add_column(shared.Inches(3)) # 需指定宽度
# 为表格对象增加行
table1.add_row() # 只能逐行添加
# 获取行对象
row0 = table1.rows[0]
# 获取列对象
col0 = table1.columns[0]
# 设置单元格对齐方式: 垂直对齐方式
from docx.enum.table import WD_ALIGN_VERTICAL
table1.cell(0,0).vertical_alignment = WD_ALIGN_VERTICAL.TOP
# 合并单元格
cell_new = table1.cell(2,0).merge(table1.cell(2,1))

注: 单元格内其实也是有 paragraph 对象的,即对单元格内字体设置方式,和对段落中文字格式设置方法一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
cell_par = cell_new.paragraphs[0] # 获取到对象
# 设置对齐方式
from docx.enum.text import WD_ALIGN_PARAGRAPH
cell_par.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 获取 run 对象
cell_run = cell_new.paragraphs[0].runs[0]
# 设置字体
cell_run.font.name = 'Times New Roman'
from docx.oxml.ns import qn
cell_run.font.element.rPr.rFonts.set(qn('w:eastAsia'),'楷体')
# 设置字体颜色
from docx.shared import RGBColor
cell_run.font.color.rgb = RGBColor(255,55,55) # 红色
  • 段落设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置段落缩进
from docx.shared import Inches
paragraph = document.add_paragraph("你说啥")
paragraph_format = paragraph.paragraph_format
paragraph_format.left_indent = Inches(0.5)
# 首行缩进
paragraph_format.first_line_indent = Inches(-0.25)
# 段落行距
from docx.shared import Length

paragraph.line_spacing_rule = WD_LINE_SPACING.EXACTLY #固定值
paragraph_format.line_spacing = Pt(18) # 固定值18磅
paragraph.line_spacing_rule = WD_LINE_SPACING.MULTIPLE #多倍行距
paragraph_format.line_spacing = 1.75 # 1.75倍行间距

更多操作样式见:

Save后操作Document对象再save会覆盖

Document对象在save之后,对其操作仍然有效,后续的操作在新的save中会保留上次save的记录,然后再添加新的(实则是覆盖)

1
2
3
4
5
6
7
8
9
10
d = Document()
resp = requests.get("https://images.zsxq.com/FodneHQIMESZXJWe4SahoZ_J_vGs?imageMogr2/auto-orient/quality/100!/ignore-error/1&e=1656604799&token=kIxbL07-8jAj8w1n4s9zv64FuZZNEATmlU_Vm6zD:onEkC9y8uloYeHTrEIpYk4dstaw=")
bf = BytesIO()
bf.write(resp.content)
d.add_picture(bf)
bf.close()
d.save("test.docx")
# save之后可以继续 **插入**, 然后再保存
d.add_paragraph("he")
d.save("test.docx")

编辑已存在的word文档

python-docx 不仅可以创建word文档,还可以编辑已存在的word文档。

1
2
3
4
5
6
from docx import Document

document = Document('existing-document-file.docx')
# 可以将 已存在 docx的内容保存到新的docx中
document.save('new-file-name.docx')
# 也可以 覆盖当前docx 成新的docx

但其实吧,这玩意儿只能编辑已存在的word文档,之所以有个“创建空白文档”的功能,只不过是拷贝一份空白word文档到工作区间,再在空白文档上编辑,看起来似乎是“创建空白文档”罢了。本质上还是编辑已存在的word文档…

打开一个word文档,编辑完后,一定要记得保存。如果保存文件名和原文件名不一样,则会另存为一份word文档;若文件名一样,则会不加提示的保存修改内容。

__slots__作用

用于需要维护两个Saver, 因此中间根据返回值来获得了枚举确定saver对象, 但是saver对象有个saver_name的属性并没有绑定起来, 因此想到
通过setattr来添加属性

1
2
3
4
5
6
7
8
9
10
11
class A:
# 使用__slots__后添加age属性报错: AttributeError: 'A' object has no attribute 'age'
__slots__ = ("name")

def __init__(self, name="cl"):
self.name = name
a = A()
setattr(a, "age", 18)
print(a)
print(a.__dict__)
print(a.age)

但不幸的是Document中也无法通过setattr添加属性, 原因应该是Document类也定义了__slots__

在此, 也明确下__slots__的特点:

  • __slots__数据类型为元组
  • __slots__只对当前类生效,对其子类不生效

BytesIO使用

说起IO,很多人首先想到的是磁盘中的文件,将磁盘中的文件读到内存以及内存内容写入文件,显然这种文件与内存的File-IO是种非常典型的IO。然而其实还存在一种内存和内存之间的IO,叫类文件对象(file-like object,在内存中创建,可以像文件一样被操作)。Python原生提供两个类文件对象:StringIO和BytesIO。

Q问题一:为什么要有内存级别的IO?

A:磁盘上的文件,就是将数据持久化到磁盘的一块区域,供后面重复使用。其优点就是持久化稳定不丢失,但是缺点也很明显,就是每次要使用都要从磁盘读入,相对内存而言很缓慢;如果只是短时间的重复利用,并不希望长期持久化,而且对速度的要求比较高,这时候就可以考虑缓存。说到缓存,很多朋友就想到redis,熟悉python的朋友还会想到装饰器和闭包函数。不过python已经原生为我们准备好了类文件对象:StringIO和BytesIO,用于临时缓冲

Q问题二:StringIO和BytesIO区别?

A:StringIO只能存储字符串,遇到从网络下载的图片视频等Bytes类型的内容就不行了,需要用到专门存储Bytes类型的BytesIO对象。

1
2
3
4
5
6
In [1]: from io import BytesIO           
In [2]: b=BytesIO()
In [3]: b.write('小付'.encode('utf-8'))
Out[3]: 6
In [4]: b.getvalue()
Out[4]: b'\xe5\xb0\x8f\xe4\xbb\x98'

Q问题三:那么这两个IO有什么作用呢?

A: BytesIO真正实用的地方还是在于存储图片视频等数据,比如网络请求了一个图片,拿到了resp.content的字节流,然后需要打开展示图片,但①pillow库打开的都是文件IO对象,无法直接通过字节流展示;②保存到本地作为文件打开又没必要,既占用存储空间,效率又低–>此时就可以写入到内存中的IO(BytesIO)中

Q问题四:那么StringIO和BytesIO跟普通用的f = open()的FileIO有什么相同点呢

A:StringIO和BytesIO仍然为IO,但拥有和读写文件具有一致的接口,其本质是将内存作为一个文件看待,只是在内存中操作str和bytes。

本项目案例:

python-docx库中Document对象可以通过add_picture()方法插入图片到word中,但是add_picture的参数为img_path或者stream,那么这个Stream流指的肯定是图片的字节流,但是直接通过bytes数据肯定是没办法传入的,因此可以将其写入到BytesIO中,供其读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 d = Document()
resp = requests.get(
"https://images.zsxq.com/FodneHQIMESZXJWe4SahoZ_J_vGs?imageMogr2/auto-orient/quality/100!/ignore-error/1&e=1656604799&token=kIxbL07-8jAj8w1n4s9zv64FuZZNEATmlU_Vm6zD:onEkC9y8uloYeHTrEIpYk4dstaw=")
bf = BytesIO()
bf.write(resp.content)
d.add_picture(bf)
# 一张图片
bf.close()
d.save("test.docx")
# save之后可以继续 **插入**, 然后再保存
d.add_paragraph("he")
d.save("test.docx")
# 一张图片,生成一个BytesIO
bf = BytesIO()
resp = requests.get(
"https://images.zsxq.com/FrqnoTw_W2Pqhn1107HLd3jveuSz?e=1656604799&token=kIxbL07-8jAj8w1n4s9zv64FuZZNEATmlU_Vm6zD:MJuNvGjp8XcfDNLPB5eLcouRFRg=")
bf.write(resp.content)
d.add_picture(bf)
d.save("test.docx")
bf.close()

注:通过bf.flush()无法清空内容, 必须close(), 否则bf.write(新图片)然后再添加时第二张图片内容仍然为第一张。

★ --> 网上没能找到清除BytesIO已有内容,因此结论为:一个bytes内容占用一个BytesIO,用完就关闭

loguru

loguru中提供全局的logger, 可以直接通过 from loguru import logger 来获得 logger, 但是这就意味着, 这样获得的都是都一个logger
对象, 因此不太方便用于分模块的日志管理, 而是全局的日志管理。

loguru的便捷体现在其开箱即用(少配置)的特点, 下面两行代码就能输出美观的日志输出

1
2
from loguru import logger
logger.info("hello")

其次, loguru配置日志文件也非常方便, 他提出属于为**“沉量”**sink
logger.add(sink='log.txt', format="{time} {level} {message}", filter="my_module", level="INFO")
将上面两个功能合起来,就能实现最基本的日志功能了。

1
2
3
from loguru import logger
logger.add(sink='log.log', format="{time} - {level} - {message}", level="INFO")
logger.info("That's it, beautiful and simple logging!")

可以用rotation、retention、compression进行日志窗口、更新、压缩管理。

1
2
3
4
5
logger.add("file_1.log", rotation="500 MB")    # 日志文件的窗口大小是500M
logger.add("file_2.log", rotation="12:00") # 每天中午12点创建新日志文件
logger.add("file_3.log", rotation="1 week") # 自动更新旧文件
logger.add("file_X.log", retention="10 days") # 清理旧文件
logger.add("file_Y.log", compression="zip") # 压缩文件

more:

  • 修改日志文字的颜色
    logger.add(sys.stdout, colorize=True, format="<green>{time}</green> <level>{message}</level>")
  • 使用enqueue,可以保证多线程安全、多进程安全
    logger.add("somefile.log", enqueue=True)
    参考: Loguru:优雅的Python程序日志

本项目自用loguru配置

  1. settings.py中找到项目绝对路径: basedir = Path(__file__).resolve().parent.parent
  2. 将loguru中的logger注册为全局LOG, 暴露给其他模块使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
from pathlib import Path

from loguru import logger
from helper.settings import basedir

log_folder_path = Path(basedir, 'logs')
log_path = Path(log_folder_path, time.strftime("%F")) # 日志根目录 ../logs/yyyy-mm-dd/
# 创建日志文件夹
log_path.mkdir(parents=True, exist_ok=True)
# 设置logger
logger.add(sink=Path(log_path, "log.txt"), format="{time} {level} {message}", level="INFO")
# 暴露LOG给其他模块使用
LOG = logger

retry模块

▲本项目中不知为何原因,options+get之后总会出现{'succeeded': False, 'code': 1059, 'info': '', 'resp_data': {}}的数据,但是进行重试发送请求后又能解决,因此retry模块在这个项目中发挥中较大的作用。

retry是一个用于错误处理的模块,功能类似try-except,但可以更加快捷方便的设置重试的次数,以及每次重试之间相隔的时间。

1
2
3
4
@retry()
def make_trouble():
'''Retry until succeed'''
print ('retrying...')
  • exceptions:传入指定的错误类型,默认为Exception,即捕获所有类型的错误,也可传入元组形式的多种指定错误类型

  • tries:定义捕获错误之后重复运行次数,默认为-1,即为无数次。

  • delay:定义每次重复运行之间的停顿时长,单位秒,默认为0,即无停顿。

  • backoff:呈指数增长的每次重复运行之间的停顿时长,需要配合delay来使用,譬如delay设置为3,backoff设置为2,则第一次间隔为3*2^0=3秒,第二次3*2^1=6秒,第三次3*2^2=12秒,以此类推,默认为1。

  • max_delay:定义backoff和delay配合下出现的最大等待时间上限,当

    delay*backoff**n大于max_delay时,等待间隔固定为该值而不再增长。

qkl

requests-html

安装: pip install requests-html,只支持python3.6及以上

由于其用了nuxt框架,因此有些路由数据保存在windows.__NUXT__.data中,而这些数据都是变量,无法直接通过正则提取出来,但是因为其可以在控制台输出已经渲染生成好的windows.__NUXT__.data数据,因此考虑到用chromium后执行渲染脚本应该可以实现。但之前有了解过requets-html,其相对于要开浏览器而言更加便捷一些,于是本次使用的是requests-html。

快速上手:

由于与requests库出于同一作者,因此API和requests库都差不多,主要的区别在于resp.html这个属性,它是整个requests_html库中最核心的一个类,负责对HTML进行解析。比如分页、渲染等功能都是由该对象提供的。

相比requests的新功能有:

  • 支持JavaScript
  • 支持页面解析:CSS选择器(又名jQuery风格, 感谢PyQuery)、支持Xpath选择器
  • 可自定义模拟User-Agent(模拟得更像真正的web浏览器)
  • 自动追踪重定向
  • 连接池与cookie持久化
  • 支持异步请求
  • 智能多页

获取链接

links和absolute_links两个属性分别返回HTML对象(URL)所包含的所有链接和绝对链接(均不包含锚点)(已经自动去掉了html标签)。

1
2
3
# 获取响应页面下的所有链接
print(r.html.links)
print(r.html.absolute_links)

获取元素

request-html支持CSS选择器(借助PyQuery)和XPATH(借助lxml)两种语法来选取HTML元素,我一般用xpath比较多,因此就介绍xpath了,selector的用法是类似的。

XPAT语法,需要另一个函数xpath的支持,它有4个参数:

  • selector,要用的XPATH选择器;
  • clean,布尔值,如果为真会忽略HTML中style和script标签造成的影响(原文是sanitize,大概这么理解);
  • first,布尔值,如果为真会返回第一个元素,否则会返回满足条件的元素列表;
  • _encoding,编码格式。
1
2
3
4
print(r.html.xpath("//div[@id='menu']", first=True).text)
print(r.html.xpath("//div[@id='menu']/a"))
# 如果XPATH中包含text()或@href这样的子属性,那么结果相应的会变成简单的字符串类型,而不是HTML元素。
print(r.html.xpath("//div[@class='content']/span/text()"))

元素内容

要搜索元素的文本内容,用search函数

1
2
print(e.search('还是{}没头脑')[0])
# 输出为: 那个

两个链接属性

1
2
print(e.links) # 相对路径 
print(e.absolute_links) # 绝对路径

JavaScript支持

有些网站是使用JavaScript渲染的,直接爬取页面结构或者是接口获得数据可能不是想要的数据,这样的网站requests-html也可以处理JS渲染。关键一步就是在HTML结果上调用一下render函数,它会在用户目录(默认是~/.pyppeteer/)中下载一个chromium,然后用它来执行JS代码。

1
2
3
h = session.get('http://python-requests.org/') 
h.html.render()
r.html.search('Python 2 will retire in only {months} months!')['months'] '<time>25</time>'

render函数还有一些参数:

  • retries: 加载页面失败的次数
  • script: 页面上需要执行的JS脚本(可选)
  • wait: 加载页面前的等待时间(秒),防止超时(可选)
  • scrolldown: 页面向下滚动的次数
  • sleep: 在页面初次渲染之后的等待时间
  • reload: 如果为假,那么页面不会从浏览器中加载,而是从内存中加载
  • keep_page: 如果为真,允许你用r.html.page访问页面

智能分页

1
2
3
rq = session.get("https://reddit.com") 
for html in rq.html:
print(html)

直接使用HTML

1
2
3
4
from requests_html 
import HTML doc = "<a href='https://httpbin.org'>"
html = HTML(html=doc)
print(html.links) {'https://httpbin.org'}

直接渲染JS代码

1
2
3
4
5
6
7
8
9
script = """ 
() => {
return {
width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, deviceScaleFactor: window.devicePixelRatio,
}
}
"""
val= html.render(script=script,reload=False)
print(val) print(html.html)

r.html.page与浏览器交互

requests-html将对chromium的键盘和鼠标API操作聚合在了r.html.page对象下

键盘事件

  • keyboard.down(‘键盘名称’):按下键盘不弹起(与键盘有点不太down(‘h’)只会出现一个h而不是hhhhhhh…)
  • keyboard.up(‘键盘名称’):抬起按键
  • keyboard.press(‘键盘名称’):按下+弹起
  • keyboard.type(‘输入的字符串内容’,{‘delay’:100}) delay为每个子输入后延迟时间单位为ms

鼠标事件

点击

  • click(‘css选择器’,{ ‘button’:‘left’, ‘clickCount’:1,‘delay’:0})
    • button为鼠标的按键left, right, or middle,
    • clickCount:点击次数默认次数为1
    • delay:点击延迟时间,单位是毫秒
  • mouse.click(x, y,{ ‘button’:‘left’, ‘clickCount’:1,‘delay’:0})
    • x,y:muber数据类型,代表点击对象的坐标

点下去不抬起

  • mouse.down({‘button’:xxx,clickCount:xxx})

抬起鼠标

  • mouse.up({‘button’:xxx,clickCount:xxx})

等待

  • waitFor(‘选择器, 方法 或者 超时时间’)
    • 选择器: css 选择器或者一个xpath 根据是不是//开头
    • 方法:时候此方法是page.waitForFunction()的简写
    • 超时时间:单位毫秒

等待元素加载: waitForSelector('css选择器')

获取x,y坐标

1
2
3
4
5
mydic =await r.html.page.evaluate('''() =>{ 
var a = document.querySelector('#kw') #对象的css选择器
var b = a.getBoundingClientRect()
return {'x':b.x,'y':b.y , 'width':b.width , 'height':b.height }
}''')

📖

request-html异步

1
2
3
4
5
6
7
8
9
10
from requests_html import AsyncHTMLSession
asession = AsyncHTMLSession()
async def get_pyclock(index):
r = await asession.get('http://httpbin.org/get')
await r.html.arender()
return r

results = asession.run(get_pyclock, get_pyclock,
get_pyclock) # 这里作者将同一个页面使用异步方式进行了3次渲染,但是实际上使用的时间并不是平时的3倍!可能只是比平时渲染一个页面多花了一点时间而已!这就是异步的好处!
print(results)

and:https://cloud.tencent.com/developer/article/1575104

charles+VPN无法共用——解决方案

由于两个都是代理型的软件,因此往往默认都指定了一个端口,导致另一个失效,所以需要通过设置让他们端口互知。

最近需要抓一个需要翻墙才能访问的网页的包,发现VPN直连时会导致 Fiddler 和 Charles 抓包工具无法正常进行抓包,网上找了以后发现了一些解决方案:Github:VPN直连,导致 Fiddler 和 Charles 抓包工具无法正常进行抓包解决方案 ——试了貌似没用、windows下,实现vpn访问下的charles抓包设置中无网络问题的解决——收此启发指导了在charles的Proxy->external proxy允许其他端口代理

1.找到VPN软件的代理端口proxy port

我这边使用的是vmess,可以在选项->参数设置中查看,需要明确的参数是端口和协议,我这边是10808和socks协议

vmess参数

2.设置charles:

Proxy->external proxy, 首先允许其他proxy,然后根据刚刚查看到的vmess端口和协议进行填写

charles

3.设置完成,开始抓包

完结撒花~

requests使用代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

cookies = {
# ...
}

headers = {
'authority': 'cn.v2ex.com',
# ...
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36',
}
# 核心是要配置下面的代理
http_proxy = "socks5h://127.0.0.1:10808"
https_proxy = "socks5h://127.0.0.1:10808"
proxies = {
"https": https_proxy,
"http": http_proxy
}

response = requests.get('https://cn.v2ex.com/about', cookies=cookies, headers=headers, proxies=proxies)

注:一开始在Sublime里运行的,结果一直在response.text时报编码错误,但是通过网页的content-type和meta charset进行确认过没问题,后来经过一个启发想到会不会是控制台有编码显示不了,于是在Pycharm中运行,成功!

aiohttp使用socks代理

from: https://pypi.org/project/aiohttp-socks/、https://www.cnblogs.com/john-xiong/p/13812567.html

  1. pip install aiohttp_socks

  2. 1
    2
    3
    4
    5
    6
    7
    8
    9
    connector = ProxyConnector.from_url('socks5://127.0.0.1:10808')

    async def getDataByChromeDriver(url):
    async with aiohttp.ClientSession(connector=connector) as session:
    async with session.get(url) as response:
    return await response.text()

    if __name__ == '__main__':
    loop.run_until_complete(asyncio.wait([getDataByChromeDriver(index) for title, index in title_list.items()]))
  3. 运行即可

request-html使用代理

Python爬虫一个requests_html模块足矣!(支持JS加载&异步请求)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from requests_html import AsyncHTMLSession

http_proxy = "socks5h://127.0.0.1:10808"
https_proxy = "socks5h://127.0.0.1:10808"
proxies = {
"https": https_proxy,
"http": http_proxy
}

session = AsyncHTMLSession()

async def getDataByChromeDriver(index: Union[int, str]):
response = await session.get('https://www.qkl123.com/sector/{}'.format(index), headers=headers, proxies=proxies)
# ...

Author: Mrli

Link: https://nymrli.top/2022/05/02/2022年5月劳动节爬虫项目记录/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
selenium不行的工作,pyppeteer上
NextPost >
mitmproxy拦截代理——PC版HttpCanary
CATALOG
  1. 1.
    1. 1.1. retry模块
    2. 1.2. 项目结构
    3. 1.3. pyinstaller
    4. 1.4. dataclass
      1. 1.4.0.1. dataclasses嵌套
  2. 1.5. toml语法学习
    1. 1.5.1. toml对象
  3. 1.6. 自用抢购尝试
  • 2. base数据库
    1. 2.1. 判断重定向
    2. 2.2. 图片懒加载
      1. 2.2.1. 本项目案例
    3. 2.3. pathlib库使用
    4. 2.4. 按住验证码
      1. 2.4.1. pyppeteer
      2. 2.4.2. 问题:按钮不生效
  • 3. zsxq
    1. 3.1. 操作word——docx
      1. 3.1.1. Save后操作Document对象再save会覆盖
      2. 3.1.2. 编辑已存在的word文档
    2. 3.2. __slots__作用
    3. 3.3. BytesIO使用
      1. 3.3.1. 本项目案例:
    4. 3.4. loguru
      1. 3.4.1. 本项目自用loguru配置
    5. 3.5. retry模块
  • 4. qkl
    1. 4.1. requests-html
      1. 4.1.1. r.html.page与浏览器交互
    2. 4.2. request-html异步
    3. 4.3. charles+VPN无法共用——解决方案
      1. 4.3.1. requests使用代理
      2. 4.3.2. aiohttp使用socks代理
      3. 4.3.3. request-html使用代理