diff --git a/LICENSE b/LICENSE
index ce3cfb5..261eeb9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,201 @@
-MIT License
-
-Copyright (c) 2017 Chen Qi
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/PageSpeedInsights/.gitignore b/PageSpeedInsights/.gitignore
new file mode 100644
index 0000000..c4c4ffc
--- /dev/null
+++ b/PageSpeedInsights/.gitignore
@@ -0,0 +1 @@
+*.zip
diff --git a/PageSpeedInsights/README.md b/PageSpeedInsights/README.md
new file mode 100644
index 0000000..126665c
--- /dev/null
+++ b/PageSpeedInsights/README.md
@@ -0,0 +1,310 @@
+
+
+使用 Google Cloud Scheduler, Pub/Sub, Functions , Storage 等云服务,搭建 PageSpeed Insights 前端网站网页的质量和性能 benchmark 定时审查系统。与 CI/CD 流程结合,定时大批量审查网站技术性能指标。
+
+### 1. PageSpeed Insights
+
+#### 1.1 简介
+
+PageSpeed Insights 是 Google 提供的一款网页性能检测优化工具,能够针对移动设备和桌面设备生成网页的实际性能报告,并提供关于如何改进相应网页的建议。它采用 Google Lighthouse 提供的各种最佳实践作为测试基准,使用 Blink 渲染工具(即 Google Chrome 的渲染引擎),模拟移动设备和桌面设备,抓取目标网站网页,进行优化分析。
+以下简称PSI。
+
+#### 1.2 版本历史
+
+版本 | 发布时间 | 功能更新
+--|--|--
+V5 | 2018年Q4 | 当前最新版本。2019.05.08更新使用 Lighthouse 5.0 作为其分析引擎。
+V4 | 2018年1月 | 2019年Q3之前停用
+V2 | 2015年1月| 已停用
+V1 | 更早期 | 已停用
+
+#### 1.3 分析报告组成
+
+#### 1.3.1 综合速度得分
+
+评分和等级:
+
++ 快 90分以上
++ 中等 50-90分
++ 慢 50分以下
+
+V5版本使用 Lighthouse 计算多项性能指标的综合加权得分。
+V4及之前版本结合 Chrome 用户体验报告数据库中的真实用户测速数据,计算评分和等级。主要参考以下两项指标。
+
++ FCP (First Contentful Paint)首次内容绘制,用于衡量用户何时看到来自相应网页的可见响应。所用时间越短,留住用户的可能性就越大。
++ DCL 文档内容加载,用于衡量何时完成 HTML 文档的加载和解析。所用时间越短,跳出率越低。
+
+#### 1.3.2 实测数据
+
+结合 Chrome 用户体验报告中的其他网页过去30天内的实测数据相比的得分。
+
+#### 1.3.3 实验室数据
+
+给出以下几项指标的耗时绝对值数据:
+
++ First Contentful Paint 首次内容绘制时间
++ First Meaningful Paint 首次有效绘制时间
++ Speed Index 速度指数
++ First CPU Idle 首次 CPU 闲置时间
++ Time to Interactive 可交互前的耗时
++ Estimated Input Latency 最长的潜在FID
+
+#### 1.3.4 关于如何加快网页加载速度的优化建议
+
+#### 1.3.5 关于Web开发最佳实践的详细诊断建议。
+
+#### 1.3.6 已通过的符合最佳实践的审查项
+
+#### 1.4 实际案例
+
+以携程机票H5航班动态首页的某线上版本为例,直观的查看分析报告:
+https://m.ctrip.com/webapp/flight/schedule/detail.html
+
+
+
+
+
+
+
+
+
+
+
+
+
+#### 1.5 使用方法
+
+PSI API是Google RESTful APIs之一, 仅需一次 HTTP 请求 ,应答返回一个 JSON Ojbect。使用极其简便。
+
+#### HTTP Request
+
+> GET https://www.googleapis.com/pagespeedonline/v5/runPagespeed
+
+必选参数1个:
+
++ `url`: 目标分析网页的链接
+
+可选参数6个:
+
++ category:`accessibility`,`best-practices`,`performance`,`pwa`,`seo`。默认是`performance`。
++ locale:返回结果文本的本地化语言版本。目前支持40种。默认英语`en`。
++ strategy:`desktop` 针对桌面浏览器进行优化分析,`mobile` 针对移动设备浏览器进行优化分析。
++ utm_campaign:广告系列名称
++ utm_source:广告系列来源
++ fields: 定制 Response 内容字段。
+
+#### HTTP Response
+
+返回一个 JSON Object ,字段内容较多,此处省略,详见官网文档。
+
+#### 最简单命令行调用
+
+> curl https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https://m.ctrip.com
+
+### 2. Google Cloude Platform (GCP)
+
+#### 2.1 系统流程图
+
+
+
+#### 2.2 Cloud Scheduler
+
+Cloud Scheduler 是 GCP 的一项全托管式企业级 cron 作业调度服务。支持 App Engine、Cloud Pub/Sub 和任意 HTTP 端点,允许作业触发 Compute Engine、Google Kubernetes Engine 和本地资源。
+使用 Google Cloud Console 创建Job。目标有3种:HTTP,Pub/Sub,App Engine HTTP。这里选择 Pub/Sub 。设置每天22:00自动触发。
+
+
+
+创建成功后查看部署状态,部署成功后可以直接“立即运行”,查看日志,确认运行正常。
+
+
+
+#### 2.3 Cloud Pub/Sub
+
+Cloud Pub/Sub 是 GCP 的一项简单、可靠、可伸缩,可以用作数据流分析和事件驱动型计算系统的基础。
+这里创建两个主题,`psi-job` 用于 Cloude Scheduler Job 的事件数据中转,`psi-single` 用于 Cloud Functions 的并发 HTTP 请求的事件数据中转。
+
+
+### 2.4 Cloud Functions
+
+实现并发大量网页的 PageSpeed Insights 检查,有多种方式。可以使用 Google App engine, Google Compute Engine。鉴于 PSI API 是上下文无关的简单 HTTP RESTful API,Cloud Functions Serverless 是最佳最简实现。
+Cloud Functions 是 GCP 的一项事件驱动型无服务器计算平台。通过构建多个分别专注于做好一件事的小型独立功能单元,再将这些功能单元组合成一个系统,实现快速开发和部署。支持在单个函数(而不是整个应用、容器或虚拟机)级构建和部署服务。
+
+#### 2.4.1 编写 Function
+
+目前支持以下几种方案:
+
+语言 | JavaScript
+--|--
+运行时 | Node.js 6(已弃用)、8、10(测试版)
+HTTP 框架 | Express
+HTTP 函数 | Express Request & Response Context
+后台函数 | (data, context, callback)
+依赖项管理 | npm/yarn + package.json
+
+语言 | Python
+--|--
+运行时 | 3.7.1
+HTTP 框架 | Flask
+HTTP 函数 | 入参:Flask Request Object。返回值:符合 Flask.make_response() 的任意对象。
+后台函数 | (data, context)
+依赖项管理 | pip + requirements.txt
+
+语言 | Go
+--|--
+运行时 | Go 1.11
+HTTP 框架 | http.HandlerFunc 标准接口
+HTTP 函数 | request: *http.Request. response: http.ResponseWriter.
+后台函数 | (ctx, Event)
+依赖项管理 | go.mod/vendor
+
+#### 2.4.2 部署 Function
+
+目前支持以下几种方式:
+
++ 从本地机器部署。`使用 gcloud 命令行工具。`
++ 通过源代码控制系统部署。`使用 Google Cloud Source Repositories ,通过 OAuth 关联源代码仓库(如 GitHub 或 Bitbucket)。`
++ 通过 GCP Console 部署。
+ + 网页内嵌编辑器.`直接在线编写函数代码。`
+ + 上传本地ZIP文件。`文件夹目录结构与上述依赖性管理的源码工程结构一致。`
+ + 导入 Cloud Storage 中的 ZIP 文件。`同上。`
+ + 引用 Google Cloud Source Repositories的源代码工程。
++ 通过CI/CD部署。`使用 Cloud Build 搭建持续集成和部署系统。`
+
+#### 2.4.3 监控 Function
+
+Google Stackdriver 提供了服务监控工具,包括 `Debugger,Monitoring,Trace,Logging, Error Reporting,Profiler`。
+
+### 3. PSI Functions 实现
+
+创建好一个 Scheduler Job 和两个 Pub/Sub 主题后,接下来实现两个对应的 Functions 。
+
+#### 3.1 psi-single function
+
+psi-single() 负责针对具体单一 URL ,调用 PSI API 获取 JSON 结果的功能。
+Google APIs 支持多种调用方式。
+
+**3.1.1 使用 `google api client` 。**
+通过 `Discovery API` ,获得已经封装好的 `Service` ,再调用具体接口。
+
+```Python
+from googleapiclient.discovery import build
+
+def run(url):
+ pagespeedonline = build(
+ serviceName = 'pagespeedonline',
+ version = 'v5',
+ developerKey = API_KEY
+ )
+ response = pagespeedonline.pagespeedapi().runpagespeed(url = url).execute()
+ print(response)
+ return 'OK'
+```
+
+**3.1.2 针对简单接口,直接调用 `HTTP RESTful API` 。**
+
+```Python
+import requests
+GAPI_PSI = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
+
+def run(url):
+ try:
+ payload = {"url": url,
+ "key": API_KEY
+ }
+ with requests.Session() as session:
+ response = session.get(url=GAPI_PSI, params=payload)
+ print(response.status_code)
+ print(response.json())
+ except requests.RequestException as _e:
+ print(_e)
+ return 'OK'
+```
+
+**3.1.3 实现 Pub/Sub 主题的订阅**
+订阅消息 `event` 的格式详见官网文档,其中 data 属性是一段 `base64` 编码的 `ByteArray` ,承载了实际的数据内容。
+
+```Python
+import base64
+
+def run_pubsub(event, context):
+ pubsub_message = base64.urlsafe_b64decode(event['data']).decode('utf-8')
+ return run(pubsub_message)
+```
+
+#### 3.2 psi-job function
+
+psi-job() 由 Scheduler Job 触发,将所有需审查的 URL 以 Pub/Sub 事件形式,并行分发给 psi-single() 。
+
+```Python
+from google.cloud import pubsub_v1
+
+def run(event, context):
+ publisher = pubsub_v1.PublisherClient()
+ topic = publisher.topic_path(PROJECT_ID, TOPIC_NAME)
+ for url in URL_DICT:
+ data = url.encode('utf-8')
+ publisher.publish(topic, data)
+ return 'OK'
+```
+
+#### 3.3 环境变量和依赖项
+
+为了避免安全敏感信息泄漏,可以将关键信息写入 Functions 环境变量和本地环境变量(本地开发调试使用)。
+上述代码中 `API_KEY, PROJECT_ID` 等数据通过 `os.getenv()` 获取。
+Cloude Functions 已内置常用依赖库,详见官网文档。如需增加依赖项,配置各语言对应的工程文件。上述代码引用了两个依赖库。
+
+```Python
+# requirements.txt
+# Function dependencies
+requests==2.21.0
+google-cloud-pubsub==0.40.0
+```
+
+### 4. Storage
+
+上述代码中的 `print()` 会写入 StackDriver 日志库,供后续过滤分析。鉴于每一个 URL 的审查结果是一个 JSON Object 字符串,可以进一步写入 BigTable , 使用 BigQuery 进行查询分析,再进一步导入 Google Data Studio , 进行可视化报表展示。
+这里使用 Cloud Storage 存储 JSON 字符串为单一文件。
+
+```Python
+from urllib import parse
+from google.cloud import storage
+from google.cloud.storage import Blob
+
+def save(url, report):
+ '''Save to https://console.cloud.google.com/storage/browser/[bucket-id]/'''
+ client = storage.Client()
+ bucket = client.get_bucket("psi-report")
+ blob = Blob(f"${parse.quote_plus(url)}.json", bucket)
+ blob.upload_from_string(report, "application/json")
+```
+
+添加依赖项。
+
+```Python
+# requirements.txt
+# Function dependencies
+google-cloud-storage==1.15.0
+```
+
+### 5. 源代码
+
+https://github.com/9468305/python-script/tree/master/PageSpeedInsights
+
+### 6. 文档链接
+
+1. PageSpeed Insights
+https://developers.google.com/speed/pagespeed/insights
+2. Google Lighthouse
+https://developers.google.com/web/tools/lighthouse/
+3. Google Cloud Scheduler
+https://cloud.google.com/scheduler/
+4. Google Cloud Pub/Sub
+https://cloud.google.com/pubsub/
+5. Google Cloud Functions
+https://cloud.google.com/functions/
+6. Google Cloud Storage
+https://cloud.google.com/storage/
+7. Google Cloud Build
+https://cloud.google.com/cloud-build/
+8. Google Stackdriver
+https://cloud.google.com/stackdriver/
diff --git a/PageSpeedInsights/image/1_score.png b/PageSpeedInsights/image/1_score.png
new file mode 100644
index 0000000..583b107
Binary files /dev/null and b/PageSpeedInsights/image/1_score.png differ
diff --git a/PageSpeedInsights/image/2_real_data.png b/PageSpeedInsights/image/2_real_data.png
new file mode 100644
index 0000000..83a5eae
Binary files /dev/null and b/PageSpeedInsights/image/2_real_data.png differ
diff --git a/PageSpeedInsights/image/3_lab_data.png b/PageSpeedInsights/image/3_lab_data.png
new file mode 100644
index 0000000..237b515
Binary files /dev/null and b/PageSpeedInsights/image/3_lab_data.png differ
diff --git a/PageSpeedInsights/image/4_opportunities.png b/PageSpeedInsights/image/4_opportunities.png
new file mode 100644
index 0000000..5b4d5ac
Binary files /dev/null and b/PageSpeedInsights/image/4_opportunities.png differ
diff --git a/PageSpeedInsights/image/5_diagnostics.png b/PageSpeedInsights/image/5_diagnostics.png
new file mode 100644
index 0000000..b6cb15f
Binary files /dev/null and b/PageSpeedInsights/image/5_diagnostics.png differ
diff --git a/PageSpeedInsights/image/6_passed_audits.png b/PageSpeedInsights/image/6_passed_audits.png
new file mode 100644
index 0000000..615c70b
Binary files /dev/null and b/PageSpeedInsights/image/6_passed_audits.png differ
diff --git a/PageSpeedInsights/image/gcp_cs_create.jpg b/PageSpeedInsights/image/gcp_cs_create.jpg
new file mode 100644
index 0000000..5cdd3ca
Binary files /dev/null and b/PageSpeedInsights/image/gcp_cs_create.jpg differ
diff --git a/PageSpeedInsights/image/gcp_cs_list.jpg b/PageSpeedInsights/image/gcp_cs_list.jpg
new file mode 100644
index 0000000..869c367
Binary files /dev/null and b/PageSpeedInsights/image/gcp_cs_list.jpg differ
diff --git a/PageSpeedInsights/image/gcp_pubsub_topic_list.jpg b/PageSpeedInsights/image/gcp_pubsub_topic_list.jpg
new file mode 100644
index 0000000..b92fe4c
Binary files /dev/null and b/PageSpeedInsights/image/gcp_pubsub_topic_list.jpg differ
diff --git a/PageSpeedInsights/image/psi.mermaid.svg b/PageSpeedInsights/image/psi.mermaid.svg
new file mode 100644
index 0000000..ad409e3
--- /dev/null
+++ b/PageSpeedInsights/image/psi.mermaid.svg
@@ -0,0 +1,360 @@
+
\ No newline at end of file
diff --git a/PageSpeedInsights/image/psi.png b/PageSpeedInsights/image/psi.png
new file mode 100644
index 0000000..ec91fba
Binary files /dev/null and b/PageSpeedInsights/image/psi.png differ
diff --git a/PageSpeedInsights/job.py b/PageSpeedInsights/job.py
new file mode 100644
index 0000000..06b9b95
--- /dev/null
+++ b/PageSpeedInsights/job.py
@@ -0,0 +1,24 @@
+#!/usr/local/bin/python3
+# -*- coding: utf-8 -*-
+'''PageSpeed Insights Job + Google Cloud Functions'''
+import os
+from google.cloud import pubsub_v1
+
+PROJECT_ID = os.getenv("GCP_PROJECT_ID")
+TOPIC_NAME = "psi-single"
+
+URL_DICT = ["https://m.ctrip.com/webapp/flight/schedule/detail.html"]
+
+def run(event, context):
+ publisher = pubsub_v1.PublisherClient()
+ topic = publisher.topic_path(PROJECT_ID, TOPIC_NAME)
+ for url in URL_DICT:
+ data = url.encode('utf-8')
+ publisher.publish(topic, data)
+ return 'OK'
+
+def test_job():
+ print('TODO')
+
+if __name__ == "__main__":
+ test_job()
diff --git a/PageSpeedInsights/main.py b/PageSpeedInsights/main.py
new file mode 100644
index 0000000..382c2cc
--- /dev/null
+++ b/PageSpeedInsights/main.py
@@ -0,0 +1,11 @@
+#!/usr/local/bin/python3
+# -*- coding: utf-8 -*-
+
+import psi
+import job
+
+def psi_pubsub(event, context):
+ psi.run_pubsub(event, context)
+
+def job_pubsub(event, context):
+ job.run(event, context)
diff --git a/PageSpeedInsights/psi.mermaid b/PageSpeedInsights/psi.mermaid
new file mode 100644
index 0000000..e25ec67
--- /dev/null
+++ b/PageSpeedInsights/psi.mermaid
@@ -0,0 +1,38 @@
+graph TB
+ Job(定时作业任务)
+ CPS_Job_P(Job Publisher)
+ CPS_Job_S(Job Subscriber)
+ CPS_PSI_P(PSI Publisher)
+ CPS_PSI_S(PSI Subscriber)
+ CF_Job(Job Function Service)
+ CF_PSI(PSI Function Service)
+ GAPI_PSI(PageSpeed Insights API)
+
+ Job -->|Push| CPS_Job_P
+ CPS_Job_S -->|Push| CF_Job
+ CF_Job -->| 并发 HTTP | CPS_PSI_P
+ CPS_PSI_S -->| 并发 Push | CF_PSI
+ CF_PSI -->| 并发 HTTP Request | GAPI_PSI
+
+ subgraph Cloud Scheduler
+ Job
+ end
+
+ subgraph Cloud Pub/Sub
+ subgraph Job Pub/Sub
+ CPS_Job_P --> CPS_Job_S
+ end
+
+ subgraph PSI Pub/Sub
+ CPS_PSI_P --> CPS_PSI_S
+ end
+ end
+
+ subgraph Cloud Functions
+ CF_Job
+ CF_PSI
+ end
+
+ subgraph Google APIs
+ GAPI_PSI
+ end
\ No newline at end of file
diff --git a/PageSpeedInsights/psi.py b/PageSpeedInsights/psi.py
new file mode 100644
index 0000000..df3b20c
--- /dev/null
+++ b/PageSpeedInsights/psi.py
@@ -0,0 +1,69 @@
+#!/usr/local/bin/python3
+# -*- coding: utf-8 -*-
+'''PageSpeed Insights Single + Google Cloud Functions'''
+import os
+import base64
+from urllib import parse
+import requests
+from google.cloud import storage
+from google.cloud.storage import Blob
+
+# Access Token, generated from GCP Console Credentials page.
+API_KEY = os.getenv('GCP_API_KEY')
+
+GAPI_PSI = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
+
+SESSION = requests.Session()
+
+PROXIES = None
+
+
+def save(url, report):
+ '''Save to https://console.cloud.google.com/storage/browser/[bucket-id]/'''
+ client = storage.Client()
+ bucket = client.get_bucket("psi-report")
+ blob = Blob(f"${parse.quote_plus(url)}.json", bucket)
+ blob.upload_from_string(report, "application/json")
+
+
+def run(url):
+ try:
+ payload = {"url": url,
+ "category": "performance",
+ "locale": "zh-CN",
+ "strategy": "mobile",
+ "key": API_KEY
+ }
+ response = SESSION.get(url=GAPI_PSI, params=payload, proxies=PROXIES)
+ print(response.status_code)
+ if 200 == response.status_code:
+ save(url, response.text)
+ except requests.RequestException as _e:
+ print(_e)
+ return 'OK'
+
+
+def run_pubsub(event, context):
+ pubsub_message = base64.urlsafe_b64decode(event['data']).decode('utf-8')
+ return run(pubsub_message)
+
+
+def test_run_http(test_url):
+ run(test_url)
+
+
+def test_run_pubsub(test_url):
+ event = {"data": base64.urlsafe_b64encode(test_url.encode('utf-8'))}
+ context = None
+ run_pubsub(event, context)
+
+
+if __name__ == "__main__":
+ _proxy = os.getenv("HTTP_PROXY")
+ PROXIES = {
+ "http": _proxy,
+ "https": _proxy,
+ }
+ _test_url = "https://m.ctrip.com/webapp/flight/schedule/detail.html"
+ test_run_http(_test_url)
+ test_run_pubsub(_test_url)
diff --git a/PageSpeedInsights/requirements.txt b/PageSpeedInsights/requirements.txt
new file mode 100644
index 0000000..536e57a
--- /dev/null
+++ b/PageSpeedInsights/requirements.txt
@@ -0,0 +1,6 @@
+# Function dependencies
+requests==2.31.0
+google-cloud-pubsub==0.40.0
+google-cloud-storage==1.15.0
+#google-cloud-bigtable==0.32.1
+#google-cloud-core==0.29.1
\ No newline at end of file
diff --git a/PageSpeedInsights/zip.sh b/PageSpeedInsights/zip.sh
new file mode 100755
index 0000000..ea6f363
--- /dev/null
+++ b/PageSpeedInsights/zip.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+# -*- coding: utf-8 -*-
+
+# chmod +x ./zip.sh
+zip -r functions.zip ./ -x *.DS_Store*
diff --git a/README.md b/README.md
index eef9af7..edf4744 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,48 @@
-## My Python Script
+My Python Script
+----
+
1. [auc_pr_roc](/auc_pr_roc)
-Python scikit-learn计算PR ROC曲线AUC值。
-用于 `携程旅行网 云海竞赛平台` [携程机票航班延误预测算法大赛](https://yunhai.ctrip.com/Games/11),竞赛算法结果核算。
-2. [cross_fetch](/cross_fetch)
-Python实现 - 一种基于搜索引擎交叉验证的网站数据采集方法
-3. [excel_combine](/excel_combine)
-Python实现 - Excel多文件一键自动合并。
-4. [geetest_offline](/geetest_offline)
-Python破解GeeTest滑块验证码offline V5.10.10,以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)网站为例。
-5. [geetest_offline_gd](/geetest_offline/README_gd.md)
-Python爬虫 - http://gd.gsxt.gov.cn 企业详细信息。
-6. [geetest_online](/geetest_online)
-Python破解GeeTest滑块验证码online,以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)网站为例。
-7. [gitstats](/gitstats)
-Python实现 - Git commit log统计分析。
-8. [gsxt_mobile](/gsxt_mobile)
-Python爬虫 - 国家企业信用信息公示系统 App,通过 App HTTP API 查询企业信息。
-9. [lagou](/lagou)
-Python爬虫 - Selenium [拉勾网](https://www.lagou.com) 数据采集。
-10. [level](/level)
-Python leveldb Utils 常用方法封装。
-11. [nacao_v1](/nacao_v1)
-Python爬虫 - [全国组织结构代码管理中心](http://www.nacao.org.cn)V1.0。
-12. [nacao_v2](/nacao_v2)
-Python爬虫 - [全国组织结构代码管理中心](http://www.nacao.org.cn)V2.0。
+Python scikit-learn计算PR ROC曲线AUC值。
+2. [excel_combine](/excel_combine)
+Python实现 - Excel多文件一键自动合并。
+3. [geetest_offline](/geetest_offline)
+Python破解GeeTest滑块验证码offline V5.10.10,以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)网站为例。
+4. [geetest_offline_gd](/geetest_offline/README_gd.md)
+Python爬虫 - [国家企业信用信息公示系统(广东)](http://gd.gsxt.gov.cn) 企业详细信息。
+5. [geetest_online](/geetest_online)
+Python破解GeeTest滑块验证码online,以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)网站为例。
+6. [gitstats](/gitstats)
+Python实现 - Git commit log统计分析。
+7. [gsxt_mobile](/gsxt_mobile)
+Python爬虫 - 国家企业信用信息公示系统 App,通过 App HTTP API 查询企业信息。
+8. [lagou](/lagou)
+Python爬虫 - Selenium [拉勾网](https://www.lagou.com) 数据采集。
+9. [level](/level)
+Python leveldb Utils 常用方法封装。
+10. [nacao_v1](/nacao_v1)
+Python爬虫 - [全国组织结构代码管理中心](http://www.nacao.org.cn)V1.0。
+11. [nacao_v2](/nacao_v2)
+Python爬虫 - [全国组织结构代码管理中心](http://www.nacao.org.cn)V2.0。
+12. [MonkeyRunner](/monkeyrunner)
+MonkeyRunner is DEAD!
+13. [PageSpeed Insights](/PageSpeedInsights)
+前端DevOps之PageSpeed Insights - 使用 Google Cloud Scheduler, Pub/Sub, Functions , Storage 等云服务,搭建 PageSpeed Insights 前端网站网页的质量和性能 benchmark 定时审查系统。与 CI/CD 流程结合,定时大批量审查网站技术性能指标。
+
+License
+----
+
+```txt
+ Copyright 2017 ChenQi
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+```
diff --git a/auc_pr_roc/README.md b/auc_pr_roc/README.md
index 9319201..afbe63b 100644
--- a/auc_pr_roc/README.md
+++ b/auc_pr_roc/README.md
@@ -1,21 +1,24 @@
-### 背景
-[携程旅行网 云海竞赛平台](https://yunhai.ctrip.com)举办算法竞赛,携程机票BU与飞常准合作命题[携程机票航班延误预测算法大赛](https://yunhai.ctrip.com/Games/11),希望以此提升航班延误的预测准确性。
-由于云海平台仅支持Python语言,原算法使用R语言实现,因此实现一份Python版,用于竞赛算法的结果核算。
-源数据样本csv格式说明:[航班动态起降数据集](https://www.kesci.com/apps/home/dataset/59793a5a0d84640e9b2fedd3)。
-提交预测样本csv格式说明:[submission_sample.csv](http://ofy9izzlw.bkt.clouddn.com/ctrip_fligtht/submission_sample.csv)。
-示例:
-```
-Flightno FlightDepcode FlightArrcode PlannedDeptime PlannedArrtime prob
-CA1351 PEK CAN 1496273700 1496285700 0.041386555
-8L9647 KMG HIA 1496272200 1496282400 0.022590361
-CZ6299 DLC SZX 1496274000 1496286900 0.025210084
-HU7377 URC CKG 1496273700 1496287500 0.106757728
-```
-本次比赛采用PR曲线的AUC(baseline:auc=0.45)。
-评估指标参考文献:http://mark.goadrich.com/articles/davisgoadrichcamera2.pdf
-
+### 背景
+
+2017年某算法竞赛平台出题,飞机航班延误预测算法,需要验收提交答案的准确性。
+
+源数据样本csv格式:失效,省略。
+提交预测样本csv格式说明:失效,省略。
+示例:
+
+Flightno | FlightDepcode | FlightArrcode | PlannedDeptime | PlannedArrtime | prob
+-- | -- | -- | -- | -- | --
+CA1351 | PEK | CAN | 1496273700 | 1496285700 | 0.041386555
+8L9647 | KMG | HIA | 1496272200 | 1496282400 | 0.022590361
+CZ6299 | DLC | SZX | 1496274000 | 1496286900 | 0.025210084
+HU7377 | URC | CKG | 1496273700 | 1496287500 | 0.106757728
+
+本次比赛采用PR曲线的AUC(baseline:auc=0.45)。评估指标参考:[The Relationship Between Precision-Recall and ROC Curves](http://mark.goadrich.com/articles/davisgoadrichcamera2.pdf)
+
### 实现
-csv文件读取使用pandas库。
+
+csv文件读取使用pandas库。
+
```Python
def load_label_prob(real_csv, result_csv):
'''读取real.csv和result.csv表格数据的label数组和prob数组'''
@@ -28,7 +31,9 @@ def load_label_prob(real_csv, result_csv):
prob[_i] = round(_e, 4)
return label, prob
```
+
PR曲线AUC值计算使用sklearn库。
+
```Python
'''使用real.csv和result.csv列数据,计算PR曲线的AUC值'''
precision, recall, _thresholds = metrics.precision_recall_curve(label, prob)
@@ -37,6 +42,7 @@ return area
```
附:ROC曲线的AUC值计算。
+
```Python
'''使用real.csv和result.csv列数据,计算ROC曲线的AUC值'''
area = metrics.roc_auc_score(label, prob)
@@ -44,14 +50,15 @@ return area
```
### 环境搭建
-Windows的scikit-learn库环境搭建略繁琐,对NumPy和SciPy版本有要求。
-因此直接使用 http://www.lfd.uci.edu/~gohlke/pythonlibs/ 的第三方预编译库。
-```
+
+scikit-learn Windows 环境搭建略繁琐,对 NumPy 和 SciPy 版本有要求。
+因此直接使用[第三方预编译库](http://www.lfd.uci.edu/~gohlke/pythonlibs/)。
+
+```bash
pip install http://www.lfd.uci.edu/~gohlke/pythonlibs/ru4fxw3r/numpy-1.13.1+mkl-cp36-cp36m-win32.whl
pip install http://www.lfd.uci.edu/~gohlke/pythonlibs/ru4fxw3r/scipy-0.19.1-cp36-cp36m-win32.whl
pip install pandas
pip install scikit-learn
```
-### 源码见GitHub
-https://github.com/9468305/python-script/blob/master/auc_pr_roc/
+### [GitHub源码](https://github.com/9468305/python-script/blob/master/auc_pr_roc/)
diff --git a/cross_fetch/README.md b/cross_fetch/README.md
deleted file mode 100644
index debf598..0000000
--- a/cross_fetch/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-### 一种基于搜索引擎交叉验证的网站数据采集方法
-TODO
diff --git a/cross_fetch/cf.py b/cross_fetch/cf.py
deleted file mode 100644
index 8030bc1..0000000
--- a/cross_fetch/cf.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/local/bin/python3
-# -*- coding: utf-8 -*-
-'''
-Cross Search and Verification by Google Search Engine.
-Thanks to https://so.ctool.cc/api/
-'''
-
-import base64
-import json
-import logging
-from logging import NullHandler
-import requests
-
-logging.getLogger(__name__).addHandler(NullHandler())
-logging.basicConfig(level=logging.DEBUG)
-
-CTOOL_API = r'https://so.ctool.cc/api/api.php?ver=3&kw={}&page={}&num={}&site={}'
-CTOOL_PAGE = 1
-CTOOL_NUM = 20
-
-
-def ctool(session, keyword, page, num, site):
- '''
- GET ctool url + params
- '''
- kw_string = ' '.join(keyword)
- kw_param = base64.b64encode(kw_string.encode()).decode()
- _url = CTOOL_API.format(kw_param, page, num, site)
- _response = session.get(_url)
- logging.debug('Response code = ' + str(_response.status_code))
- if _response.status_code != 200:
- return None
- _json_text = base64.b64decode(_response.text).decode()
- _json_obj = json.loads(_json_text)
- logging.debug(_json_obj)
- return _json_obj
-
-
-def cross_fetch(keyword, site):
- '''Cross fetch keyword by Search Engine.'''
- with requests.Session() as session:
- fetch = ctool(session, keyword, CTOOL_PAGE, CTOOL_NUM, site)
- keyword.reverse()
- cross = ctool(session, keyword, CTOOL_PAGE, CTOOL_NUM, site)
- #TODO Add cross verify
- #TODO ctool API fail
-
-
-if __name__ == "__main__":
- KEYWORD = ['腾讯科技(深圳)有限公司', '统一社会信用代码']
- SITE = ''
- cross_fetch(KEYWORD, SITE)
diff --git a/excel_combine/README.md b/excel_combine/README.md
index 1a7282a..e0de6b7 100644
--- a/excel_combine/README.md
+++ b/excel_combine/README.md
@@ -1,10 +1,12 @@
-工作中经常遇到一种繁琐的事情。例如绩效考核,晋升调薪,固定资产盘点,团建出游时,部门秘书发来一个全部门的Excel文件,各小组Leader拆分出自己团队的Excel文件。下发给每个成员。成员填写完毕后发回给小组Leader,小组Leader汇总后发给部门秘书。部门秘书再汇总各小组Excel到一个完整的大Excel文件。
-**吐槽:由于种种原因,我司内网Web OA系统建设不足,很多事情都以Excel分发和汇总。**
+工作中经常遇到一些繁琐事情,例如绩效考核,晋升调薪,固定资产盘点,团建出游时,部门秘书发来一个全部门的Excel文件,各小组Leader拆分出自己团队的Excel文件。下发给每个成员。成员填写完毕后发回给小组Leader,小组Leader汇总后发给部门秘书。部门秘书再汇总各小组Excel到一个完整的大Excel文件。
一直没有找到方便好用的Excel多文件合并工具。于是自己动手撸一个Python脚本,一键自动合并。
+**吐槽:由于种种原因,我司内网Web OA系统建设不足,很多事情都以Excel分发和汇总。**
### 一键执行
+
为了方便非技术人员使用,不添加任何启动参数,直接将脚本放在Excel文件夹根目录,双击执行。
也不需要指定合并汇总文件名,`combine.xlsx`这个名字的重复率很低,使用者留意即可。
+
```Python
if __name__ == "__main__":
FROM_DIR = os.getcwd()
@@ -13,11 +15,14 @@ if __name__ == "__main__":
```
### openpyxl
+
目前主流Office版本支持Excel 2010格式,即xlsx后缀名。如果源文件是xls后缀名,直接另存为xlsx即可。因此使用openpyxl库读写Excel文件,忽略xls后缀名文件。
[openpyxl - A Python library to read/write Excel 2010 xlsx/xlsm files](https://openpyxl.readthedocs.io/en/default/)
### 遍历文件夹,查找Excel文件
+
使用os.walk()
+
```Python
_results = []
for _root, _dirs, _files in os.walk(from_dir):
@@ -27,8 +32,10 @@ _results = []
return _results
```
-### 注意:删除合并汇总文件,即combine.xlsx。
+### 注意:删除合并汇总文件,即combine.xlsx
+
合并之前,删除结果文件,以防数据错误。
+
```Python
_result = search_file(from_dir)
try:
@@ -39,16 +46,19 @@ return _result
```
### 多文件重复数据清理
+
+ 如何确定Excel中每行数据是唯一的?
+ 如何检测重复的Excel文件(例如文件名不同,内容相同)?
+ 如何合并不同格式(行,列)的Excel文件?
这里设定一些潜规则:
+
+ Excel第一行必须是Title标题。
+ Excel第一列必须是唯一Key。例如工号,邮箱等全局唯一值。
因此使用 Python 内建的 collections 集合模块的 OrderedDict 有序字典,以第一列为Key。
完整的读取Excel文件并建立内存字典如下:
+
```Python
_wb = load_workbook(excel_file, read_only=True)
_ws = _wb.active
@@ -67,14 +77,17 @@ _wb.close()
return _title, _items
```
-### 如何判断2个字典的元素一致?
+### 如何判断2个字典的元素一致
+
Python內建了强大的[operator](https://docs.python.org/3/library/operator.html)。
+
```Python
if not operator.eq(dict_src, dict_dst):
print('Warning: dict elements are different!')
```
### 最后把数据写入Excel文件
+
```Python
_wb = Workbook()
_ws = _wb.active
@@ -85,8 +98,8 @@ _wb.save(excel_file)
```
### 遗留的坑
+
+ 没有处理多个Sheet的Case。
+ 没有处理Excel公式计算。
-### 源码见GitHub
-https://github.com/9468305/python-script/tree/master/excel_combine
+### [GitHub源码](https://github.com/9468305/python-script/tree/master/excel_combine)
diff --git a/geetest_offline/README.md b/geetest_offline/README.md
index 84e34e5..73024a1 100644
--- a/geetest_offline/README.md
+++ b/geetest_offline/README.md
@@ -1,14 +1,17 @@
-## GeeTest滑块验证码offline模式的破解
+# GeeTest滑块验证码offline模式的分析
+
GeeTest滑块验证码通过机器学习检查鼠标行为轨迹,识别人工或机器行为。
online在线验证的流程,目前最全面的分析文档详见 [https://zhuanlan.zhihu.com/windev](https://zhuanlan.zhihu.com/windev) 。
online模式的验证流程,网站后台与GeeTest后台 [http://api.geetest.com](http://api.geetest.com) 进行通讯验证。浏览器前端仅做数据采集和简单加密传输。
offline模式的离线验证,网站后台自行验证,GeeTest后台 [http://static.geetest.com](http://static.geetest.com) 仅提供滑块验证码图片下载功能。浏览器前端做数据采集和本地验证。
在安全性上,online模式相对非常可靠,offline模式仅仅是障眼法。
-### 1. 测试网站
-以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)为例:
-主站使用geetest 5.10.10 online 在线验证模式。各省市站点使用的版本和模块略有差异。
-**geetest offline 5.9.0**
+### 1. 测试网站
+
+以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)为例:主站使用 geetest 5.10.10 online 在线验证模式。各省市站点使用的版本和模块略有差异。
+
+**geetest offline 5.9.0**
+
+ 上海
+ 河北
+ 内蒙古
@@ -25,76 +28,108 @@ offline模式的离线验证,网站后台自行验证,GeeTest后台 [http://
+ 青海
+ 宁夏
-**geetest offline 5.10.10**
+**geetest offline 5.10.10**
+
+ 贵州
+ 陕西
### 2. offline模式验证流程
-以上海站点为例 [http://sh.gsxt.gov.cn](http://sh.gsxt.gov.cn)
-#### 2.1 GET 首页 http://sh.gsxt.gov.cn/notice/
-返回HTML页面,解析得到session.token。
-#### 2.2 GET register http://sh.gsxt.gov.cn/notice/pc-geetest/register
+
+以[上海站点](http://sh.gsxt.gov.cn)为例。
+
+#### 2.1 GET 首页 http://sh.gsxt.gov.cn/notice/
+
+返回HTML页面,解析得到session.token。
+
+#### 2.2 GET register http://sh.gsxt.gov.cn/notice/pc-geetest/register
+
返回JSON数据
-```
+
+```json
{
- "success":0,
- "gt":"39134c54afef1e0b19228627406614e9",
- "challenge":"d2ddea18f00665ce8623e36bd4e3c7c543"
+ "success":0,
+ "gt":"39134c54afef1e0b19228627406614e9",
+ "challenge":"d2ddea18f00665ce8623e36bd4e3c7c543"
}
```
+
success = 0 表示启用 offline 验证模式。
-#### 2.3 POST http://sh.gsxt.gov.cn/notice/security/verify_ip
-返回200 True,表示成功。
-#### 2.4 POST http://sh.gsxt.gov.cn/notice/security/verify_keyword
-返回200 True,表示成功。
-#### 2.5 POST http://sh.gsxt.gov.cn/notice/pc-geetest/validate
+
+#### 2.3 POST http://sh.gsxt.gov.cn/notice/security/verify_ip
+
+返回200 True,表示成功。
+
+#### 2.4 POST http://sh.gsxt.gov.cn/notice/security/verify_keyword
+
+返回200 True,表示成功。
+
+#### 2.5 POST http://sh.gsxt.gov.cn/notice/pc-geetest/validate
+
上传滑块验证码的本地验证结果数据,简称validate。返回JSON数据
-```
+
+```json
{
- "status":"success",
- "version":"3.3.0"
+ "status":"success",
+ "version":"3.3.0"
}
-```
-offline模式,后台根本不知道浏览器使用哪张滑块验证码图片,只知道浏览器上传了验证结果的数据。所以完全可以省略下载验证码图片,进行图像识别,计算滑块移动位置,模拟鼠标滑动轨迹这些步骤。
-**验证数据的格式**
-例如:1517aab3f_51aa460f_75555a6a38,其中以_分隔的3段数据是由geetest.5.x.x.js中的distance, rand0, rand1加密混淆得到。
-具体加密过程分析详见 [寻找阿登高地——爬虫工程师如何绕过验证码](http://www.jianshu.com/p/5b6fb04ea686) 。
-其实不用关心加密算法的实现细节,只需找到JavaScript调用入口,传入参数执行即可:
```
+
+offline模式,后台根本不知道浏览器使用哪张滑块验证码图片,只知道浏览器上传了验证结果的数据。所以完全可以省略下载验证码图片,进行图像识别,计算滑块移动位置,模拟鼠标滑动轨迹这些步骤。
+
+**验证数据格式**
+
+例如:`1517aab3f_51aa460f_75555a6a38`,其中以 `_` 分隔的3段数据是由 `geetest.5.x.x.js` 中的 distance, rand0, rand1 加密混淆得到。
+具体加密过程分析详见 [寻找阿登高地——爬虫工程师如何绕过验证码](http://www.jianshu.com/p/5b6fb04ea686) 。
+其实不用关心加密算法的实现细节,只需找到JavaScript调用入口,传入参数执行即可:
+
+```javascript
function userresponse(a, b) {
- for (var c = b.slice(32), d = [], e = 0; e < c.length; e++) {
- var f = c.charCodeAt(e);
- d[e] = f > 57 ? f - 87 : f - 48
- }
- c = 36 * d[0] + d[1];
- var g = Math.round(a) + c; b = b.slice(0, 32);
- var h, i = [ [], [], [], [], [] ], j = {}, k = 0; e = 0;
- for (var l = b.length; e < l; e++)
- h = b.charAt(e), j[h] || (j[h] = 1, i[k].push(h), k++, k = 5 == k ? 0 : k);
- for (var m, n = g, o = 4, p = "", q = [1, 2, 5, 10, 50]; n > 0;)
- n - q[o] >= 0 ? (m = parseInt(Math.random() * i[o].length, 10), p += i[o][m], n -= q[o]) : (i.splice(o, 1), q.splice(o, 1), o -= 1);
- return p
+ for (var c = b.slice(32), d = [], e = 0; e < c.length; e++) {
+ var f = c.charCodeAt(e);
+ d[e] = f > 57 ? f - 87 : f - 48
}
+ c = 36 * d[0] + d[1];
+ var g = Math.round(a) + c; b = b.slice(0, 32);
+ var h, i = [ [], [], [], [], [] ], j = {}, k = 0; e = 0;
+ for (var l = b.length; e < l; e++)
+ h = b.charAt(e), j[h] || (j[h] = 1, i[k].push(h), k++, k = 5 == k ? 0 : k);
+ for (var m, n = g, o = 4, p = "", q = [1, 2, 5, 10, 50]; n > 0;)
+ n - q[o] >= 0 ? (m = parseInt(Math.random() * i[o].length, 10), p += i[o][m], n -= q[o]) : (i.splice(o, 1), q.splice(o, 1), o -= 1);
+ return p
+}
```
-**3个参数的正确生成**
+
+**3个参数的正确生成**
+
distance,rand0,rand1,这3个参数都是随机生成,但是如果写代码直接随机生成,会发现验证成功率不高,那么这3个参数之间存在什么隐藏关联关系?后台是如何校验这3个随机数的正确性?
其实它们之间存在什么关系不重要,重要的是能够成功通过验证。
只需人工采样N次,构造足够的样本数组,每次随机选取1个,调用JavaScript加密方法,得到验证数据即可。
-#### 2.6 POST http://sh.gsxt.gov.cn/notice/search/ent_info_list
+
+#### 2.6 POST http://sh.gsxt.gov.cn/notice/search/ent_info_list
+
上传session.token(步骤1获得),challenge(步骤2获得),validate(步骤5计算),keyword(查询关键字),返回HTML页面,解析DOM结构,即可获得查询结果和session.token的更新(用于下一次查询)。
-### 3. 源码见GitHub
-https://github.com/9468305/python-script/tree/master/geetest_offline, Python 3.6
-Install:
-```
+
+### 3. 源码
+
+Python 3.6
+
+Install:
+
+```bash
pip install requests # HTTP Request库
pip install PyExecJS # Python调用JavaScript, 配合node.js更佳
pip install beautifulsoup4 # 解析HTML页面
```
+
Demo:
-```
+
+```bash
python ./geetest_offline.py
python ./geetest_offline_nm.py
```
-Entry Code:
-[geetest_offline.py](/geetest_offline/geetest_offline.py) for 上海,河北。
-[geetest_offline_nm.py](/geetest_offline/geetest_offline_nm.py) for 内蒙古,HTTP Request&Response 略有不同。
+
+#### Entry Code
+
+[geetest_offline.py](/geetest_offline/geetest_offline.py) for 上海,河北。
+
+[geetest_offline_nm.py](/geetest_offline/geetest_offline_nm.py) for 内蒙古,HTTP Request&Response 略有不同。
diff --git a/geetest_offline/README_gd.md b/geetest_offline/README_gd.md
index 49ebc28..b97a635 100644
--- a/geetest_offline/README_gd.md
+++ b/geetest_offline/README_gd.md
@@ -1,9 +1,8 @@
-这次的爬虫换个目标,不仅仅抓取统一社会信用代码(税号),还要抓取企业基础信息。
-目标网站设定为 http://gd.gsxt.gov.cn 国家企业信用信息公示系统(广东)。
+这次的爬虫换个目标,不仅仅抓取统一社会信用代码(税号),还要抓取企业基础信息。目标网站设定为 http://gd.gsxt.gov.cn 国家企业信用信息公示系统(广东)。
-首页依然是采用GeeTest滑块验证码Offline模式验证,应对方案详见[《Python破解GeeTest滑块验证码offline V5.10.10》](http://www.jianshu.com/p/7623ff64ee54)。
-校验过程省略,以搜索关键字“腾讯科技”为例,第一步获得如下数据。
-```
+首页依然是采用GeeTest滑块验证码Offline模式验证,校验过程内容省略。以搜索关键字“腾讯科技”为例,第一步获得如下数据。
+
+```python
[
('广州腾讯科技有限公司',
'http://gsxt.gzaic.gov.cn/aiccips/GSpublicity/GSpublicityList.html?service=entInfo_nPNw57QPCnL961TNeXO4Gqc/FgBy7ESTwWPrP4zJe5g=-FBrJ/suNwXMupXtmIUvNKg=='),
@@ -27,20 +26,24 @@
'https://www.szcredit.org.cn/GJQYCredit/GSZJGSPTS/QYGS.aspx?rid=6B553DC2860F51DD7501B40D8BFA3C22E27771C25B8DF96FD1F35DF7C350F5A9')
]
```
-**吐槽:企业详细信息页面分3种。每种网站又有不同的网页模板,因此解析HTML页面元素需要分别处理。深圳的网页模板是一套,广州和其他的是另一套。所以需要区分2种DOM树进行解析。**
+
+**吐槽:企业详细信息页面分3种。每种网站又有不同的网页模板,因此解析HTML页面元素需要分别处理。深圳的网页模板是一套,广州和其他的是另一套。所以需要区分2种DOM树进行解析。**
+ 深圳 https://www.szcredit.org.cn
+ 广州 http://gsxt.gzaic.gov.cn/aiccips/GSpublicity/GSpublicityList.html
+ 其他 http://gd.gsxt.gov.cn/aiccips/CheckEntContext/../GSpublicity/GSpublicityList.html
使用 BeautifulSoup 解析搜索结果页面后,需要判断URL:
+
```Python
_url = _company['href']
if _url.startswith('../'):
_url = INDEX + '/aiccips/CheckEntContext/' + _url
```
+
最终整理得到数据:
-```
+
+```python
{
'注册号/统一社会信用代码': '91440101327598294H',
'注册资本': '0',
@@ -55,8 +58,11 @@ if _url.startswith('../'):
'经营范围': '电子、通信与自动控制技术研究、开发;网络技术的研究、开发;计算机技术开发、技术服务;软件服务;软件测试服务;软件批发;软件零售;软件开发;游戏软件设计制作;信息技术咨询服务;数据处理和存储服务;(依法须经批准的项目,经相关部门批准后方可开展经营活动)〓'
}
```
-**吐槽+1:〓是什么鬼?**
+
+**吐槽+1:〓是什么鬼?**
+
由于这些网站性能极差,默认的15秒超时经常失败,因此在每次网络请求之上添加保护,对于可以多次重试的请求,添加循环等待。
+
```Python
def safe_query_detail(url):
'''Safe query url, handle network timeout and retry multi times.'''
@@ -69,13 +75,15 @@ def safe_query_detail(url):
time.sleep(5)
return None
```
-**吐槽+2:降低网站性能也是一种非常有效的反爬技术。**
-2017.12.05 记录
-从http://gd.gsxt.gov.cn 查询“深圳兴腾讯科技有限公司”,跳转链接失败,服务器500错误。
-https://www.szcredit.org.cn/GJQYCredit/GSZJGSPTS/QYGS.aspx?rid=6B553DC2860F51DD8179F9821CA72F8094E73CE96BD2D49EC7C4690757FA61D9
+**吐槽+2:降低网站性能也是一种非常有效的反爬技术。**
+
+2017.12.05 更新
+
+从 http://gd.gsxt.gov.cn 查询“深圳兴腾讯科技有限公司”,跳转链接失败,服务器500错误。
+> https://www.szcredit.org.cn/GJQYCredit/GSZJGSPTS/QYGS.aspx?rid=6B553DC2860F51DD8179F9821CA72F8094E73CE96BD2D49EC7C4690757FA61D9
+
从 https://www.szcredit.org.cn ,查询和跳转正常。
-https://www.szcredit.org.cn/web/gspt/newGSPTDetail3.aspx?ID=2e82a6a7aaec419884738d2421e7a838
+> https://www.szcredit.org.cn/web/gspt/newGSPTDetail3.aspx?ID=2e82a6a7aaec419884738d2421e7a838
-**吐槽+3:这都是什么运维水平?**
-最后,[源码见GitHub](https://github.com/9468305/script/blob/master/geetest_offline/geetest_offline_gd.py)。
+**吐槽+3:这都是什么运维水平?**
diff --git a/geetest_offline/util.py b/geetest_offline/util.py
index 1f65206..8af60f4 100644
--- a/geetest_offline/util.py
+++ b/geetest_offline/util.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/local/bin/python3
# -*- coding: utf-8 -*-
'''
geetest常用公共方法
diff --git a/geetest_online/README.md b/geetest_online/README.md
index 4972fbb..c173649 100644
--- a/geetest_online/README.md
+++ b/geetest_online/README.md
@@ -1,29 +1,33 @@
### GeeTest滑块验证码online模式的破解
-继续以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)为例。补充一个完成度80%的项目和文档。代码实现主要参考[https://zhuanlan.zhihu.com/windev](https://zhuanlan.zhihu.com/windev)的相关分析文章。
-已实现以下功能:
+
+继续以[国家企业信用信息公示系统](http://www.gsxt.gov.cn)为例。补充一个完成度80%的项目和文档。代码实现主要参考[https://zhuanlan.zhihu.com/windev](https://zhuanlan.zhihu.com/windev)的相关分析文章。
+
+### 已实现功能
#### 1. 所有HTTP Request & Response协议
-使用`requests`库实现。
+
+使用`requests`库。
#### 2. 验证码图片的拼图重组和识别
+
使用`Pillow`库,实现滑块拼图位置的精确定位。
-全局变量`IMAGE_DEBUG`,实现不同精准度的图片本地临时文件存储,以便观察定位效果和改进。
+全局变量`IMAGE_DEBUG`,实现不同精准度的图片本地临时文件存储,以便观察定位效果和改进。
#### 3. GeeTest Javascript 加解密算法破解
+
使用`PyExecJS`库,执行GeeTest Javascript方法,获得正确的明文和密文。
-配合`NodeJS`使用更佳。
+配合`NodeJS`使用更佳。
#### 4. 使用`BeautifulSoup4`库,进行网页数据解析
-#### 未完成的20%包括2部分。
+### 未完成的20%
+
+ 完善用户鼠标轨迹运行的数据仿真算法。
-+ 补全官网针对爬虫返回HTTP 521的处理,补全Cookie校验逻辑。
++ 补全官网针对爬虫返回 HTTP 521 的处理,补全Cookie校验逻辑。
-#### 源码见GitHub
-https://github.com/9468305/python-script/tree/master/geetest_online
+### Python Dependence
-#### Dependence
-```
+```bash
pip install requests
pip install Pillow
pip install PyExecJS
diff --git a/gitstats/README.md b/gitstats/README.md
index 1b44b0c..72a469b 100644
--- a/gitstats/README.md
+++ b/gitstats/README.md
@@ -1,8 +1,11 @@
-Python实现的一个小工具,用于分析Git commit log,获得Git Project每个成员的简单行为数据。
+Python实现的一个小工具,用于分析Git commit log,获得Git Project每个成员的简单行为数据。
+
**Warning:代码量不能代表程序员能力水平!**
### 启动参数
+
共5个。
+
+ Repo地址
+ Commit 起始日期
+ Commit 结束日期
@@ -10,27 +13,30 @@ Python实现的一个小工具,用于分析Git commit log,获得Git Project
+ 统计分析结果CSV文件目标路径
### exec_git
-Git Log命令:
+
+Git Log命令:
+
> git -C {} log --since={} --until={} --pretty=tformat:%ae --shortstat --no-merges -- {} > {}
-填入参数,调用系统命令'os.system()',输出结果至本地临时文件。读取至内存,简单的String Array。
+填入参数,调用系统命令 `os.system()`,输出结果至本地临时文件。读取至内存,简单的String Array。
### parse
+
Git Log输出有3种格式,对应3种正则表达式。
+
```Python
REPATTERN_FULL = r"\s(\d+)\D+(\d+)\D+(\d+)\D+\n"
REPATTERN_INSERT_ONLY = r"\s(\d+)\D+(\d+)\sinsertion\D+\n"
REPATTERN_DELETE_ONLY = r"\s(\d+)\D+(\d+)\sdeletion\D+\n"
```
-遍历得到的数据,首先构造一个以Author为Key,分析结果为Value的字典。
-分析结果构造一个元祖,包括:
+
+遍历得到的数据,首先构造一个以Author为Key,分析结果为Value的字典。分析结果构造一个元祖,包括:
+
+ Commit 次数
+ 增加代码行数
+ 删除代码行数
+ 变更代码行数
### save_csv
-简单省略。
-### 源码见GitHub
-https://github.com/9468305/python-script/tree/master/gitstats
+简单省略。
diff --git a/gsxt_mobile/README.md b/gsxt_mobile/README.md
index 5021d51..d3afe86 100644
--- a/gsxt_mobile/README.md
+++ b/gsxt_mobile/README.md
@@ -1,21 +1,30 @@
[国家企业信用信息公示系统](http://www.gsxt.gov.cn)使用GeeTest滑块验证码。主站使用online验证模式,难破解。部分分站使用offline验证模式,易破解但多次HTTP请求应答往复,查询效率低。
[国家工商总局](http://www.saic.gov.cn/)提供了Android,iOS App,这次就来尝试分析一下App的情况。
-总局网站有2套:
-新版 http://www.saic.gov.cn/
-旧版 http://old.saic.gov.cn/
-于是App下载说明页面也有2套:
-新版 http://gzhd.saic.gov.cn/gszj/index/telephone/android2.html
-旧版 http://gzhd.saic.gov.cn/gszj/index/telephone/android.html
-**威武**
-还好App只有1套。
-国家工商行政管理总局移动版客户端:
-Android版 http://gzhd.saic.gov.cn/gszj/saicwap.apk
-iOS版 https://itunes.apple.com/cn/app/gong-shang-zong-ju/id725956822?mt=8
-国家企业信用信息公示系统:
-Android版 http://gzhd.saic.gov.cn/gszj/gongshi.apk
-iOS版 https://itunes.apple.com/cn/app/%E5%9B%BD%E5%AE%B6%E4%BC%81%E4%B8%9A%E4%BF%A1%E7%94%A8%E4%BF%A1%E6%81%AF%E5%85%AC%E7%A4%BA%E7%B3%BB%E7%BB%9F/id1048375712?mt=8
+
+总局网站有2套:
+
++ 新版 http://www.saic.gov.cn/
++ 旧版 http://old.saic.gov.cn/
+
+于是App下载说明页面也有2套:
+
++ 新版 http://gzhd.saic.gov.cn/gszj/index/telephone/android2.html
++ 旧版 http://gzhd.saic.gov.cn/gszj/index/telephone/android.html
+
+还好App只有1套。
+
+国家工商行政管理总局移动版客户端:
+
++ Android版 http://gzhd.saic.gov.cn/gszj/saicwap.apk
++ iOS版 https://itunes.apple.com/cn/app/gong-shang-zong-ju/id725956822?mt=8
+
+国家企业信用信息公示系统:
+
++ Android版 http://gzhd.saic.gov.cn/gszj/gongshi.apk
++ iOS版 https://itunes.apple.com/cn/app/%E5%9B%BD%E5%AE%B6%E4%BC%81%E4%B8%9A%E4%BF%A1%E7%94%A8%E4%BF%A1%E6%81%AF%E5%85%AC%E7%A4%BA%E7%B3%BB%E7%BB%9F/id1048375712?mt=8
### 分析
+
**saicwap.apk,看这个名称,好像已经明白了什么。**
安装&运行&解包查看`国家企业信用信息公示系统Android APK`文件。
UI交互体验基本上就是一个网页。
@@ -23,9 +32,11 @@ dex很小,assets文件很多。
根据名称搜索加猜测,直接得出结论,WebView外壳,JQuery+AJAX实现网页。
使用Fiddler抓包,仅有一条简单的HTTP Request & Response。
Response是标准JSON文本。
-
-随手写个实现
-### 填写Android Mobile HTTP Header参数。
+
+随手写个实现
+
+### 填写Android Mobile HTTP Header参数
+
```Python
URL = 'http://yd.gsxt.gov.cn/QuerySummary'
MOBILE_ACTION = 'entSearch'
@@ -40,7 +51,9 @@ XRW = 'com.zongjucredit'
ORIGIN = 'file://'
CHARSET = 'application/x-www-form-urlencoded; charset=UTF-8'
```
-### 使用requests库。
+
+### 使用requests库
+
```Python
def query(keyword):
_data = [('mobileAction', MOBILE_ACTION),
@@ -62,9 +75,11 @@ def query(keyword):
_content = _response.json()
print(json.dumps(_content, indent=2, sort_keys=True, ensure_ascii=False))
```
-### 测试运行
-搜索关键字`腾讯科技`,得到[50条数据](https://github.com/9468305/python-script/blob/master/gsxt_mobile/%E8%85%BE%E8%AE%AF%E7%A7%91%E6%8A%8050.txt)。
-格式示例:
+
+### 测试运行
+
+搜索关键字`腾讯科技`,得到[50条数据](https://github.com/9468305/python-script/blob/master/gsxt_mobile/%E8%85%BE%E8%AE%AF%E7%A7%91%E6%8A%8050.txt)。格式示例:
+
```JSON
{
"BUSEXCEPTCOUNT": "0",
@@ -81,9 +96,5 @@ def query(keyword):
"UNISCID": "915101006771521538"
}
```
-
-**实测,有时会封IP,24小时解禁,一旦封禁,爬虫和官方App一概屏蔽。**
-**威武+1。**
-### 源码见GitHub
-https://github.com/9468305/python-script/tree/master/gsxt_mobile
+**实测,有时会封IP,24小时解禁,一旦封禁,爬虫和官方App一概屏蔽。**
diff --git a/lagou/README.md b/lagou/README.md
index 4bd2245..5ba4b61 100644
--- a/lagou/README.md
+++ b/lagou/README.md
@@ -1,13 +1,18 @@
关于拉勾网数据采集爬虫的文章,网上已经写烂了。这里简单记录一个之前帮助同事妹子写的小爬虫工具。
某天,HR同事妹子接到一个任务,收集并分析拉勾网BAT三家公司所有招聘岗位的分类,要求,薪酬范围,人数等信息。
-人肉采集辛苦枯燥,随手写段代码搭救妹子。
+人肉采集辛苦枯燥,随手写段代码搭救妹子。
+
### 开始
-``拉勾网页面可能改版,以下代码实现可能已失效,不考虑持续维护更新。``
+
+拉勾网页面可能改版,以下代码实现可能已失效,不考虑持续维护更新。
拉勾网给每家注册公司分配一个数字,URL形式是:
+
```Python
LAGOU_URL = r'https://www.lagou.com/gongsi/j%d.html'
```
+
人肉筛选目标公司如下:
+
```Python
COMPANY = {
'腾讯': 451,
@@ -20,21 +25,27 @@ COMPANY = {
'百度外卖': 104601
}
```
+
每家公司子页面的实现,使用了较多复杂Javascript代码和框架,因此不采用抓包分析HTTP协议的方案。
-简单粗暴直接的组合: Selenium + WebDriver + Chrome。
+简单粗暴直接的组合: Selenium + WebDriver + Chrome。
+
### Selenium
+
官网 http://www.seleniumhq.org/
GitHub https://github.com/SeleniumHQ/selenium
文档 http://selenium-python.readthedocs.io/
### ChromeDriver
+
[ChromeDriver - WebDriver for Chrome](https://sites.google.com/a/chromium.org/chromedriver/)
为什么不使用运行效率更高的 [PhantomJS](http://phantomjs.org/) ?
因为需要频繁调试代码和观察运行情况。稳定运行后可以随时修改一行代码参数,替换成 PhantomJS 。
**Chrome 59 beta 开始支持 Headless。** 详见 [Getting Started with Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome)。所以以后应该也不再需要 PhantomJS 了。
### 数据定义
+
继续简单粗暴直接:(参数有点多,PyLint 报 Warning 了,无视吧。)
+
```Python
class JobInfo(object):
'''Job Info Object'''
@@ -64,11 +75,15 @@ class JobInfo(object):
```
### 页面加载解析
+
WebDriver API 方便好用强大。
+
```Python
con_list_item = WebDriverWait(browser, SLEEPTIME).until(lambda x: x.find_elements_by_class_name('con_list_item'))
```
+
执行点击翻页跳转
+
```Python
try:
pages = browser.find_element_by_class_name('pages')
@@ -81,19 +96,21 @@ except NoSuchElementException as _e:
print(_e)
```
-**数据采集完成后写入csv文件,略。**
+**数据采集完成后写入csv文件,略。**
+
### 坑
+
WebDriver API 简单易用,但超时处理机制仍不完善。
+
```Python
browser = webdriver.Chrome()
browser.get(url)
browser.refresh()
browser.quit()
```
-`implicitly_wait()`无法判断页面内部各种Ajax操作执行完成的时机。只好注释掉这一行代码。
+
+`implicitly_wait()` 无法判断页面内部各种Ajax操作执行完成的时机。只好注释掉这一行代码。
+
```Python
browser.implicitly_wait(TIMEOUT)
```
-
-### 源码见GitHub
-https://github.com/9468305/python-script/tree/master/lagou
diff --git a/level/README.md b/level/README.md
index 283a422..0c30887 100644
--- a/level/README.md
+++ b/level/README.md
@@ -1,17 +1,24 @@
-### Python leveldb Utils 常用方法封装。
-[leveldb](http://leveldb.org/)是Google开源的一个轻量级,高性能,KeyValue 存储数据库。作者是Google战神Jeff Dean,基于他自己的BigTable论文,使用C++ POSIX实现。
+### Python leveldb Utils 常用方法封装。
+
+[leveldb](http://leveldb.org/)是Google开源的一个轻量级,高性能,KeyValue 存储数据库。作者是Google战神Jeff Dean,基于他自己的BigTable论文,使用C++ POSIX实现。
+
> LevelDB is a light-weight, single-purpose library for persistence with bindings to many platforms.
官网 http://leveldb.org/
GitHub https://github.com/google/leveldb
官方 Javascript Binding https://github.com/Level/levelup
+
### Python Binding
+
早期官方仅提供C++和Javascript。Python实现均是第三方开发。其中使用较广泛和稳定的是 https://github.com/rjpower/py-leveldb 。目前处于稳定运行,维护停滞状态。
在Python爬虫实现过程中,常常需要快速简单处理存储大量数据至本地文件,构建SQL数据库表过于复杂,变更不灵活。使用JSON文本格式,缺乏索引,过滤,随机增删数据。因此leveldb是一种轻便快捷的最佳解决方案。
这里封装了一些常用方法,均是日常爬虫数据采集存储的常用方法。
+
### exist()
+
判断key是否存在于database中。返回Boolean值。
+
```Python
def exist(db_src, key):
try:
@@ -22,7 +29,9 @@ def exist(db_src, key):
```
### count()
+
统计database中数据总量计数,支持key过滤子字符串,value过滤子字符串,返回总数和过滤后有效总数。
+
```Python
def count(db_src, k_filter, v_filter):
total, valid = 0, 0
@@ -39,7 +48,9 @@ def count(db_src, k_filter, v_filter):
```
### copy()
+
从源库到目标库,拷贝database,支持key过滤子字符串。返回源库总数和过滤后有效拷贝总数。
+
```Python
def copy(db_src, db_dst, k_filter):
total, valid = 0, 0
@@ -56,7 +67,9 @@ def copy(db_src, db_dst, k_filter):
```
### delete()
+
删除目标库中与源库相同key的数据项。
+
```Python
def delete(db_src, db_dst):
for _k, _v in db_src.RangeIter():
@@ -64,7 +77,9 @@ def delete(db_src, db_dst):
```
### diff()
+
查找源库与目标库的key值差异数据项,存储至差异库。返回差异项总数。
+
```Python
def diff(db_src, db_dst, db_diff):
diff_count = 0
@@ -76,7 +91,9 @@ def diff(db_src, db_dst, db_diff):
```
### clean_copy()
+
拷贝源库至目标库,并删除value值为空的数据项。返回拷贝总数。
+
```Python
def clean_copy(db_src, db_dst):
total = 0
@@ -89,8 +106,10 @@ def clean_copy(db_src, db_dst):
```
### dump()
+
打印输出当前数据库中所有key value数据。
安全兼容:当参数是字符串时,当作本地路径文件名处理,临时打开数据库。
+
```Python
def dump(db_src):
_db = leveldb.LevelDB(db_src, create_if_missing=False) if isinstance(db_src, str) else db_src
@@ -99,8 +118,10 @@ def dump(db_src):
```
### db_to_text()
+
导出leveldb数据库至文本文件,以','分隔。
安全兼容:当参数是字符串时,当作本地路径文件名处理,临时打开数据库。
+
```Python
def db_to_text(from_db, to_text):
_db = leveldb.LevelDB(from_db, create_if_missing=False) if isinstance(from_db, str) else from_db
@@ -110,8 +131,10 @@ def db_to_text(from_db, to_text):
```
### text_to_db()
+
从文本文件导入至leveldb数据库。参数支持自定义分隔符。
安全兼容:当参数是字符串时,当作本地路径文件名处理,临时打开数据库。
+
```Python
def text_to_db(from_text, to_db, split_char):
total, invalid = 0, 0
@@ -134,9 +157,11 @@ def text_to_db(from_text, to_db, split_char):
```
### db_to_excel()
+
导出leveldb数据库至Excel文件,返回总数。
Excel文件共2列,分别对应leveldb的Key,Value。
安全兼容:当参数是字符串时,当作本地路径文件名处理,临时打开数据库。
+
```Python
def db_to_excel(from_db, to_excel):
_db = leveldb.LevelDB(from_db, create_if_missing=False) if isinstance(from_db, str) else from_db
@@ -151,9 +176,11 @@ def db_to_excel(from_db, to_excel):
```
### excel_to_db()
+
从Excel文件导入至leveldb数据库。
仅读取Excel文件中的前2列数据,对应leveldb的Key,Value。
安全兼容:当参数是字符串时,当作本地路径文件名处理,临时打开数据库。
+
```Python
def excel_to_db(from_excel, to_db):
_wb = load_workbook(from_excel, read_only=True)
@@ -175,9 +202,8 @@ def excel_to_db(from_excel, to_db):
_wb.close()
return total
```
-### 源码见GitHub
-https://github.com/9468305/python-script/tree/master/level
### 后记
+
这篇文档整理完成时,leveldb官网已经推出官方Python版本。
详见 https://plyvel.readthedocs.io/en/latest/
diff --git a/monkeyrunner/README.md b/monkeyrunner/README.md
new file mode 100644
index 0000000..d8f0a21
--- /dev/null
+++ b/monkeyrunner/README.md
@@ -0,0 +1,251 @@
+# MonkeyRunner is DEAD
+
+## UI Automator
+
+https://developer.android.com/training/testing/ui-automator
+
+Android 平台所有自动化测试框架的底层实现都依赖官方提供的 UI Automator 测试框架,适用于跨系统和已安装应用程序的跨应用程序功能UI测试。主要功能包括三部分:
+
++ UI Automator Viewer 检查布局层次结构的查看器。
++ UiDevice 设备状态信息并在目标设备上执行操作的API。
++ UI Automator API 支持跨应用程序UI测试的API。
+
+## UI Automator Viewer
+
+PC 端 GUI 工具,扫描和分析 Android 设备上当前显示的 UI 组件。展示 UI 布局层次结构,查看设备上当前对用户可见的 UI 组件的属性。从名称可以看出,它是 UI Automator 的只读功能部分,即只能查看 UI 组件的树形结构和属性,不能操作控制 UI 组件。
+
+`uiautomatorviewer` 位于 `/tools/bin` 目录。
+启动入口是一个bash文件,实际调用 `/tools/lib` 目录的 `uiautomatorviewer-26.0.0-dev.jar` 。
+GUI 基于 Eclipse + SWT 实现,使用 Gradle 构建。
+系列工具源码在 `https://android.googlesource.com/platform/tools/swt/` 。
+依赖 `https://android.googlesource.com/platform/tools/base/` 。
+活跃分支: `mirror-goog-studio-master-dev` 。
+该仓库还包含以下工具。
+
++ chimpchat
++ ddms
++ hierarchyviewer2
++ monkeyrunner
++ swtmenubar
++ traceview
+
+其内部实现基于 `adb shell uiautomator dump` 。从源码仓库提交记录看,主要功能开发的活跃时间是 2014-2015,2016之后已经很少更新维护。那个年代的 Android 开发主要使用 Eclipse , 所以基于 SWT 实现多平台 PC GUI ,在当时合理。
+
+该工具实际使用运行不稳定,极易报错。
+> `Error while obtaining UI hierarchy XML file: com.android.ddmlib.SyncException: Remote object doesn't exist!`
+
+错误原因通常是:
+
++ adb 连接通道不稳定。
++ 机型兼容性问题,权限问题。
++ 当前手机应用程序界面处于动态,例如播放视频,动画。并且10秒超时时间仍未进入静态。
+
+分析源码可知,错误都源于 `Android Framework uiautomator` 。
+
+## MonkeyRunner
+
+https://developer.android.com/studio/test/monkeyrunner
+
+官方提供的另外一个工具,封装 uiautomator API,供 Python 脚本调用,也可注入 java 扩展插件。
+相比 `uiautomatorviewer` 和 `uiautomator` 命令行工具,可编程扩展性更佳。
+MonkeyRunner 使用了比较冷门的 Jython 实现。
+
+### 1. 启动运行入口
+
+> monkeyrunner -plugin
+
+monkeyrunner 是一个bash文件,位于 `/tools/bin` ,启动调用 `/tools/lib/monkeyrunner-26.0.0-dev.jar` 。
+
+```bash
+export ANDROID_HOME="~/Library/Android/sdk"
+$ANDROID_HOME/tools/bin/monkeyrunner uiparser.py
+```
+
+### 2. 主要方法
+
+#### MonkeyDevice.getProperty()
+
+等同于调用 `adb shell getprop ` 。获取设备系统环境变量。
+不同厂商的设备,key可能不同。针对具体测试机型,可使用 `adb shell getprop` ,显示所有系统环境变量的key。
+
+#### MonkeyDevce.shell()
+
+等同于调用`adb shell`命令。
+
+### 3. 缺陷
+
+MonkeyRunner 基于 Jython 2.5.3 。看上去结合了Java和Python的优势,实际对于Java和Python编程都不友好。
+
++ Jython 2.5.3 过时,主流的Python 3.x和2.7的很多语法和库无法使用。
++ 使用vscode等编辑器编码时,缺少智能提示和自动补全。编辑器和pylint无法识别导入的库, 例如 `from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice, MonkeyImage` 。
++ Jython 似乎不能像常规的python程序一样引用外部库。实测只能使用 MonkeyRunner 内置的 `os, sys, subprocess` 等库。
++ Java extend plugin 能做的事情较少。
+
+MonkeyRunner 实际仍然是使用 `adb shell` 和其中的 `uiautomator` 命令获取UI组件状态和属性。所以它跟 `UI Automator Viewer` 一样受限于 `uiautomator` 本身的缺陷,导致运行不稳定。
+
+## adb shell uiautomator
+
+**adb**
+https://developer.android.google.cn/studio/command-line/adb
+
+**adb shell am**
+https://developer.android.google.cn/studio/command-line/adb#am
+使用 Activity Manager (am) 工具发出命令以执行各种系统操作,如启动 Activity、强行停止进程、广播 intent、修改设备屏幕属性及其他操作。
+
+**adb shell pm**
+https://developer.android.google.cn/studio/command-line/adb#pm
+使用软件包管理器 Package Manager (pm) 工具发出命令,安装,卸载,查询安装包。
+
+**adb shell uiatomator**
+官网相关页面已被删除,仅能从搜索引擎历史快照中找到。猜测可能近期会有变更,或者官方建议不再使用。
+通过执行命令可以查看使用方法和参数。
+
+```bash
+Usage: uiautomator [options]
+
+Available subcommands:
+
+help: displays help message
+
+runtest: executes UI automation tests
+ runtest [options]
+ : < -c | -e class >
+ : a list of jar files containing test classes and dependencies. If
+ the path is relative, it's assumed to be under /data/local/tmp. Use
+ absolute path if the file is elsewhere. Multiple files can be
+ specified, separated by space.
+ : a list of test class names to run, separated by comma. To
+ a single method, use TestClass#testMethod format. The -e or -c option
+ may be repeated. This option is not required and if not provided then
+ all the tests in provided jars will be run automatically.
+ options:
+ --nohup: trap SIG_HUP, so test won't terminate even if parent process
+ is terminated, e.g. USB is disconnected.
+ -e debug [true|false]: wait for debugger to connect before starting.
+ -e runner [CLASS]: use specified test runner class instead. If
+ unspecified, framework default runner will be used.
+ -e : other name-value pairs to be passed to test classes.
+ May be repeated.
+ -e outputFormat simple | -s: enabled less verbose JUnit style output.
+
+dump: creates an XML dump of current UI hierarchy
+ dump [--verbose][file]
+ [--compressed]: dumps compressed layout information.
+ [file]: the location where the dumped XML should be stored, default is
+ /sdcard/window_dump.xml
+
+events: prints out accessibility events until terminated
+```
+
+### uiautomator 缺陷
+
+运行耗时长,失败率高,频繁报错。
+`ERROR: could not get idle state.` 通常表示当前UI处于动态渲染刷新期间,例如正在播放视频,动画。在10秒超时时间内仍未进入静态。因为此时 UI 树的节点对象快速变化中,不能稳定获取。
+
+### uiautomator 源码
+
+PC端工具源码位于仓库 https://android.googlesource.com/platform/frameworks/testing/ `master` 分支。
+最新更新于 2014.11.14。之后活跃分支变更为 `android-support-test` 分支。`uiautomator` 源码被移除,改成 `android.support.test library, expresso` 等工具的源码工程。
+手机端框架源码位于仓库 https://android.googlesource.com/platform/frameworks/base/ `master` 分支。
+`uiAutomation.waitForIdle(1000, 1000 * 10);` 是报错的关键代码,即单次超时等待1秒,最长超时等待10秒。超时抛出异常。
+
+`DumpCommand.java`
+> https://android.googlesource.com/platform/frameworks/testing/+/master/uiautomator/cmds/uiautomator/src/com/android/commands/uiautomator/DumpCommand.java
+
+```Java
+// It appears that the bridge needs time to be ready. Making calls to the
+// bridge immediately after connecting seems to cause exceptions. So let's also
+// do a wait for idle in case the app is busy.
+try {
+ UiAutomation uiAutomation = automationWrapper.getUiAutomation();
+ uiAutomation.waitForIdle(1000, 1000 * 10);
+ AccessibilityNodeInfo info = uiAutomation.getRootInActiveWindow();
+ if (info == null) {
+ System.err.println("ERROR: null root node returned by UiTestAutomationBridge.");
+ return;
+ }
+ Display display =
+ DisplayManagerGlobal.getInstance().getRealDisplay(Display.DEFAULT_DISPLAY);
+ int rotation = display.getRotation();
+ Point size = new Point();
+ display.getSize(size);
+ AccessibilityNodeInfoDumper.dumpWindowToFile(info, dumpFile, rotation, size.x, size.y);
+} catch (TimeoutException re) {
+ System.err.println("ERROR: could not get idle state.");
+ return;
+} finally {
+ automationWrapper.disconnect();
+}
+System.out.println(
+ String.format("UI hierchary dumped to: %s", dumpFile.getAbsolutePath()));
+```
+
+`UiAutomation.java`
+> https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/UiAutomation.java
+
+```Java
+/**
+ * Waits for the accessibility event stream to become idle, which is not to
+ * have received an accessibility event within idleTimeoutMillis.
+ * The total time spent to wait for an idle accessibility event stream is bounded
+ * by the globalTimeoutMillis.
+ *
+ * @param idleTimeoutMillis The timeout in milliseconds between two events
+ * to consider the device idle.
+ * @param globalTimeoutMillis The maximal global timeout in milliseconds in
+ * which to wait for an idle state.
+ *
+ * @throws TimeoutException If no idle state was detected within
+ * globalTimeoutMillis.
+ */
+public void waitForIdle(long idleTimeoutMillis, long globalTimeoutMillis)
+ throws TimeoutException {
+ synchronized (mLock) {
+ throwIfNotConnectedLocked();
+ final long startTimeMillis = SystemClock.uptimeMillis();
+ if (mLastEventTimeMillis <= 0) {
+ mLastEventTimeMillis = startTimeMillis;
+ }
+ while (true) {
+ final long currentTimeMillis = SystemClock.uptimeMillis();
+ // Did we get idle state within the global timeout?
+ final long elapsedGlobalTimeMillis = currentTimeMillis - startTimeMillis;
+ final long remainingGlobalTimeMillis =
+ globalTimeoutMillis - elapsedGlobalTimeMillis;
+ if (remainingGlobalTimeMillis <= 0) {
+ throw new TimeoutException("No idle state with idle timeout: "
+ + idleTimeoutMillis + " within global timeout: "
+ + globalTimeoutMillis);
+ }
+ // Did we get an idle state within the idle timeout?
+ final long elapsedIdleTimeMillis = currentTimeMillis - mLastEventTimeMillis;
+ final long remainingIdleTimeMillis = idleTimeoutMillis - elapsedIdleTimeMillis;
+ if (remainingIdleTimeMillis <= 0) {
+ return;
+ }
+ try {
+ mLock.wait(remainingIdleTimeMillis);
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ }
+}
+```
+
+## Android Device Monitor
+
+https://developer.android.com/studio/profile/monitor
+
+Android SDK 工具集的 `Android Device Monitor` 已废弃。
+
+>Android Device Monitor was deprecated in Android Studio 3.1 and removed from Android Studio 3.2. The features that you could use through the Android Device Monitor have been replaced by new features. The table below helps you decide which features you should use instead of these deprecated and removed features.
+
+官方给出的替代品 `Layout Inspector` 功能更强大,界面也更美观,但目前还不成熟,相比 iOS 神器 [Reveal](https://revealapp.com/) , 仍需努力。
+https://developer.android.com/studio/debug/layout-inspector
+
+## uiparser
+
+参照 MonkeyRunner 官方文档实现的 Python Demo。
+
+https://github.com/9468305/python-script/tree/master/monkeyrunner
diff --git a/monkeyrunner/uiparser.py b/monkeyrunner/uiparser.py
new file mode 100644
index 0000000..5ebecdf
--- /dev/null
+++ b/monkeyrunner/uiparser.py
@@ -0,0 +1,74 @@
+#! $ANDROID_HOME/tools/bin monkeyrunner
+# -*- coding: utf-8 -*-
+'''uiparser'''
+
+import os
+import sys
+import subprocess
+import datetime
+import logging
+from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice, MonkeyImage #pylint: disable=import-error
+
+class NullHandler(logging.Handler):
+ def emit(self, record):
+ pass
+
+logging.getLogger(__name__).addHandler(NullHandler())
+logging.basicConfig(level=logging.DEBUG)
+
+SHORT = 1
+MIDDLE = 5
+LONG = 15
+
+ADB = os.path.join(os.environ['ANDROID_HOME'], 'platform-tools', 'adb')
+
+# Example of Ctrip Android Apk
+TARGET_PACKAGE = 'ctrip.android.view'
+LAUNCH_ACTIVITY = 'ctrip.business.splash.CtripSplashActivity'
+HOME_ACTIVITY = 'ctrip.android.publicproduct.home.view.CtripHomeActivity'
+FLIGHT_ACTIVITY = 'ctrip.android.flight.view.inland.FlightInquireActivity'
+START_COMPONENT = TARGET_PACKAGE + '/' + LAUNCH_ACTIVITY
+
+DEVICE_DIR = '/sdcard/uiparser/'
+HOST_DIR = './'
+
+
+def capture(device, index):
+ ''''''
+ _dumpXML = DEVICE_DIR + index + '.xml'
+ _localXML = HOST_DIR + index + '.xml'
+ _localImage = HOST_DIR + index + '.png'
+
+ _shell = [ADB, 'shell', 'uiautomator', 'dump', _dumpXML]
+ logging.debug(datetime.datetime.now())
+ subprocess.call(_shell) # Stupid uiautomator, always failed here!
+ logging.debug(datetime.datetime.now())
+ #MonkeyRunner.sleep(MIDDLE)
+
+ _shell = [ADB, 'pull', _dumpXML, _localXML]
+ subprocess.call(_shell)
+
+ _image = device.takeSnapshot()
+ _image.writeToFile(_localImage, 'png')
+
+
+def uiparser():
+ '''Main Entry'''
+ device = MonkeyRunner.waitForConnection(MIDDLE)
+
+ _shell = [ADB, 'shell', 'rm', '-rf', DEVICE_DIR]
+ subprocess.call(_shell)
+
+ _shell = [ADB, 'shell', 'mkdir', '-p', DEVICE_DIR]
+ subprocess.call(_shell)
+
+ device.startActivity(component=START_COMPONENT)
+ MonkeyRunner.sleep(MIDDLE)
+
+ capture(device, str(0))
+
+
+if __name__ == "__main__":
+ # MonkeyRunner Jython version is 2.5.3 (Outdated!)
+ logging.info(sys.version)
+ uiparser()
diff --git a/nacao_v1/README.md b/nacao_v1/README.md
index 8da2194..d5c7139 100644
--- a/nacao_v1/README.md
+++ b/nacao_v1/README.md
@@ -4,6 +4,7 @@
动手编码之前,先来看看该网站的前端设计实现,有哪些值得吐槽的技术点。
### 槽点1:网站开放服务时间 每天12小时
+
>重要公告:网站核查平台服务时间为7*12小时(即每天8:00-20:00)
[知乎:为什么全国组织机构代码管理中心网站(www.nacao.org.cn)只在上班时间开放查询呢?](https://www.zhihu.com/question/33204926)
@@ -13,26 +14,34 @@
第三期,技术人员直接注释了前端Javascript拦截代码。(服务器是否拦截判断,未核实。)
### 槽点2:信息核查系统 vs 信息校核结果公示系统
+
首页从上至下,分为2个查询系统。
第一个是`全国统一社会信用代码信息核查系统`。输入关键字,弹出英文字母图片验证码。
第二个是`全国统一社会信用代码信息校核结果公示系统`。输入关键字,直接跳转结果页面!!!
查询数据结果对比,基本一致。那么谁还去用第一个图片验证码系统???
多次测试后发现,第一个系统还有IP反爬机制,一旦封禁,3工作日解封。第二个系统任意使用无限制。
-```
+
+```txt
IP被封怎么办?
本网站核查平台是为公众基于统一社会信用代码信息的一般性核查设立的,不支持大量且频繁的查询请求,如查询过程中出现下图 所示,说明您的查询过于频繁,系统已经对您进行了查询限制,限制期为3个工作日。请于限制期后再访问系统进行查询。
```
### 槽点3:存在疑似SQL注入漏洞
+
在分析SQL注入漏洞之前,先来看看系统2的爬虫实现,即HTTP参数的含义解释。
-1. 关键字查询
+
+#### 关键字查询
+
GET http://125.35.63.141:8080/PublicNotificationWeb/search.do
参数:searchText = 关键字,searchType = 3。
响应:返回一个字符串Referer。用于query.do的HTTP header参数。
-2. 分页获取数据
+
+#### 分页获取数据
+
POST http://125.35.63.141:8080/PublicNotificationWeb/query.do
-参数:
-```
+参数:
+
+```python
_params = [ ('pageSize', 20),
('searchText', keyword),
('searchType', 3),
@@ -40,7 +49,9 @@ _params = [ ('pageSize', 20),
('sortField', ''),
('currentPage', 1)]
```
-含义:
+
+含义:
+
+ pageSize 分页数据量,默认20,实测可以改成100。即一页返回100项数据。
+ searchText 搜索关键字
+ searchType 搜索类型,固定=3
@@ -49,23 +60,22 @@ _params = [ ('pageSize', 20),
+ currentPage 当前分页索引,实测最多5页,值范围0-4。
响应:返回一个JSON字符串。
+
+ totalPage 总页数,最大5页。
+ foundCount 总共查询结果数量
+ dataList JSON数组
+ JGMC 公司名称
+ TYSHXYDM 统一社会信用代码(税号)
-至此,爬虫已经实现完毕。接下来探索每个参数的可能性。
+至此,爬虫已经实现完毕。接下来探索每个参数的可能性。
+
+ pageSize 从20改成100,最多可一次获得500条数据。
+ searchText 搜索关键字改成*,可获得服务器数据库默认排序数据。此时foundCount等于官网数据库数据总量。
实测数据日志见[sql_injection.txt](https://github.com/9468305/python-script/tree/master/nacao_v1/sql_injection.txt)。
-所以这里能否使用SQLInjection,获得数据库访问权限,直接拖库呢?
-留待有心人探索。
-
-### V1.0源码见GitHub
-https://github.com/9468305/python-script/tree/master/nacao_v1
+所以这里能否使用SQLInjection,获得数据库访问权限,直接拖库呢?留待有心人探索。
### 后记
+
官网进行了改版,原方案接口失效。新方案V2.0:
https://github.com/9468305/python-script/tree/master/nacao_v2
diff --git a/nacao_v2/README.md b/nacao_v2/README.md
index ebb73ab..201e929 100644
--- a/nacao_v2/README.md
+++ b/nacao_v2/README.md
@@ -1,26 +1,31 @@
-书接前文,全国组织机构代码管理中心 http://www.nacao.org.cn 网站改版,V1.0方案已失效。
-继续分析这次改版的变化,以及V2.0方案的实现。
-V1.0方案见 https://github.com/9468305/python-script/tree/master/nacao_v1 。
+书接前文,[全国组织机构代码管理中心](http://www.nacao.org.cn)网站改版,V1.0 方案已失效。继续分析这次改版的变化,以及V2.0方案的实现。
+
### IP变域名
+
125.35.63.141:8080 变成 dmedu.org.cn 。(该域名解析IP仍是 125.35.63.141 )因此2个API接口改变。
-旧:
+
+旧:
+
http://125.35.63.141:8080/PublicNotificationWeb/search.do
http://125.35.63.141:8080/PublicNotificationWeb/query.do
-新:
+
+新:
+
http://www.dmedu.org.cn/search.do
http://www.dmedu.org.cn/query.do
### search.do可绕过
+
search.do接口返回一个字符串Referer,用于query.do的HTTP Header参数。实测不再需要,直接调用query.do,Referer空值。
### 参数一致
-V1.0和V2.0参数没有变化。
-pageSize 20扩大至100 继续可用
-searchText使用*通配符,继续可用
-### 实现
-因此V2.0的代码更简单,源码见GitHub
-https://github.com/9468305/python-script/tree/master/nacao_v2
++ V1.0和V2.0参数没有变化。
++ pageSize 20扩大至100 继续可用
++ searchText使用*通配符,继续可用
+
+**因此V2.0的代码更简单。**
### 后记
-目前官网已经针对该接口增加了图片字符验证码,因此该方案已失效。关于如何识别图片字符验证码,可以Google“远程打码”。
+
+目前官网已经针对该接口增加了图片字符验证码,因此该方案已失效。关于如何识别图片字符验证码,搜索“远程打码”。